v0.33.0 — standardized 401 responses
Standardized 401 responses
Before this release a 401 was whatever Falcon produced: a styled HTML page served to every caller, including the Arrow client, which then pasted the entire page into RpcError.error_message. The only machine-readable signal was the status code, so a client wanting to refresh a token and retry had to substring-match an English sentence.
Every 401 now carries a reason code from a closed set — missing_credential, invalid_credential, expired_credential, insufficient_scope, proxy_required, unauthorized — on a VGI-Auth-Reason header and in a JSON envelope. Accept: text/html still gets the styled page; */*, which is what every RPC client sends, gets JSON.
HTTP/1.1 401 Unauthorized
VGI-Auth-Reason: proxy_required
VGI-Auth-Proxy-Required: true
Cache-Control: no-store
{"error":"unauthorized","reason":"proxy_required",
"detail":"Missing x-forwarded-client-cert header",
"proxy_hint":"This service only accepts requests that arrive through its
configured reverse proxy, which must set the x-forwarded-client-cert
header. …"}
The proxy note
Authentication schemes that read a proxy-injected header — mTLS as x-forwarded-client-cert, a proxy proof — fail identically whether the caller sent a bad certificate or the proxy was never configured to forward one. The second is far more common during a deployment, and the response used to say nothing about it.
The note is derived from server configuration, not from what failed on a given request, so it is identical on every 401 a proxy-dependent service emits and discloses nothing a caller could not already read off the capability headers. That is also what makes it correct in the case it exists for: when the proxy is not forwarding the header, everything 401s.
Header dependencies are discovered automatically — mtls_authenticate* and the require-mode proxy_proof_gate declare their own, and chain_authenticate / require_all propagate them. make_wsgi_app(proxy_auth_headers=[...]) covers a custom callback the framework cannot introspect.
Client side
401s now raise AuthenticationError, a subclass of RpcError with error_type still "AuthenticationError", so existing except RpcError call sites are unaffected. It exposes reason, detail and proxy_hint. A body that is not the envelope degrades instead of being dumped whole — a 401 can come from a gateway, WAF or SSO portal the service never sees.
Compatibility
- The 401 body changes shape for non-browser clients (HTML → JSON). Anything parsing that body needs to read the envelope, or the
VGI-Auth-Reasonheader. bearer_authenticatepreviously raised one message for both an absentAuthorizationheader and a non-Bearer scheme; those are nowmissing_credentialandinvalid_credentialwith distinct messages.- The reason code names the stage that refused, never a verifier's diagnosis — every proxy-proof outcome collapses onto
proxy_required, preserving the uniform-rejection rule of the proxy-proof spec.
Contract: docs/unauthorized-spec.md. Cross-language conformance group TestUnauthorized, capability-gated on VGI-Auth-Reason so unported languages skip cleanly.
Brand
The circular VGI seal is replaced by the shield mark across every asset, now on a transparent background and cut from a single committed master by scripts/regenerate_logo_assets.py.
Full changelog: v0.32.0...v0.33.0