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

  1. Move cleanup into the handler's return path (try/finally around building the Response) so it does not depend on the signal at all
  2. 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
  3. Start deno with --unstable-no-legacy-abort to remove the legacy abort, so request.signal only aborts on real cancellation/failure
  4. 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

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.