Skip to main content
On this page

Deno.serve historically fired the abort event on a request's AbortSignal (request.signal) whenever the request finished, including when your handler returned a successful response. The signal was meant to indicate that the client went away (for example, the connection was closed before the response was sent), but in practice it also fired on every successful response.

This is the "legacy abort" behavior, and it is enabled by default today. The --unstable-no-legacy-abort flag opts in to the corrected behavior, which will become the default in an upcoming release.

Why the behavior is changing Jump to heading

Because the signal aborted on success, code that watches request.signal could not tell the difference between "the client disconnected" and "the response was delivered normally". This breaks a number of common libraries, most visibly Node-style HTTP proxies such as http-proxy and http-proxy-middleware (used by Vite, among others), which treat the upstream abort as a genuine failure and surface a spurious error.

See issues #28850 and #29111 for background.

The new behavior Jump to heading

With --unstable-no-legacy-abort enabled, request.signal aborts only when the client actually cancels the request or the connection is lost before the response has been fully sent. A successful response no longer triggers an abort.

Deno.serve((req) => {
  req.signal.addEventListener("abort", () => {
    // With --unstable-no-legacy-abort this only runs on a real client
    // disconnect, not on every successful response.
    console.log("client went away");
  });

  return new Response("hello");
});

Detecting when a request has been fully delivered Jump to heading

If you relied on the legacy abort to know that a response had been fully sent, use the completed promise on the second argument to the handler (ServeHandlerInfo) instead. It is the intended, mode-independent way to observe delivery:

Deno.serve((req, info) => {
  info.completed
    .then(() => {
      // The response, including a streaming body, was sent successfully.
      console.log("response fully delivered");
    })
    .catch((err) => {
      // Delivery failed partway through (client disconnected, write error, ...).
      console.error("response was not sent successfully:", err);
    });

  return new Response(someStream);
});

info.completed:

  • resolves once the response (including any streaming body) has been sent to the client, and
  • rejects if delivery failed before completing.

The promise is lazily created, so handlers that never read info.completed pay no extra cost.

Migrating Jump to heading

  1. Enable the flag while you test:

    ›_
    deno run --unstable-no-legacy-abort main.ts
    

    or in deno.json:

    deno.json
    {
      "unstable": ["no-legacy-abort"]
    }
    
  2. Use request.signal only for cancellation, and move completion/cleanup logic onto info.completed (or onto the handler's return path).

Once you have adopted the new behavior, your code will continue to work unchanged when it becomes the default.

Last updated on

Did you find what you needed?

Edit this page
Privacy policy