upstream: a diagnostics episode follows health, and a peer fault carries its cause
Gates / frontend (push) Successful in 2m5s
Gates / test (push) Successful in 2m43s
Gates / test-aarch64 (push) Successful in 8m19s
Gates / package (push) Successful in 4m21s
Gates / container (push) Successful in 13s
CI / gates (push) Successful in 30m43s
Gates / frontend (push) Successful in 2m5s
Gates / test (push) Successful in 2m43s
Gates / test-aarch64 (push) Successful in 8m19s
Gates / package (push) Successful in 4m21s
Gates / container (push) Successful in 13s
CI / gates (push) Successful in 30m43s
This commit is contained in:
+90
-78
@@ -66,7 +66,7 @@ pub const DohClient = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn client(self: *DohClient) transport.Client {
|
||||
pub fn leaf(self: *DohClient) transport.Leaf {
|
||||
return .{ .ptr = self, .exchangeFn = exchangeFn };
|
||||
}
|
||||
|
||||
@@ -75,12 +75,8 @@ pub const DohClient = struct {
|
||||
io: std.Io,
|
||||
query: []const u8,
|
||||
response_buf: []u8,
|
||||
selected: *?[]const u8,
|
||||
) transport.ExchangeError![]u8 {
|
||||
) transport.LeafError!transport.Outcome {
|
||||
const self: *DohClient = @ptrCast(@alignCast(ptr));
|
||||
// The endpoint outlives the client, so the borrow is safe for the whole
|
||||
// query. Set before the attempt: a failure names this resolver too.
|
||||
selected.* = self.endpoint.url;
|
||||
return self.exchange(io, query, response_buf);
|
||||
}
|
||||
|
||||
@@ -96,7 +92,7 @@ pub const DohClient = struct {
|
||||
io: std.Io,
|
||||
query: []const u8,
|
||||
response_buf: []u8,
|
||||
) transport.ExchangeError![]u8 {
|
||||
) transport.LeafError!transport.Outcome {
|
||||
// `std.http.Client` carries the `std.Io` it was constructed with and
|
||||
// takes none per request, so the interface's `io` is unused here. It
|
||||
// stays in the signature because DoT and the pool need it.
|
||||
@@ -117,22 +113,22 @@ pub const DohClient = struct {
|
||||
// `Request.Headers` has no `accept` field, so this one goes in by
|
||||
// hand.
|
||||
.extra_headers = &.{.{ .name = "accept", .value = media_type }},
|
||||
}) catch |err| return mapError(err, .connect);
|
||||
}) catch |err| return fault(err, .connect);
|
||||
defer req.deinit();
|
||||
|
||||
req.sendBodyComplete(self.request_buf[0..query.len]) catch |err|
|
||||
return mapError(sendCause(&req, err), .send);
|
||||
return fault(sendCause(&req, err), .send);
|
||||
|
||||
// An empty redirect buffer is legal under `.not_allowed`: a redirect
|
||||
// is an error before the location is ever read.
|
||||
var resp = req.receiveHead(&.{}) catch |err| return mapError(headCause(&req, err), .receive);
|
||||
var resp = req.receiveHead(&.{}) catch |err| return fault(headCause(&req, err), .receive);
|
||||
|
||||
if (resp.head.status != .ok) return error.HttpStatus;
|
||||
if (resp.head.status != .ok) return peerFault(error.HttpStatus, error.HttpStatus);
|
||||
// `head.content_type` points into memory that `resp.reader` invalidates,
|
||||
// so the check happens before the body stream starts.
|
||||
if (!contentTypeOk(resp.head.content_type)) return error.HttpContentType;
|
||||
if (!contentTypeOk(resp.head.content_type)) return peerFault(error.HttpContentType, error.HttpContentType);
|
||||
if (resp.head.content_length) |declared| {
|
||||
if (declared > response_buf.len) return error.ResponseTooLarge;
|
||||
if (declared > response_buf.len) return peerFault(error.ResponseTooLarge, error.ResponseTooLarge);
|
||||
}
|
||||
|
||||
const body = resp.reader(self.transfer_buf);
|
||||
@@ -140,7 +136,7 @@ pub const DohClient = struct {
|
||||
var ended = false;
|
||||
while (len < response_buf.len) {
|
||||
const n = body.readSliceShort(response_buf[len..]) catch |err|
|
||||
return mapError(bodyCause(&resp, err), .receive);
|
||||
return fault(bodyCause(&resp, err), .receive);
|
||||
len += n;
|
||||
if (n == 0) {
|
||||
ended = true;
|
||||
@@ -152,12 +148,13 @@ pub const DohClient = struct {
|
||||
// that fits from one that was cut off.
|
||||
var probe: [1]u8 = undefined;
|
||||
const n = body.readSliceShort(&probe) catch |err|
|
||||
return mapError(bodyCause(&resp, err), .receive);
|
||||
if (n != 0) return error.ResponseTooLarge;
|
||||
return fault(bodyCause(&resp, err), .receive);
|
||||
if (n != 0) return peerFault(error.ResponseTooLarge, error.ResponseTooLarge);
|
||||
}
|
||||
|
||||
try transport.validateResponse(query, response_buf[0..len]);
|
||||
return response_buf[0..len];
|
||||
transport.validateResponse(query, response_buf[0..len]) catch |err|
|
||||
return peerFault(err, err);
|
||||
return .{ .reply = response_buf[0..len] };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -177,8 +174,18 @@ const Phase = enum { connect, send, receive };
|
||||
/// `Connection.getReadError` (Client.zig:392), so its record-layer members
|
||||
/// arrive here as themselves. Without them a decode error or a bad record MAC
|
||||
/// would be reported as a plain receive failure.
|
||||
fn mapError(err: anyerror, phase: Phase) transport.ExchangeError {
|
||||
fn fault(err: anyerror, phase: Phase) transport.LeafError!transport.Outcome {
|
||||
if (transport.mapLocal(err)) |local| return local;
|
||||
return peerFault(kindOf(err, phase), err);
|
||||
}
|
||||
|
||||
/// A peer fault as an `Outcome`. Written out rather than inlined at every call
|
||||
/// site so the classification and the cause cannot drift apart by a typo.
|
||||
fn peerFault(kind: transport.PeerFault, cause: anyerror) transport.Outcome {
|
||||
return .{ .fault = .{ .kind = kind, .cause = cause } };
|
||||
}
|
||||
|
||||
fn kindOf(err: anyerror, phase: Phase) transport.PeerFault {
|
||||
switch (err) {
|
||||
error.TlsInitializationFailed,
|
||||
error.CertificateBundleLoadFailure,
|
||||
@@ -209,7 +216,7 @@ fn mapError(err: anyerror, phase: Phase) transport.ExchangeError {
|
||||
comptime {
|
||||
for (@typeInfo(std.crypto.tls.Client.ReadError).error_set.?) |member| {
|
||||
const value: anyerror = @field(std.crypto.tls.Client.ReadError, member.name);
|
||||
if (mapError(value, .receive) != error.TlsFailed) {
|
||||
if (kindOf(value, .receive) != error.TlsFailed) {
|
||||
@compileError("unclassified TLS read cause: " ++ member.name);
|
||||
}
|
||||
}
|
||||
@@ -268,7 +275,7 @@ test "init builds a uri from the endpoint url" {
|
||||
try testing.expectEqualStrings("dns.example", doh.endpoint.host);
|
||||
}
|
||||
|
||||
test "DohClient satisfies the transport.Client interface" {
|
||||
test "DohClient satisfies the transport.Leaf interface" {
|
||||
var http: std.http.Client = undefined;
|
||||
var request_buf: [min_request_buf]u8 = undefined;
|
||||
var transfer_buf: [min_transfer_buf]u8 = undefined;
|
||||
@@ -278,7 +285,7 @@ test "DohClient satisfies the transport.Client interface" {
|
||||
// Instantiation is the check: the vtable is built from `exchangeFn`, so a
|
||||
// signature drift is a compile error here. The `std.http.Client` above is
|
||||
// never driven, and no exchange runs.
|
||||
const c: transport.Client = doh.client();
|
||||
const c: transport.Leaf = doh.leaf();
|
||||
try testing.expectEqual(@as(*anyopaque, @ptrCast(&doh)), c.ptr);
|
||||
try testing.expectEqual(
|
||||
@as(@TypeOf(c.exchangeFn), DohClient.exchangeFn),
|
||||
@@ -318,34 +325,46 @@ test "contentTypeOk rejects anything else" {
|
||||
try testing.expect(!contentTypeOk("application/dns-message-extra"));
|
||||
}
|
||||
|
||||
test "mapError maps local errors before phase errors" {
|
||||
try testing.expectEqual(error.OutOfMemory, mapError(error.OutOfMemory, .connect));
|
||||
try testing.expectEqual(error.Canceled, mapError(error.Canceled, .receive));
|
||||
try testing.expectEqual(error.Unexpected, mapError(error.Unexpected, .send));
|
||||
test "a local cause stays an error instead of becoming a fault" {
|
||||
try testing.expectError(error.OutOfMemory, fault(error.OutOfMemory, .connect));
|
||||
try testing.expectError(error.Canceled, fault(error.Canceled, .receive));
|
||||
try testing.expectError(error.Unexpected, fault(error.Unexpected, .send));
|
||||
}
|
||||
|
||||
test "mapError maps the collapsed tls errors regardless of phase" {
|
||||
try testing.expectEqual(error.TlsFailed, mapError(error.TlsInitializationFailed, .connect));
|
||||
try testing.expectEqual(error.TlsFailed, mapError(error.TlsInitializationFailed, .receive));
|
||||
try testing.expectEqual(error.TlsFailed, mapError(error.CertificateBundleLoadFailure, .connect));
|
||||
test "kindOf maps the collapsed tls errors regardless of phase" {
|
||||
try testing.expectEqual(error.TlsFailed, kindOf(error.TlsInitializationFailed, .connect));
|
||||
try testing.expectEqual(error.TlsFailed, kindOf(error.TlsInitializationFailed, .receive));
|
||||
try testing.expectEqual(error.TlsFailed, kindOf(error.CertificateBundleLoadFailure, .connect));
|
||||
}
|
||||
|
||||
test "mapError maps every unwrapped record-layer cause to TlsFailed" {
|
||||
test "kindOf maps every unwrapped record-layer cause to TlsFailed" {
|
||||
// The set is the one `Connection.getReadError` can hand back, so the loop
|
||||
// fails the day std adds a member the switch does not name.
|
||||
inline for (@typeInfo(std.crypto.tls.Client.ReadError).error_set.?) |member| {
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.TlsFailed,
|
||||
mapError(@field(std.crypto.tls.Client.ReadError, member.name), .receive),
|
||||
transport.PeerFault.TlsFailed,
|
||||
kindOf(@field(std.crypto.tls.Client.ReadError, member.name), .receive),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "mapError maps remaining errors by phase" {
|
||||
try testing.expectEqual(error.ConnectFailed, mapError(error.ConnectionRefused, .connect));
|
||||
try testing.expectEqual(error.SendFailed, mapError(error.WriteFailed, .send));
|
||||
try testing.expectEqual(error.ReceiveFailed, mapError(error.ReadFailed, .receive));
|
||||
try testing.expectEqual(error.ReceiveFailed, mapError(error.HttpHeadersInvalid, .receive));
|
||||
test "kindOf maps remaining errors by phase" {
|
||||
try testing.expectEqual(error.ConnectFailed, kindOf(error.ConnectionRefused, .connect));
|
||||
try testing.expectEqual(error.SendFailed, kindOf(error.WriteFailed, .send));
|
||||
try testing.expectEqual(error.ReceiveFailed, kindOf(error.ReadFailed, .receive));
|
||||
try testing.expectEqual(error.ReceiveFailed, kindOf(error.HttpHeadersInvalid, .receive));
|
||||
}
|
||||
|
||||
test "a fault carries the classification and the concrete cause" {
|
||||
const failed = try faultOf(error.ConnectionRefused, .connect);
|
||||
try testing.expectEqual(transport.PeerFault.ConnectFailed, failed.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.ConnectionRefused), failed.cause);
|
||||
|
||||
var buf: [64]u8 = undefined;
|
||||
try testing.expectEqualStrings(
|
||||
"ConnectFailed (cause ConnectionRefused)",
|
||||
try std.fmt.bufPrint(&buf, "{f}", .{failed}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Only the fields the unwrap helpers read are set. The rest of a `Connection`
|
||||
@@ -367,6 +386,15 @@ fn stubConnection(
|
||||
return connection;
|
||||
}
|
||||
|
||||
/// The fault half of `fault`, for the unwrap tests. A local cause leaves this
|
||||
/// as an error, which is what those tests assert instead.
|
||||
fn faultOf(err: anyerror, phase: Phase) !transport.Fault {
|
||||
return switch (try fault(err, phase)) {
|
||||
.reply => error.TestExpectedFault,
|
||||
.fault => |f| f,
|
||||
};
|
||||
}
|
||||
|
||||
fn stubRequest(connection: *Connection, body_err: ?std.http.Reader.BodyError) Request {
|
||||
var req: Request = undefined;
|
||||
req.connection = connection;
|
||||
@@ -377,59 +405,46 @@ fn stubRequest(connection: *Connection, body_err: ?std.http.Reader.BodyError) Re
|
||||
test "the send unwrap keeps a cancelled write out of the peer fault group" {
|
||||
var connection = stubConnection(null, error.Canceled);
|
||||
var req = stubRequest(&connection, null);
|
||||
const mapped = mapError(sendCause(&req, error.WriteFailed), .send);
|
||||
try testing.expectEqual(transport.ExchangeError.Canceled, mapped);
|
||||
try testing.expectEqual(transport.Group.cancellation, transport.group(mapped));
|
||||
try testing.expectError(error.Canceled, fault(sendCause(&req, error.WriteFailed), .send));
|
||||
}
|
||||
|
||||
test "the send unwrap keeps a local resource write failure out of the peer fault group" {
|
||||
var connection = stubConnection(null, error.SystemResources);
|
||||
var req = stubRequest(&connection, null);
|
||||
const mapped = mapError(sendCause(&req, error.WriteFailed), .send);
|
||||
try testing.expectEqual(transport.ExchangeError.SystemResources, mapped);
|
||||
try testing.expectEqual(transport.Group.local_resource, transport.group(mapped));
|
||||
try testing.expectError(error.SystemResources, fault(sendCause(&req, error.WriteFailed), .send));
|
||||
}
|
||||
|
||||
test "the send unwrap reports a peer side cause as a send fault" {
|
||||
var connection = stubConnection(null, error.ConnectionResetByPeer);
|
||||
var req = stubRequest(&connection, null);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.SendFailed,
|
||||
mapError(sendCause(&req, error.WriteFailed), .send),
|
||||
);
|
||||
const failed = try faultOf(sendCause(&req, error.WriteFailed), .send);
|
||||
try testing.expectEqual(transport.PeerFault.SendFailed, failed.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.ConnectionResetByPeer), failed.cause);
|
||||
}
|
||||
|
||||
test "the head unwrap keeps a local resource read failure out of the peer fault group" {
|
||||
var connection = stubConnection(error.SystemResources, null);
|
||||
var req = stubRequest(&connection, null);
|
||||
const mapped = mapError(headCause(&req, error.ReadFailed), .receive);
|
||||
try testing.expectEqual(transport.ExchangeError.SystemResources, mapped);
|
||||
try testing.expectEqual(transport.Group.local_resource, transport.group(mapped));
|
||||
try testing.expectError(error.SystemResources, fault(headCause(&req, error.ReadFailed), .receive));
|
||||
|
||||
var canceled = stubConnection(error.Canceled, null);
|
||||
var canceled_req = stubRequest(&canceled, null);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.Canceled,
|
||||
mapError(headCause(&canceled_req, error.ReadFailed), .receive),
|
||||
);
|
||||
try testing.expectError(error.Canceled, fault(headCause(&canceled_req, error.ReadFailed), .receive));
|
||||
}
|
||||
|
||||
test "the head unwrap reports a peer side cause as a receive fault" {
|
||||
var connection = stubConnection(error.ConnectionResetByPeer, null);
|
||||
var req = stubRequest(&connection, null);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.ReceiveFailed,
|
||||
mapError(headCause(&req, error.ReadFailed), .receive),
|
||||
);
|
||||
const failed = try faultOf(headCause(&req, error.ReadFailed), .receive);
|
||||
try testing.expectEqual(transport.PeerFault.ReceiveFailed, failed.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.ConnectionResetByPeer), failed.cause);
|
||||
}
|
||||
|
||||
test "the body unwrap keeps a cancelled read out of the peer fault group" {
|
||||
var connection = stubConnection(error.Canceled, null);
|
||||
var req = stubRequest(&connection, null);
|
||||
const resp: Response = .{ .request = &req, .head = undefined };
|
||||
const mapped = mapError(bodyCause(&resp, error.ReadFailed), .receive);
|
||||
try testing.expectEqual(transport.ExchangeError.Canceled, mapped);
|
||||
try testing.expectEqual(transport.Group.cancellation, transport.group(mapped));
|
||||
try testing.expectError(error.Canceled, fault(bodyCause(&resp, error.ReadFailed), .receive));
|
||||
}
|
||||
|
||||
test "the body unwrap prefers an http framing fault over the connection" {
|
||||
@@ -440,10 +455,9 @@ test "the body unwrap prefers an http framing fault over the connection" {
|
||||
var req = stubRequest(&connection, error.HttpChunkTruncated);
|
||||
const resp: Response = .{ .request = &req, .head = undefined };
|
||||
try testing.expectEqual(error.HttpChunkTruncated, bodyCause(&resp, error.ReadFailed));
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.ReceiveFailed,
|
||||
mapError(bodyCause(&resp, error.ReadFailed), .receive),
|
||||
);
|
||||
const failed = try faultOf(bodyCause(&resp, error.ReadFailed), .receive);
|
||||
try testing.expectEqual(transport.PeerFault.ReceiveFailed, failed.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.HttpChunkTruncated), failed.cause);
|
||||
}
|
||||
|
||||
test "the unwraps report the collapsed error when no cause was stored" {
|
||||
@@ -455,14 +469,13 @@ test "the unwraps report the collapsed error when no cause was stored" {
|
||||
try testing.expectEqual(error.ReadFailed, headCause(&req, error.ReadFailed));
|
||||
try testing.expectEqual(error.ReadFailed, bodyCause(&resp, error.ReadFailed));
|
||||
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.SendFailed,
|
||||
mapError(sendCause(&req, error.WriteFailed), .send),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.ReceiveFailed,
|
||||
mapError(bodyCause(&resp, error.ReadFailed), .receive),
|
||||
);
|
||||
const sent = try faultOf(sendCause(&req, error.WriteFailed), .send);
|
||||
try testing.expectEqual(transport.PeerFault.SendFailed, sent.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.WriteFailed), sent.cause);
|
||||
|
||||
const received = try faultOf(bodyCause(&resp, error.ReadFailed), .receive);
|
||||
try testing.expectEqual(transport.PeerFault.ReceiveFailed, received.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.ReadFailed), received.cause);
|
||||
}
|
||||
|
||||
test "the unwraps pass a non-collapsed error through untouched" {
|
||||
@@ -475,8 +488,7 @@ test "the unwraps pass a non-collapsed error through untouched" {
|
||||
try testing.expectEqual(error.EndOfStream, sendCause(&req, error.EndOfStream));
|
||||
try testing.expectEqual(error.HttpHeadersInvalid, headCause(&req, error.HttpHeadersInvalid));
|
||||
try testing.expectEqual(error.EndOfStream, bodyCause(&resp, error.EndOfStream));
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.ReceiveFailed,
|
||||
mapError(headCause(&req, error.HttpHeadersInvalid), .receive),
|
||||
);
|
||||
const failed = try faultOf(headCause(&req, error.HttpHeadersInvalid), .receive);
|
||||
try testing.expectEqual(transport.PeerFault.ReceiveFailed, failed.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.HttpHeadersInvalid), failed.cause);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user