Skip to content

Commit 025da4a

Browse files
authored
docs: document Deno.serve legacy request abort behavior (#3386)
1 parent 58e8c37 commit 025da4a

3 files changed

Lines changed: 113 additions & 3 deletions

File tree

go.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"sandbox": "/runtime/reference/cli/sandbox/",
3737
"serve": "/runtime/reference/cli/serve/",
3838
"test": "/runtime/reference/cli/test/",
39+
"unstable-no-legacy-abort": "/runtime/reference/deno_serve_legacy_abort/",
3940
"upgrade": "/runtime/reference/cli/upgrade/",
4041
"databases": "/deploy/reference/databases/",
4142
"pre-deploy": "/deploy/reference/builds/",

runtime/reference/cli/unstable_flags.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
last_modified: 2026-06-24
2+
last_modified: 2026-07-02
33
title: "Unstable feature flags"
44
oldUrl:
55
- /runtime/tools/unstable_flags/
@@ -227,8 +227,13 @@ rather than installing every npm package listed in `package.json` on startup.
227227
## `--unstable-no-legacy-abort`
228228

229229
Use the abort signal in [`Deno.serve`](/api/deno/~/Deno.serve) without the
230-
legacy behavior. With this flag the server is not aborted when a request is
231-
handled successfully.
230+
legacy behavior. With this flag `request.signal` aborts only when the client
231+
actually disconnects, rather than on every successful response.
232+
233+
See
234+
[Deno.serve request abort behavior](/runtime/reference/deno_serve_legacy_abort/)
235+
for why this is changing and how to detect when a request has been fully
236+
delivered.
232237

233238
## `--unstable`
234239

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
last_modified: 2026-07-02
3+
title: "Deno.serve request abort behavior"
4+
description: "Understand the legacy request.signal abort behavior in Deno.serve, why it is changing, and how to detect request completion with the --unstable-no-legacy-abort flag."
5+
---
6+
7+
[`Deno.serve`](/api/deno/~/Deno.serve) historically fired the `abort` event on a
8+
request's
9+
[`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
10+
(`request.signal`) whenever the request finished, **including when your handler
11+
returned a successful response**. The signal was meant to indicate that the
12+
client went away (for example, the connection was closed before the response was
13+
sent), but in practice it also fired on every successful response.
14+
15+
This is the "legacy abort" behavior, and it is enabled by default today. The
16+
[`--unstable-no-legacy-abort`](/runtime/reference/cli/unstable_flags/#--unstable-no-legacy-abort)
17+
flag opts in to the corrected behavior, which will become the default in an
18+
upcoming release.
19+
20+
## Why the behavior is changing
21+
22+
Because the signal aborted on success, code that watches `request.signal` could
23+
not tell the difference between "the client disconnected" and "the response was
24+
delivered normally". This breaks a number of common libraries, most visibly
25+
Node-style HTTP proxies such as
26+
[`http-proxy`](https://www.npmjs.com/package/http-proxy) and
27+
[`http-proxy-middleware`](https://www.npmjs.com/package/http-proxy-middleware)
28+
(used by Vite, among others), which treat the upstream abort as a genuine
29+
failure and surface a spurious error.
30+
31+
See issues [#28850](https://github.com/denoland/deno/issues/28850) and
32+
[#29111](https://github.com/denoland/deno/issues/29111) for background.
33+
34+
## The new behavior
35+
36+
With `--unstable-no-legacy-abort` enabled, `request.signal` aborts **only** when
37+
the client actually cancels the request or the connection is lost before the
38+
response has been fully sent. A successful response no longer triggers an abort.
39+
40+
```ts
41+
Deno.serve((req) => {
42+
req.signal.addEventListener("abort", () => {
43+
// With --unstable-no-legacy-abort this only runs on a real client
44+
// disconnect, not on every successful response.
45+
console.log("client went away");
46+
});
47+
48+
return new Response("hello");
49+
});
50+
```
51+
52+
## Detecting when a request has been fully delivered
53+
54+
If you relied on the legacy abort to know that a response had been fully sent,
55+
use the `completed` promise on the second argument to the handler
56+
([`ServeHandlerInfo`](/api/deno/~/Deno.ServeHandlerInfo)) instead. It is the
57+
intended, mode-independent way to observe delivery:
58+
59+
```ts
60+
Deno.serve((req, info) => {
61+
info.completed
62+
.then(() => {
63+
// The response, including a streaming body, was sent successfully.
64+
console.log("response fully delivered");
65+
})
66+
.catch((err) => {
67+
// Delivery failed partway through (client disconnected, write error, ...).
68+
console.error("response was not sent successfully:", err);
69+
});
70+
71+
return new Response(someStream);
72+
});
73+
```
74+
75+
`info.completed`:
76+
77+
- **resolves** once the response (including any streaming body) has been sent to
78+
the client, and
79+
- **rejects** if delivery failed before completing.
80+
81+
The promise is lazily created, so handlers that never read `info.completed` pay
82+
no extra cost.
83+
84+
## Migrating
85+
86+
1. Enable the flag while you test:
87+
88+
```sh
89+
deno run --unstable-no-legacy-abort main.ts
90+
```
91+
92+
or in `deno.json`:
93+
94+
```json title="deno.json"
95+
{
96+
"unstable": ["no-legacy-abort"]
97+
}
98+
```
99+
100+
2. Use `request.signal` only for cancellation, and move completion/cleanup logic
101+
onto `info.completed` (or onto the handler's return path).
102+
103+
Once you have adopted the new behavior, your code will continue to work
104+
unchanged when it becomes the default.

0 commit comments

Comments
 (0)