denoland/deno · warning
Deno.serve: request.signal aborts on successful responses (l
Error message
Deno.serve: request.signal aborts on successful responses (legacy behavior). To detect when a request has been fully delivered use the `completed` promise on the handler's info argument. Move cleanup to the handler's return path, or opt in to the new behavior with --unstable-no-legacy-abort. See https://docs.deno.com/go/unstable-no-legacy-abort
What it means
Deno.serve historically aborted request.signal after a response was sent, even successfully, which contradicts fetch-spec expectations. In default (legacy) mode, when close() runs with success=true and the handler had touched request.signal, this warning prints once per process (legacyAbortWarned at ext/http/00_serve.ts:131). It tells you your abort-based cleanup fires at the wrong time and points to the supported replacements.
Source
Thrown at ext/http/00_serve.ts:223
}
// The completion signal fires only if someone cares
if (this.#completed) {
if (success) {
this.#completed.resolve(undefined);
} else {
if (!this.#context.legacyAbort) {
abortRequest(this.request);
}
this.#completed.reject(
new Interrupted("HTTP response was not sent successfully"),
);
}
}
if (this.#context.legacyAbort) {
if (success && this.#signalAccessed && !legacyAbortWarned) {
legacyAbortWarned = true;
// deno-lint-ignore no-console
console.warn(
"Deno.serve: request.signal aborts on successful responses (legacy behavior). To detect when a request has been fully delivered use the `completed` promise on the handler's info argument. Move cleanup to the handler's return path, or opt in to the new behavior with --unstable-no-legacy-abort. See https://docs.deno.com/go/unstable-no-legacy-abort",
);
}
abortRequest(this.request);
}
this.#external = null;
}
get [_upgraded]() {
return this.#upgraded;
}
_throwIfUpgraded() {
if (this.#upgraded) {
throw new Deno.errors.Http("Already upgraded");
}
}
View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Move cleanup into the handler's return path (try/finally around building the Response) so it does not depend on the signal at all
- Use the completed promise on the handler's info argument (ext/http/00_serve.ts:280): it resolves when the response is fully delivered and rejects with Interrupted on failure
- Start deno with --unstable-no-legacy-abort to remove the legacy abort, so request.signal only aborts on real cancellation/failure
- Audit every req.signal listener so it tolerates an abort firing right after a successful response under the legacy default
Example fix
// before
Deno.serve((req) => {
req.signal.addEventListener("abort", () => release(req)); // fires after success too: warning
return handle(req);
});
// after
Deno.serve(async (req, info) => {
try {
return await handle(req);
} finally {
info.completed.then(() => release(req), () => release(req)); // delivered or failed
}
}); Defensive patterns
Strategy: try-catch
Validate before calling
// smoke out reliance on legacy semantics in CI: // deno run --unstable-no-legacy-abort server.test.ts // handlers that depended on post-success aborts will visibly change behavior there // instead of silently relying on the legacy default.
Try / catch
// cleanup that fires on full delivery OR failure — no request.signal dependency
Deno.serve(async (req, info) => {
try {
return await handle(req);
} finally {
info.completed.then(() => release(req), () => release(req));
}
}); Prevention
- Never wire request.signal 'abort' listeners for success-path cleanup in Deno.serve handlers
- Prefer info.completed or try/finally around response construction
- Run the suite with --unstable-no-legacy-abort to detect legacy reliance before the default flips
- Remember the warning prints only once per process; do not use its absence as proof of correctness
When it happens
Trigger: A handler accesses req.signal (e.g., req.signal.addEventListener('abort', cleanup) or AbortSignal.any with it) and the response then completes successfully; #signalAccessed and success must both be true, and the process must not have printed it already. It never fires with --unstable-no-legacy-abort, on failed responses, or when signal was never read.
Common situations: Porting Node http/express request-cleanup patterns to Deno.serve; SSE or long-poll handlers wiring teardown to the abort signal; libraries that defensively listen on request.signal for every request; teams wanting spec-compliant AbortSignal semantics before the default flips.
Related errors
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/44c30a2fe9e118cb.
Report an issue: GitHub.