A malformed status crashed the lookup, and eight other quiet failures - #4
Merged
Conversation
Three of these are in `src/server/lookup.ts`, and all three arrived with the same
change: the pinned `node:https` transport replaced `fetch`, and undici had been
doing more than it looked.
`new Response(body, { status })` takes a status of 200–599 and refuses a body on
the four statuses defined not to carry one. A server is bound by neither. A 205
with an HTML body, or a 600, makes the constructor throw — and it throws inside
the `https.request` response callback, where there is no promise for it to land
in. That is an uncaught exception, which takes the dev server, or the serverless
invocation, down with it. Construction moved into `toResponse`: an out-of-range
status becomes 502, a bodyless one has its stream torn down, and the callback
wraps the call so a throw destroys the socket and rejects instead.
A response dropped without being read — a redirect hop, an error page, a PDF —
used to be collected along with its socket, because `fetch` has a GC backstop and
a body timeout. The raw transport has neither, and the abort timer is cleared in
`finally`, so a server that writes a 302 body and never ends it holds a socket
and an `IncomingMessage` open after `lookupProduct` has returned. In dev that is
one socket per request; in a function it is the invocation held open until the
platform's hard timeout, which is the exact thing the size cap and the timer
exist to prevent. Every early exit cancels the body first now.
And a timeout that lands mid-body no longer looks like one. `fetch` rejects with
an `AbortError`; `node:https` destroys the `IncomingMessage`, so a body still
being read fails with an `aborted` ECONNRESET, and the user was told "could not
reach that page" about a page that had answered and then stalled. The catch asks
`controller.signal.aborted` rather than the error's name. Nothing caught any of
this because the 504 test stubs the transport with a rejecting `AbortError` and
`pinnedFetch` is never exercised by a test at all — there are now four over
`toResponse`, one driving the mid-body timeout on fake timers, and one asserting
the body is cancelled on all three drop paths.
In `walk.ts`, the `inside` branch of `contactNormal` summed a unit vector toward
every edge, and for a rectangle — every wall segment, most of the furniture — the
four cancel to exactly zero. The branch always returned null while its comment
said it pointed at open air. It takes the nearest edge alone now, which is what
the comment always claimed. It was also re-running the broad phase `isClear` had
just run and projecting every edge twice; `slide` collects the touching blockers
once and hands them over.
A move squarely into a surface loses its whole length to the normal and leaves a
1e-13mm residue, which is a new position every frame for a walker standing still.
`slide` returns `from` itself. The test that claimed to cover this passed for a
different reason — its blocked point landed exactly on the hypotenuse, where no
edge has a distance and no normal exists, so the axis fallback answered — and it
now starts from a point where the projection is what runs. A second test covers a
running step long enough to land its centre inside the polygon, which is where
the nearest-edge branch earns its place.
`scene.bench.ts` never measured a collision. At the sparse pitch the walker
started inside a placement, so `slide` took the free-move escape; at the other
two the grid fills a corner of a 40×32m hall and the walker stood in open floor.
The 0.011–0.028ms §10.4 quoted as the per-frame cost was the cost of a step that
touches nothing, and the contact-normal pass it was quoted about was never in it.
Two rows now, clear and blocked, seeded against the first row of the grid, with
`assertStride` failing the bench if either walker is standing inside something or
stepping where its label does not say. 0.029–0.032ms clear, 0.045–0.049ms
blocked; §10.4 carries both and says why the old figure was wrong.
`playwright.media.config.ts` had `reuseExistingServer: true`, so a preview left
running from an earlier build would be photographed and committed to `docs/media`
as the current application — and every assert in that run checks state, not
pixels, so all of them would pass. `playwright.config.ts` already sets it false,
with a comment about the time that happened. And `capture.spec.ts` said the
walker seeds facing east, which is why the clip has to look 90° first; it seeds
facing north.
779 unit tests, 124 e2e, `tsc` and `eslint` clean.
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.
Three of these are in
src/server/lookup.ts, and all three arrived with the same change: the pinnednode:httpstransport replacedfetch, and undici had been doing more than it looked.new Response(body, { status })takes a status of 200–599 and refuses a body on the four statuses defined not to carry one. A server is bound by neither. A 205 with an HTML body, or a 600, makes the constructor throw — and it throws inside thehttps.requestresponse callback, where there is no promise for it to land in. That is an uncaught exception, which takes the dev server, or the serverless invocation, down with it. Construction moved intotoResponse: an out-of-range status becomes 502, a bodyless one has its stream torn down, and the callback wraps the call so a throw destroys the socket and rejects instead.A response dropped without being read — a redirect hop, an error page, a PDF — used to be collected along with its socket, because
fetchhas a GC backstop and a body timeout. The raw transport has neither, and the abort timer is cleared infinally, so a server that writes a 302 body and never ends it holds a socket and anIncomingMessageopen afterlookupProducthas returned. In dev that is one socket per request; in a function it is the invocation held open until the platform's hard timeout, which is the exact thing the size cap and the timer exist to prevent. Every early exit cancels the body first now.And a timeout that lands mid-body no longer looks like one.
fetchrejects with anAbortError;node:httpsdestroys theIncomingMessage, so a body still being read fails with anabortedECONNRESET, and the user was told "could not reach that page" about a page that had answered and then stalled. The catch askscontroller.signal.abortedrather than the error's name. Nothing caught any of this because the 504 test stubs the transport with a rejectingAbortErrorandpinnedFetchis never exercised by a test at all — there are now four overtoResponse, one driving the mid-body timeout on fake timers, and one asserting the body is cancelled on all three drop paths.In
walk.ts, theinsidebranch ofcontactNormalsummed a unit vector toward every edge, and for a rectangle — every wall segment, most of the furniture — the four cancel to exactly zero. The branch always returned null while its comment said it pointed at open air. It takes the nearest edge alone now, which is what the comment always claimed. It was also re-running the broad phaseisClearhad just run and projecting every edge twice;slidecollects the touching blockers once and hands them over.A move squarely into a surface loses its whole length to the normal and leaves a 1e-13mm residue, which is a new position every frame for a walker standing still.
slidereturnsfromitself. The test that claimed to cover this passed for a different reason — its blocked point landed exactly on the hypotenuse, where no edge has a distance and no normal exists, so the axis fallback answered — and it now starts from a point where the projection is what runs. A second test covers a running step long enough to land its centre inside the polygon, which is where the nearest-edge branch earns its place.scene.bench.tsnever measured a collision. At the sparse pitch the walker started inside a placement, soslidetook the free-move escape; at the other two the grid fills a corner of a 40×32m hall and the walker stood in open floor. The 0.011–0.028ms §10.4 quoted as the per-frame cost was the cost of a step that touches nothing, and the contact-normal pass it was quoted about was never in it. Two rows now, clear and blocked, seeded against the first row of the grid, withassertStridefailing the bench if either walker is standing inside something or stepping where its label does not say. 0.029–0.032ms clear, 0.045–0.049ms blocked; §10.4 carries both and says why the old figure was wrong.playwright.media.config.tshadreuseExistingServer: true, so a preview left running from an earlier build would be photographed and committed todocs/mediaas the current application — and every assert in that run checks state, not pixels, so all of them would pass.playwright.config.tsalready sets it false, with a comment about the time that happened. Andcapture.spec.tssaid the walker seeds facing east, which is why the clip has to look 90° first; it seeds facing north.779 unit tests, 124 e2e,
tscandeslintclean.