Errors and diagnostics
Error responses
Proxy-generated errors carry an RFC 9209
Proxy-Status header naming the proxy (ProxyName, default forward-proxy)
and the error type, without internal details. ProxyName is also the Via
pseudonym, so it must be an HTTP token starting with a letter (no : or /):
HTTP/1.1 403 Forbidden
Proxy-Status: egress; error=destination_ip_prohibited
Content-Length: 0
Connection: close
| Status | error= |
Cause |
|---|---|---|
| 400 | http_request_error |
Malformed request or target, absolute-form https://, userinfo in target |
| 403 | http_request_denied |
Port, host rule or IForwardProxyDestinationFilter denied |
| 421 | http_request_denied |
Intercepted request's Host does not match the CONNECT target |
| 403 | destination_ip_prohibited |
Target IP or every resolved address is blocked |
| 502 | http_request_denied |
Forwarding loop: the request's Via already has an entry received by ProxyName (details="forwarding loop detected") |
| 502 | dns_error |
Name did not resolve |
| 502 | connection_refused |
Destination refused the connection |
| 502 | destination_ip_unroutable, destination_unavailable |
Network unreachable or other connect failure |
| 502 | http_response_incomplete |
Destination response was invalid (http:// and intercepted requests) |
| 502 | tls_certificate_error |
Destination's certificate failed validation (intercepted requests) |
| 502 | tls_protocol_error |
TLS with the destination failed (intercepted requests) |
| 500 | proxy_internal_error |
Unexpected proxy failure (logged) |
| 503 | connection_limit_reached |
MaxConcurrentTunnels reached (CONNECT or WebSocket upgrade) |
| 504 | connection_timeout |
ConnectTimeout elapsed (including DNS), or HttpActivityTimeout for http:// and intercepted requests |
407 and authorization 403 responses come from the authentication handler
and do not carry Proxy-Status. Set AddProxyStatusHeader = false to omit the
header. Malformed HTTP/1.1 CONNECT heads get 400 (or 431 when too large)
and the connection is closed.
Forwarding loops
Forwarded http:// and intercepted requests get a Via entry with
ProxyName as the received-by pseudonym (set AddViaHeader = false to omit
it). When a request arrives whose Via already contains an entry received by
ProxyName (compared case-insensitively), it has looped back through this
proxy: it gets 502 without contacting the destination:
HTTP/1.1 502 Bad Gateway
Proxy-Status: egress; error=http_request_denied; details="forwarding loop detected"
Give each proxy in a chain a distinct ProxyName. Set
RejectForwardingLoops = false to forward such requests anyway. CONNECT
requests carry no Via and are not checked.
Logging
Logs use the categories DamianH.Http.ForwardProxy.Internal.*. Rejections
(Information) include the request kind, host, port, status and reason.
Tunnel closure (Information) includes duration and bytes in each direction.
Upgraded WebSocket connections are tunnels too, in logs, metrics and traces.
Intercepted tunnels log at Information when they open, and a missing or
failing interception certificate logs a Warning.
An open proxy logs a Warning at startup.
Metrics
Meter DamianH.Http.ForwardProxy. An intercepted tunnel counts as
connect/intercepted, and each request inside it as intercepted:
| Instrument | Type | Tags |
|---|---|---|
forward_proxy.requests |
Counter | forward_proxy.kind (connect, http, intercepted), forward_proxy.outcome (allowed, intercepted, unauthorized, denied, error) |
forward_proxy.tunnels.active |
UpDownCounter | |
forward_proxy.tunnel.bytes |
Counter (bytes) | forward_proxy.direction (upstream, downstream) |
forward_proxy.tunnel.duration |
Histogram (s) |
builder.Services.AddOpenTelemetry().WithMetrics(m => m.AddMeter("DamianH.Http.ForwardProxy"));Tracing
Each tunnel creates a ForwardProxy.Tunnel activity from the
DamianH.Http.ForwardProxy activity source, tagged with server.address and
server.port. http:// forwarding and intercepted requests are traced by
HttpClient.