What happens
When a client closes the connection while a request is still running, Start prints the abort reason as an unhandled error on the server, with status 500:
Error: aborted
at abortIncoming (node:_http_server:921:17)
... 2 lines matching cause stack trace ...
at TCP.<anonymous> (node:net:362:12) {
cause: Error: aborted
at abortIncoming (node:_http_server:921:17)
at socketOnClose (node:_http_server:914:3)
at Socket.emit (node:events:526:24)
at TCP.<anonymous> (node:net:362:12) {
code: 'ECONNRESET'
},
status: 500,
statusText: undefined,
headers: undefined,
data: undefined,
body: undefined,
unhandled: true
}
Nobody receives the 500, because the client has gone. In production, every user who navigates away while a loader or server function is waiting prints one of these. It cannot be caught from application code. A request middleware that catches the abort and returns its own Response still gets the line, and so does a custom src/server.ts that wraps handler.fetch in a try/catch, because the error never leaves handler.fetch. Start returns a 500 Response and logs it on the way.
Why
- Once
request.signal aborts, executeMiddleware throws signal.reason whatever the middleware returned: createStartHandler.ts L413, L436, L475.
requestHandler passes the rejected promise to h3's toResponse with no config: request-response.ts L141.
- h3's
prepareResponse treats anything that is not an HTTPError as unhandled, logs it with console.error unless config.silent is set, and answers 500.
The reason is whatever the server runtime aborts the signal with. Under srvx it is Node's socket error (Error: aborted, ECONNRESET). An AbortError would be logged the same way, since it is an Error and not an HTTPError.
This is close to #8155, which fixed the SSR render stream surfacing the same abort. That path is fixed in our versions. This one is the middleware runner's throw, which reaches h3 whatever the render does.
Steps
- A route whose loader calls a server function that waits for a few seconds, for example an upstream call with a long deadline.
vite build, then start the built server.
curl -m 0.2 http://localhost:3000/<that route>, so the client leaves while the loader waits.
- The server prints the error above. In our app the request middleware's own log line records the request as a 499 with
aborted: true just before it, so the app did see the abort.
Expected
A request the client aborted ends quietly. Two possible shapes: requestHandler answers a bare response without calling h3 when request.signal.aborted and the rejection is request.signal.reason, or it passes h3 an onError that does the same.
Versions
@tanstack/react-start 1.168.56 (1.168.58 is the same: it depends on the same server core)
@tanstack/start-server-core 1.169.37 (latest). main at 3cdd1af04b has the same code.
h3-v2 (h3@2.0.1-rc.20), bundled by Start
nitro 3.0.260903-beta via nitro/vite, srvx 1.0.3
- Node 24.21.0, macOS
What happens
When a client closes the connection while a request is still running, Start prints the abort reason as an unhandled error on the server, with status 500:
Nobody receives the 500, because the client has gone. In production, every user who navigates away while a loader or server function is waiting prints one of these. It cannot be caught from application code. A request middleware that catches the abort and returns its own
Responsestill gets the line, and so does a customsrc/server.tsthat wrapshandler.fetchin a try/catch, because the error never leaveshandler.fetch. Start returns a 500Responseand logs it on the way.Why
request.signalaborts,executeMiddlewarethrowssignal.reasonwhatever the middleware returned:createStartHandler.tsL413, L436, L475.requestHandlerpasses the rejected promise to h3'stoResponsewith no config:request-response.tsL141.prepareResponsetreats anything that is not anHTTPErroras unhandled, logs it withconsole.errorunlessconfig.silentis set, and answers 500.The reason is whatever the server runtime aborts the signal with. Under srvx it is Node's socket error (
Error: aborted,ECONNRESET). AnAbortErrorwould be logged the same way, since it is anErrorand not anHTTPError.This is close to #8155, which fixed the SSR render stream surfacing the same abort. That path is fixed in our versions. This one is the middleware runner's throw, which reaches h3 whatever the render does.
Steps
vite build, then start the built server.curl -m 0.2 http://localhost:3000/<that route>, so the client leaves while the loader waits.aborted: truejust before it, so the app did see the abort.Expected
A request the client aborted ends quietly. Two possible shapes:
requestHandleranswers a bare response without calling h3 whenrequest.signal.abortedand the rejection isrequest.signal.reason, or it passes h3 anonErrorthat does the same.Versions
@tanstack/react-start1.168.56 (1.168.58 is the same: it depends on the same server core)@tanstack/start-server-core1.169.37 (latest).mainat3cdd1af04bhas the same code.h3-v2(h3@2.0.1-rc.20), bundled by Startnitro3.0.260903-beta vianitro/vite,srvx1.0.3