fix: probe Bearer too, so a half-stripped host stops reading healthy (#121) - #123
Merged
Conversation
…ing healthy self_check() could report auth_header: ok on a site where every OAuth client was being refused, because probe_headers() sent exactly one loopback and it carried Basic. Basic is the scheme that survives where others do not. Apache and LiteSpeed consume an RFC 7617 header natively into PHP_AUTH_USER, so it reaches PHP even on setups that never forward the raw Authorization header, and a firewall rule can be written against Bearer alone. On such a host every pasted-key client works and every app that signs in through Saddle gets 401 — which reads from outside as "the AI app is broken" rather than "my server drops a header", and which Saddle's own health check was confirming. That matters because the two paths have disjoint client populations. Application Passwords carry Basic: Claude Code, Claude Desktop, Cursor, VS Code. OAuth carries Bearer, and ChatGPT has no field for a custom header, so Bearer is the only way it can ever connect. "Claude works, so the header is fine" is the wrong inference and this is the code that was licensing it. So: a second loopback carrying Bearer, judged on the scheme that arrived rather than on arrival alone (a host that rewrites an unrecognised Authorization header would otherwise pass a test it should fail), a bearer_header field, a bearer_header_stripped status ranked below the both-stripped case, and htaccess_fixable extended — the rule Saddle already generates forwards the header whatever scheme it carries, so it was always the fix for both. The public probe route gains `scheme`, its first non-boolean. The contract test pinning its key set caught that, as designed; it is updated deliberately, and the value is a closed enum naming the shape of what the caller itself just sent, never any part of it. Closes #121
This was referenced Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #121
What
Saddle_Connection::self_check()could reportauth_header: okon a site where every OAuth client was being refused, becauseprobe_headers()sent exactly one loopback request and it carriedBasic.Now it sends two — Basic and Bearer — and reports each.
Why this is the one that mattered
Basic is the scheme that survives where others do not. Apache and LiteSpeed consume an RFC 7617 header natively into
PHP_AUTH_USER, so it reaches PHP even on CGI/FastCGI setups that never forward the rawAuthorizationheader, and a firewall rule can be written againstBeareralone.On such a host:
From outside that reads as "the AI app is broken", not "my server drops a header". "Claude works against the same endpoint, so the header must be fine" is the wrong inference, and this function was the thing licensing it.
How
Authorization: Bearer saddle-probe. Neither probe value is a credential; nothing reads them.bearer_header: ok|stripped|unknown, and abearer_header_strippedstatus ranked belowauth_header_stripped(both-stripped is the more severe report) and above the nonce.htaccess_fixableextended to the bearer case. The rule Saddle already generates forwards the wholeAuthorizationheader, so it was always the fix for both — it simply was never offered for this one.ConnectionHealth.jsxgets its own copy for the partial case, because the generic warning would be wrong: pasted-key apps are fine, and saying otherwise sends people hunting the wrong thing.The public probe route gains a field
rest_auth_probe()now returnsscheme— its first non-boolean.test_auth_probe_returns_only_the_documented_booleanscaught that immediately, which is exactly what it is for; it is updated deliberately rather than loosened. The value is a closed enum (''/basic/bearer) naming the shape of the credential the caller itself just sent, never any part of its value, and there is a test asserting a token cannot appear in the response.Testing
composer test— 603 tests (was 599), 2017 assertions, 1 pre-existing skip, greenUndefined array key "bearer_header"/"scheme")test_self_check_catches_a_bearer_stripped_while_basic_survives— assertsauth_header: okandbearer_header: strippedandstatus: bearer_header_strippedandhtaccess_fixable: true, from a loopback stub that answers differently per scheme. That combination is the customer's exact site.ok) and the forward-compat guard (a probe response with noschemekey readsunknown, neverstripped— guessing would put a scary, wrong warning on a healthy site).composer lint— 0 errors (pre-existing warnings only);npm run lint:jscleannpm run build;languages/saddle.potregenerated