Skip to content

test(web): pin the HTTP invariants nothing was watching - #3132

Merged
ryansolid merged 2 commits into
solidjs:nextfrom
frenzzy:test/http-invariants
Aug 31, 2026
Merged

test(web): pin the HTTP invariants nothing was watching#3132
ryansolid merged 2 commits into
solidjs:nextfrom
frenzzy:test/http-invariants

Conversation

@frenzzy

@frenzzy frenzzy commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

An audit of the server-function runtime against the HTTP surface — methods, addressing, headers, bodies, caching, CORS, cookies — turned up four defects (#3128, #3129, #3130, #3131) and something larger: a great deal of behaviour that is deliberate, correct, and guarded by nothing at all.

This PR pins that behaviour. Every test here passes on next today; none of it would have been noticed breaking. Tests only, no runtime change, no changeset.

What is now pinned

The origin gate's reading of the headersOrigin: null (sandboxed iframe, data:), an explicit :443, byte-for-byte comparison rather than normalised, punycode, a Referer that parses but names no origin, an unrecognised Sec-Fetch-Site value falling through to the Origin check, and duplicated Sec-Fetch-Site/Origin — comma-joined in transit — still refusing. #3111 pinned the gate's POST branches; these are the header-reading cases it did not reach.

Methods — exact matching (post, Post, POST are not POST), Allow correct on OPTIONS and on an unknown verb, and that no method-override header or ?_method= is honoured, so a front proxy that translates one cannot smuggle a mutation past the gate.

Addressing — an encoded path segment or control character does not fold onto the plain id, the data segment and the mount are read literally, and an oversized id is refused before the body is read.

CORS — nothing is emitted on any path and a browser-shaped preflight fails closed. That is the entire cross-origin story of this runtime, and it rested on no test.

Header hygiene — CR/LF stripped from the error label so it cannot become a header of its own; no-store on the refusals built before dispatch; and the Vary merge preserving an author's value, deduping case-insensitively, and short-circuiting on Vary: *.

HEAD over a streamed result — the GET's headers, no body, and the source cancelled rather than pumped to completion.

Conditional requests and Range on a declared read — answered in full, never a bogus 304 or 206. Spec-legal, deliberate, and the kind of thing a well-meaning change would "fix".

How each test earned its place

By mutating the built runtime and confirming the test fails — 24 mutations, every new test killed by at least one. A sample:

mutation turns red
matchesOrigin compares both sides lowercased compares the Origin byte for byte
read only the first comma-separated Sec-Fetch-Site refuses a duplicated Sec-Fetch-Site or Origin
honour X-HTTP-Method-Override honours no method-override header or query parameter
emit Access-Control-Allow-Origin: * emits no Access-Control-* on any path; preflight fails closed
match the data/ segment case-insensitively reads the data segment and the mount literally
default no-store only on 2xx keeps no-store on the refusals built before dispatch
drop the body without cancelling the source HEAD answers the GET's headers with no body, and cancels
stop stripping CR/LF from the error label strips CR/LF so the label cannot become a header

Two cases that resisted, stated rather than hidden

An explicit :443 in the request url, and the matching half of the punycode pair, are both normalised by the URL parser before the runtime ever sees them — so no mutation of the gate can turn either red. The first was dropped and refolded into the Origin-header direction, where it is a real decision (Origin: https://app.example:443 → 403). The second is kept as half of a pair whose other half does discriminate, because the pair is the statement; trim it if you would rather have only killable assertions.

Notes

No new files — every invariant had a home in an existing spec. Suite: 45 files, 474 passed, 1 expected fail, 2 skipped (up from 452 passed). tsc gains one error, bodySizeLimit missing from HandleServerFunctionOptions in the generated types/ — the same stale-types problem that already produces five identical errors in server-functions-request-bounds.spec.tsx on next; the option is real in the source. I followed the existing convention rather than working around it.

Follow-up from review

A later pass found a third assertion that could not fail — refuses Origin: null passed on a gate that had stopped reading Origin entirely, since the no-proof branch refuses by default. It now runs with the no-proof escape hatch open, so only the literal value can produce the refusal, and a mutation treating Origin: "null" as absent fails it. A redundant Referer case was dropped (the pre-existing fallback test already carries a path), and the oversized-id test was renamed to what it pins: the runtime has no id size check, it answers an unknown id before buffering the body.

These all pass today. That is the point: an audit of the runtime against
the HTTP surface turned up a lot of behaviour that is deliberate, correct
and completely unguarded, so the next change to any of it would be
silent.

- The origin gate's reading of the headers: `Origin: null`, an explicit
  `:443`, byte-for-byte comparison, punycode, a Referer that parses but
  names no origin, an unrecognised `Sec-Fetch-Site` falling through to
  Origin, and duplicated `Sec-Fetch-Site`/`Origin` (comma-joined in
  transit) still refusing.
- Methods: exact matching, `Allow` on OPTIONS and an unknown verb, and
  that no method-override header or `?_method=` is honoured.
- Addressing: an encoded path or control character does not fold onto the
  plain id, the `data` segment and the mount are read literally, and an
  oversized id is refused before the body is read.
- CORS: nothing is emitted on any path and a browser preflight fails
  closed — which is the whole cross-origin story, and was untested.
- Header hygiene: CR/LF stripped from the error label, `no-store` on the
  refusals built before dispatch, and the `Vary` merge preserving an
  author's value, deduping case-insensitively and short-circuiting on `*`.
- HEAD over a streamed result: the GET's headers, no body, and the source
  cancelled rather than pumped.
- Conditional requests and Range on a declared read: answered in full,
  never a bogus 304 or 206.

Each test was checked by mutating the built runtime — 24 mutations, every
new test killed by at least one. Two cases resisted: an explicit `:443`
in the request url and the matching half of the punycode pair are both
normalized by the URL parser before the runtime sees them, so no mutation
of the gate can turn them red. The first was dropped and refolded into
the Origin-header direction, where it is a real decision; the second is
kept as half of a pair whose other half does discriminate.
@changeset-bot

changeset-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: bd51e46

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codspeed-hq

codspeed-hq Bot commented Aug 30, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 28.51%

❌ 3 regressed benchmarks
✅ 133 untouched benchmarks
⏩ 132 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
merge 225.1 µs 326.5 µs -31.05%
merge 264.7 µs 364.2 µs -27.31%
merge 265.1 µs 363.7 µs -27.1%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing frenzzy:test/http-invariants (bd51e46) with next (d6a4a52)

Open in CodSpeed

Footnotes

  1. 132 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

…ant one

Review found a second assertion that could not fail. `refuses Origin:
null` passed on a gate that had stopped reading Origin at all, because
the no-proof branch refuses by default — so the test never depended on
the literal value it names. It now runs with
`allowRequestsWithoutOriginCheck: true`, where a request carrying nothing
is accepted, so only the `null` itself can produce the refusal. Verified:
mutating the gate to treat `Origin: "null"` as absent now fails it.

Also dropped `reads only the origin out of a Referer, path and all` — the
pre-existing `falls back to Referer when Origin is absent` already uses a
path-carrying Referer, and no mutation kills the new one alone. And
renamed the oversized-id test to what it actually pins: there is no id
size check in the runtime, only an unknown id answered before the body is
buffered.
@ryansolid
ryansolid merged commit 40af4d6 into solidjs:next Aug 31, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants