Repository navigation
Server Fn Signal and API route (request.signal) does not actually fire 'abort' event. #3490
Description
Which project does this relate to?
Start
Describe the bug
When canceling a server function or fetch to API Route, either by abortController.abort() or by refreshing / closing the tab to disconnect. The signal's 'abort' event is actually never fired on the server side.
I've made a minimal reproduction here:
https://github.com/cpakken/start-signal-issue
On the terminal (server), it should console.log for 'abort' signal, but the server function will just continue its work. For computationally heavy server functions, you could use this abort event to trip a flag so that it stops any processing mid way through. Or if you pass the abort signal to a fetch called from the server, it never actually cancels it.
This might have to do with H3 Fetch and the way it implements aborts or detects when connection is closed abruptly?
Heres implementation from the minimal reproduction:
import { createServerFn } from '@tanstack/start'
export const fooServerFn = createServerFn({ method: 'POST' }).handler(async ({ signal }) => {
signal.addEventListener('abort', () => {
//THIS IS NEVER RUN!!!!
//This should run when abort controller is called on the client side
//Or when the client disconnects
console.log('ABORTED: THIS NEVER RUNS!')
throw new Error('aborted')
})
await new Promise((resolve) => setTimeout(resolve, 2000))
//Why is this signal already aborted?!? when abort is not called?
console.log('RETURNED SUCCESS BUT IS Signal aborted?', signal.aborted)
return {
data: 'SERVER FN: FOO!',
}
})import { json } from '@tanstack/start'
import { createAPIFileRoute } from '@tanstack/start/api'
export const APIRoute = createAPIFileRoute('/api/foo')({
GET: async ({ request }) => {
const { signal } = request
signal.addEventListener('abort', () => {
//THIS IS NEVER RUN!!!!
//This should run when abort controller is called on the client side
//Or when the client disconnects
console.log('ABORTED: THIS NEVER RUNS!')
throw new Error('aborted')
})
console.log('FETCHING...')
await new Promise((resolve) => setTimeout(resolve, 2000))
console.log('RETURNED SUCCESS BUT IS Signal aborted?', signal.aborted)
return json({
data: 'API: DATA IS FOO!',
})
},
})I've been using this as my abort signal, but ideally the request.signal or signal passed in from createServerFn should just work
import { getEvent } from 'vinxi/http'
export const createAbortSignal = () => {
const controller = new AbortController()
const { res } = getEvent().node
res.on('close', () => {
// console.log('RES close')
if (!res.writableEnded) {
console.log('aborting')
controller.abort()
}
})
return controller.signal
}Your Example Website or App
https://github.com/cpakken/start-signal-issue
Steps to Reproduce the Bug or Issue
https://github.com/cpakken/start-signal-issue
npm run dev or bun dev
Click 'fetch' button and 'abort'
Click 'fetch' button and refresh the tab to disconnect
Expected behavior
In the terminal running Start, you should see the abort event triggering
Screenshots or Videos
No response
Platform
- OS: Windows 10
- Browser: Edge 133.0.3065.69
Additional context
No response
Activity
schiller-manuel commentedon Feb 21, 2025
I think your server function example is incorrect.
When running, I see
ABORTED: THIS NEVER RUNS!being logged on the server logs.However, you throw the error from the signal's event handler; this would NOT influence the execution of the server function.
In this example, throwing an error from the signal handler causes the node process to crash.
Instead, you should synchronize the signal's event handler with the function execution, e.g. as shown in the docs: https://tanstack.com/start/latest/docs/framework/react/server-functions#cancellation
I did not try the API route example, but I suspect it has the same issue.
cpakken commentedon Feb 21, 2025
I'm sorry, but I'm not recieving
ABORTED: THIS NEVER RUNS!on my end.I tried on both node v23 and v22
I updated to the latest version 1.109.2
To be clear, on the client, it will show ABORTED and update, but on the server terminal the
abortevent is just not triggered.Doesn't the docs say that you can throw errors?
https://tanstack.com/start/latest/docs/framework/react/server-functions#throwing-errors
Nevertheless, I've made changes so that its using
new Promiseinstead of async await but no difference. It's just not calling the 'abort' event for me. I don't think reject in new Promise is different than using async and throwing new Error.I've made the updates in
https://github.com/cpakken/start-signal-issue
schiller-manuel commentedon Feb 22, 2025
So it seems that signal works as expected for
GETserver functions, but not forPOSTmihaiandrei97 commentedon Mar 5, 2025
Seems it only works for GET server function. For a GET api route it is not called.
alvarosevilla95 commentedon May 4, 2025
I'm experiencing what I thing is the same issue. What I noticed, is that a serverFn POST triggered from the client seems to always have the signal in aborted status from the start of the execution.
See this sandbox ( (modified src/utils/posts serverFn)).
Go to /posts and refresh, then trigger a refetch from the query devtools. You will see the first time the server logs
aborted: falsewhen the serverFn is triggered from the ssr loader. When you refetch the query on the client, you will seeaborted: true.mihaiandrei97 commentedon Jul 9, 2025
Hello!
Seems the example from the docs never aborts the signal. If you try the following code:
The signal is never aborted on server side. So you will see the
console.log('Sending event... Count:', count)until it reaches 100.The fix for this seems to be to use the
closeevent from node and dispatch the abort to stop the stream.Full working example:
The behaviour is in both dev/prod environment.
Tested with a plain
create-tsrouter-app@latest:"@tanstack/react-router": "^1.121.2",
"@tanstack/react-start": "^1.121.2",
"@tanstack/router-plugin": "^1.121.2",
"vite": "^6.3.3",