upstream: an unnamed connect errno fails over, local-resource exchanges are counted, doh faults carry the http status

This commit is contained in:
2026-10-20 10:11:34 +02:00
parent 2697adfa27
commit 8efe504c51
8 changed files with 191 additions and 15 deletions
+24 -2
View File
@@ -123,7 +123,11 @@ pub const DohClient = struct {
// is an error before the location is ever read.
var resp = req.receiveHead(&.{}) catch |err| return fault(headCause(&req, err), .receive);
if (resp.head.status != .ok) return peerFault(error.HttpStatus, error.HttpStatus);
if (resp.head.status != .ok) return .{ .fault = .{
.kind = error.HttpStatus,
.cause = error.HttpStatus,
.http_status = resp.head.status,
} };
// `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 peerFault(error.HttpContentType, error.HttpContentType);
@@ -175,7 +179,19 @@ const Phase = enum { connect, send, receive };
/// arrive here as themselves. Without them a decode error or a bad record MAC
/// would be reported as a plain receive failure.
fn fault(err: anyerror, phase: Phase) transport.LeafError!transport.Outcome {
if (transport.mapLocal(err)) |local| return local;
// `.connect` is the whole of `http.request`: the name lookup, the TCP
// connect and the handshake. The handshake's only `error.Unexpected` is the
// errno the socket stream stored (std/http/Client.zig:1466; the TLS
// client's `InitError` has none), and a CA bundle scan fails as
// `CertificateBundleLoadFailure`. The lookup's resolver socket can fail
// locally, but that still leaves this upstream unreachable while a DoT
// upstream, always an IP literal, needs no lookup, so the fault belongs to
// this endpoint and the pool is right to move on.
const local = switch (phase) {
.connect => transport.mapLocalConnect(err),
.send, .receive => transport.mapLocal(err),
};
if (local) |claimed| return claimed;
return peerFault(kindOf(err, phase), err);
}
@@ -331,6 +347,12 @@ test "a local cause stays an error instead of becoming a fault" {
try testing.expectError(error.Unexpected, fault(error.Unexpected, .send));
}
test "an unnamed connect errno is a connect fault, so the pool fails over" {
const outcome = try fault(error.Unexpected, .connect);
try testing.expectEqual(transport.PeerFault.ConnectFailed, outcome.fault.kind);
try testing.expectEqual(error.Unexpected, outcome.fault.cause);
}
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));