Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/modern-birds-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tus/server": patch
---

Handle request cancellation gracefully on Node.js runtime
33 changes: 32 additions & 1 deletion packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ export class Server extends EventEmitter {
const context = this.createContext()
const headers = new Headers()

// Special case on the Node.js runtime,
// We handle gracefully request errors such as disconnects or timeouts.
// This is important to avoid memory leaks and ensure that the server can
// handle subsequent requests without issues.
if ('node' in req && req.node) {
const nodeReq = (req.node as { req: http.IncomingMessage }).req
nodeReq.once('error', () => {
context.abort()
})
}

const onError = async (error: {
status_code?: number
body?: string
Expand Down Expand Up @@ -212,7 +223,15 @@ export class Server extends EventEmitter {
// Invoke the handler for the method requested
const handler = this.handlers[req.method as keyof Handlers]
if (handler) {
return handler.send(req, context, headers).catch(onError)
const resp = await handler.send(req, context, headers).catch(onError)

if (context.signal.aborted) {
// If the request was aborted, we should not send any response body.
// The server should just close the connection.
return this.handleAbortedRequest(context, resp)
}

return resp
}

return this.write(context, headers, 404, 'Not found\n')
Expand Down Expand Up @@ -266,6 +285,18 @@ export class Server extends EventEmitter {
return this.datastore.deleteExpired()
}

protected handleAbortedRequest(context: CancellationContext, resp: Response) {
const isAborted = context.signal.aborted
if (isAborted) {
// If the request was aborted, we should not send any response body.
// The server should just close the connection.
resp.headers.set('Connection', 'close')
return resp
}

return resp
}

protected createContext() {
// Initialize two AbortControllers:
// 1. `requestAbortController` for instant request termination, particularly useful for stopping clients to upload when errors occur.
Expand Down
Loading