{"record":{"id":"44c30a2fe9e118cb","repo":"denoland/deno","slug":"deno-serve-request-signal-aborts-on-successful-re","errorCode":null,"errorMessage":"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","messagePattern":"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","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"ext/http/00_serve.ts","lineNumber":223,"sourceCode":"    }\n    // The completion signal fires only if someone cares\n    if (this.#completed) {\n      if (success) {\n        this.#completed.resolve(undefined);\n      } else {\n        if (!this.#context.legacyAbort) {\n          abortRequest(this.request);\n        }\n        this.#completed.reject(\n          new Interrupted(\"HTTP response was not sent successfully\"),\n        );\n      }\n    }\n    if (this.#context.legacyAbort) {\n      if (success && this.#signalAccessed && !legacyAbortWarned) {\n        legacyAbortWarned = true;\n        // deno-lint-ignore no-console\n        console.warn(\n          \"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\",\n        );\n      }\n      abortRequest(this.request);\n    }\n    this.#external = null;\n  }\n\n  get [_upgraded]() {\n    return this.#upgraded;\n  }\n\n  _throwIfUpgraded() {\n    if (this.#upgraded) {\n      throw new Deno.errors.Http(\"Already upgraded\");\n    }\n  }\n","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/http/00_serve.ts#L205-L241","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nDeno.serve((req) => {\n  req.signal.addEventListener(\"abort\", () => release(req)); // fires after success too: warning\n  return handle(req);\n});\n\n// after\nDeno.serve(async (req, info) => {\n  try {\n    return await handle(req);\n  } finally {\n    info.completed.then(() => release(req), () => release(req)); // delivered or failed\n  }\n});","handlingStrategy":"try-catch","validationCode":"// smoke out reliance on legacy semantics in CI:\n//   deno run --unstable-no-legacy-abort server.test.ts\n// handlers that depended on post-success aborts will visibly change behavior there\n// instead of silently relying on the legacy default.","typeGuard":null,"tryCatchPattern":"// cleanup that fires on full delivery OR failure — no request.signal dependency\nDeno.serve(async (req, info) => {\n  try {\n    return await handle(req);\n  } finally {\n    info.completed.then(() => release(req), () => release(req));\n  }\n});","preventionTips":["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"],"tags":["http","serve","abortsignal","legacy-behavior","lifecycle"],"backgroundTag":"unexpected-abort-signal","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}