b1f7116231
Three small defects found underneath error clusters that are themselves benign. bounceToAuth returned a bare NextResponse.redirect and never copied the cookies off supabaseResponse, so the Set-Cookie headers that clear a dead session were thrown away on the first bounce and the browser replayed the dead refresh token once more on /login. That doubled both the GoTrue 400s and the log volume. The AuthApiError itself is left alone: it is correct session-expiry handling that auth-js logs from inside node_modules, and getUser() returns it as a value. "The destination stream closed early." is a client disconnecting mid-stream, produced inside React's Flight server. Next's own isAbortError filter does not recognise React's cancel error, so instrumentation.ts reported it to PostHog Error Tracking as a real exception against real users' session replays and paid an awaited flush on an otherwise-healthy request. A narrow predicate now early-returns before PostHog is touched. This cannot remove the line from Vercel's runtime-error table, which is fed by Next's stderr. other-account-hint.ts issued an unfiltered journal_entries probe on the blocking Hem render path, inside a render Promise.all: roughly 1 in 20 Hem loads waited an extra 2.5 s for an advisory nudge. An unfiltered probe on a multi-tenant table is also a correctness smell. It is now company-scoped and off the blocking path. Claude-Session: https://claude.ai/code/session_016ifKg6Ec67A39oxfGPU1yc Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
53 lines
2.6 KiB
TypeScript
53 lines
2.6 KiB
TypeScript
/**
|
|
* Is this error a client hanging up mid-response, rather than a failure?
|
|
*
|
|
* When a browser navigates away while a page or RSC payload is still
|
|
* streaming, the HTTP response closes, Next destroys the stream it handed
|
|
* React (`node_modules/next/dist/server/pipe-readable.js`), and React's
|
|
* `createCancelHandler` aborts the in-flight render with a bare
|
|
* `Error('The destination stream closed early.')`. Both renderers do it: the
|
|
* Flight server (react-server-dom-webpack-server.node.production.js:3922) and
|
|
* react-dom's SSR streamer (react-dom-server.node.production.js:7822, :8099).
|
|
* The response itself still completes: every occurrence we sampled in
|
|
* production rode on a 200.
|
|
*
|
|
* Next means to swallow these. `create-error-handler.js` early-returns on
|
|
* `isAbortError`, but that predicate knows only `name === 'AbortError'` and
|
|
* `name === 'ResponseAborted'` (pipe-readable.js:32), and React's cancel error
|
|
* is a plain `Error`, so it falls through to `onRequestError` and is reported
|
|
* as an exception that never happened. This predicate closes that gap on our
|
|
* side. It is a Next filtering gap, not a version bug: 16.3.1 and 16.3.4 are
|
|
* byte-identical here, so upgrading is not the fix.
|
|
*
|
|
* Matching is on the EXACT message. A React rewording brings the noise back,
|
|
* which is the safe direction; a substring match would let a real error hide
|
|
* behind the phrase.
|
|
*
|
|
* Deliberately NOT matched: a bare `AbortError`, `ECONNRESET`,
|
|
* `ERR_STREAM_PREMATURE_CLOSE`. Those are how this codebase's outbound calls
|
|
* report genuine failures (`AbortSignal.timeout` in `lib/http/fetch-with-timeout`
|
|
* and every provider client; `lib/providers/with-provider-call` treats them as
|
|
* retryable provider faults), and swallowing them would hide real integration
|
|
* breakage. They also buy nothing here: Next already filters `AbortError`
|
|
* before instrumentation ever sees it.
|
|
*/
|
|
|
|
const CLIENT_DISCONNECT_MESSAGES = new Set([
|
|
'The destination stream closed early.',
|
|
'The destination stream errored while writing data.',
|
|
])
|
|
|
|
/**
|
|
* Next's own error for a response the client aborted
|
|
* (`server/web/spec-extension/adapters/next-request.js:39`). Unlike a plain
|
|
* `AbortError`, this name is only ever produced by that one situation.
|
|
*/
|
|
const RESPONSE_ABORTED_NAME = 'ResponseAborted'
|
|
|
|
export function isClientDisconnectError(error: unknown): boolean {
|
|
if (typeof error !== 'object' || error === null) return false
|
|
const { name, message } = error as { name?: unknown; message?: unknown }
|
|
if (name === RESPONSE_ABORTED_NAME) return true
|
|
return typeof message === 'string' && CLIENT_DISCONNECT_MESSAGES.has(message)
|
|
}
|