Repository navigation
Answer one address per route, and name a destination the operator can read - #41
Open
Deep-Sixed wants to merge 3 commits into
Open
Deep-Sixed wants to merge 3 commits into
Deep-Sixed wants to merge 3 commits into
Conversation
`BaseHTTPRequestHandler.parse_request` rewrites a leading `//...` to `/...`
before a handler runs (CPython gh-87389, which stops a client reading a
redirect to `//host/path` as an absolute URI). Inherited, that made
`//api/v1/...` a second spelling of every address this surface answers --
reads and the command addresses alike -- while `tail`, which is written to
keep empty components rather than filter them, never saw the spelling it
refuses, because the rewrite had already happened.
Observed on a running `careersignal serve`: `GET //api/v1/session` answered
200, `///` and `////` with it, and `POST //api/v1/opportunities/{id}/status`
reached the command. The existing alias test sends `/api/v1//...`,
`...//{id}/...` and a trailing slash -- every spelling the rewrite leaves
alone -- so it stayed green throughout.
The target is now read from the raw request line and split on the first `?`.
`urlsplit` is gone from this path with it: its rules are a URI reference's
rather than a request target's, so it reads `//api/v1/session` as the
authority `api`, which is the same alias arriving by a second route. In a
request line that is an ordinary absolute path whose first component is
empty; it is beneath no root and reaches nothing. Origin form is exactly an
absolute path, so `_origin_form` is now a leading `/` -- absolute, authority
and asterisk form all begin with something else and are still refused 400.
`//api/v1/session` is 404 and `POST //api/v1/.../status` is 405, the same
answers the other empty-component spellings already gave. Both new tests
fail against the previous behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ViK1h7GSCZtShZqeknnVJ
The approval packet built its destination row by concatenating `action.provider` and `action.provider_namespace`. Those are what a *recorded* decision bound, and until one exists the API reports both as null -- correctly, because nothing is bound. Concatenated, that published the string "null · null" in the one pane whose stated job is to say what an approval would bind, directly under a note reading "Approving binds this packet to the destination named below" and beside a button reading "Approve for controlled". Observed in a browser against a running serve; an already-decided row rendered "controlled · controlled" beside it. The recorded pair is now read only when there is a decision to read it from. An undecided packet names the launch destination instead -- `decision_target` from `/session`, the same session fact the re-approval control already compares against and the same one the button's label is built from -- so the note above it is true and the two controls agree. Before the session has arrived there is nothing to name and the row says so. Neither value is computed here; both arrive from the server, which is the rule this layer keeps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ViK1h7GSCZtShZqeknnVJ
Brings in #40 (the 0003-era upgrade regression). Its change to test_web_surface.py withholds the body on the one refuse-before-read call site that still sent it; the new leading-empty-component test added here already does the same, for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ViK1h7GSCZtShZqeknnVJ
Deep-Sixed
marked this pull request as ready for review
September 26, 2026 00:07
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.
Two defects found by running
careersignal serveand driving it, not by reading the diff. Both were green under the existing suite.Head:
9f1021cBase:
main @ d0a40f9Two commits and a base merge, five files changed. No behaviour outside the loopback surface changes.
2abf871server.py,test_web_surface.py,docs/web-surface.md79d2864screens.js,test_web_frontend.py9f1021cmainafter #40 landed1. A leading
//was a working alias for every addressBaseHTTPRequestHandler.parse_requestrewrites a leading//…to/…before a handler runs — gh-87389, which stops a client reading a redirect to//host/pathas an absolute URI. This surface inherited it, andtail— written to keep empty components rather than filter them — never saw the spelling it refuses, because the rewrite came first.Observed against a running serve on
a50e83c:Auth,
Host,Originand the token all still applied, so nothing was open — butdocs/web-surface.mdandtail's own docstring both stated the opposite in absolute terms, andtest_the_command_address_is_singular_rather_than_a_family_of_aliasessends/api/v1//…,…//{id}/…,…//statusand a trailing slash: every spelling the rewrite leaves alone. The suite was green while the claim was false.The fix. The target is read from the raw request line and split on the first
?.urlsplitleaves this path with it — its rules are a URI reference's rather than a request target's, so it reads//api/v1/sessionas the authorityapi, which is the same alias arriving by a second route. In a request line that string is an ordinary absolute path whose first component is empty: beneath no root, reaching nothing. Origin form is an absolute path, so_origin_formis now a leading/; absolute, authority and asterisk form all begin with something else and are still400.GET //api/v1/session,///,////No such resourcePOST //api/v1/opportunities/{id}/statusNo command at this addressGET ////is not/GET /api/v1/session,GET /GET http://host/api/v1/sessionGET /api/v1//session,/api/v1/session/Both new tests fail against the previous behaviour — checked by pointing
_targetback atself.path:test_nothing_outside_the_packaged_asset_names_can_be_servedalready parametrised//etc/passwdand expected 404. It passed before only because the rewrite made it/etc/passwd, which is not a packaged asset name; it now passes for the reason it was written for.The new command test withholds its body, for the reason #40's third commit gives: the refusal is settled on the request line, so the bytes would go unread, and closing on unread bytes is a reset rather than a clean shutdown on Windows.
2. The approval packet published
null · nullThe destination row concatenated
action.providerandaction.provider_namespace. Those are what a recorded decision bound, and until one exists the API reports both as null — correctly, because nothing is bound. Concatenated, that put the word "null" twice in the one pane whose stated job is to say what an approval would bind, directly under a note reading "Approving binds this packet to the destination named below" and beside a button reading Approve for controlled. An already-decided row renderedcontrolled · controlledbeside it.The launch always knew the answer:
/sessionreportsdecision_target, the re-approval control already compares against it, and the button's label is built from it.The recorded pair is now read only when there is a decision to read it from; an undecided packet names the launch destination; before the session arrives the row says
—. Neither value is computed in the browser.test_the_packet_names_a_destination_rather_than_publishing_a_missing_onepins the guard and forbids the unguarded concatenation by name, so reintroducing it fails rather than merely reading differently. It fails against the previous line.Verification on
9f1021c1688 tests (1686 on
mainafter #40, → 1688), Ruff check and format clean,verify_tree100 publishable text files / 5 reviewed renders / 1 reviewed screenshot,verify_secretsPASS,verify_wheelPASS.Both fixes were also re-checked at the surface on a fresh database: the address table above was driven over a raw socket, and the packet was read in headless Chromium —
destinationreadscontrolled · controlledundecided and after approving, the page contains no renderednull, and there were no console or page errors.Not changed here:
docs/verification.mdstill records 1517 tests in its Tests row. That drift predates this branch and is not what either fix is about.🤖 Generated with Claude Code
https://claude.ai/code/session_012ViK1h7GSCZtShZqeknnVJ