Skip to content

Commit bf1aebc

Browse files
http: add req.signal to IncomingMessage
PR-URL: #62541 Fixes: #62481 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com>
1 parent 3bb416c commit bf1aebc

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed

doc/api/http.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2989,6 +2989,51 @@ added: v0.5.9
29892989

29902990
Calls `message.socket.setTimeout(msecs, callback)`.
29912991

2992+
### `message.signal`
2993+
2994+
<!-- YAML
2995+
added: REPLACEME
2996+
-->
2997+
2998+
* Type: {AbortSignal}
2999+
3000+
An {AbortSignal} that is aborted when the underlying socket closes or the
3001+
request is destroyed. The signal is created lazily on first access — no
3002+
{AbortController} is allocated for requests that never use this property.
3003+
3004+
This is useful for cancelling downstream asynchronous work such as database
3005+
queries or `fetch` calls when a client disconnects mid-request.
3006+
3007+
```mjs
3008+
import http from 'node:http';
3009+
3010+
http.createServer(async (req, res) => {
3011+
try {
3012+
const data = await fetch('https://example.com/api', { signal: req.signal });
3013+
res.end(JSON.stringify(await data.json()));
3014+
} catch (err) {
3015+
if (err.name === 'AbortError') return;
3016+
res.statusCode = 500;
3017+
res.end('Internal Server Error');
3018+
}
3019+
}).listen(3000);
3020+
```
3021+
3022+
```cjs
3023+
const http = require('node:http');
3024+
3025+
http.createServer(async (req, res) => {
3026+
try {
3027+
const data = await fetch('https://example.com/api', { signal: req.signal });
3028+
res.end(JSON.stringify(await data.json()));
3029+
} catch (err) {
3030+
if (err.name === 'AbortError') return;
3031+
res.statusCode = 500;
3032+
res.end('Internal Server Error');
3033+
}
3034+
}).listen(3000);
3035+
```
3036+
29923037
### `message.socket`
29933038

29943039
<!-- YAML

lib/_http_incoming.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@ const {
2929

3030
const { Readable, finished } = require('stream');
3131

32+
const { AbortController } = require('internal/abort_controller');
33+
3234
const kHeaders = Symbol('kHeaders');
3335
const kHeadersDistinct = Symbol('kHeadersDistinct');
3436
const kHeadersCount = Symbol('kHeadersCount');
3537
const kTrailers = Symbol('kTrailers');
3638
const kTrailersDistinct = Symbol('kTrailersDistinct');
3739
const kTrailersCount = Symbol('kTrailersCount');
40+
const kAbortController = Symbol('kAbortController');
3841

3942
function readStart(socket) {
4043
if (socket && !socket._paused && socket.readable)
@@ -90,6 +93,7 @@ function IncomingMessage(socket) {
9093
// Flag for when we decide that this message cannot possibly be
9194
// read by the user, so there's no point continuing to handle it.
9295
this._dumped = false;
96+
this[kAbortController] = null;
9397
}
9498
ObjectSetPrototypeOf(IncomingMessage.prototype, Readable.prototype);
9599
ObjectSetPrototypeOf(IncomingMessage, Readable);
@@ -184,6 +188,25 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', {
184188
},
185189
});
186190

191+
ObjectDefineProperty(IncomingMessage.prototype, 'signal', {
192+
__proto__: null,
193+
configurable: true,
194+
get: function() {
195+
if (this[kAbortController] === null) {
196+
const ac = new AbortController();
197+
this[kAbortController] = ac;
198+
if (this.destroyed) {
199+
ac.abort();
200+
} else {
201+
this.once('close', function() {
202+
ac.abort();
203+
});
204+
}
205+
}
206+
return this[kAbortController].signal;
207+
},
208+
});
209+
187210
IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) {
188211
if (callback)
189212
this.on('timeout', callback);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
7+
// Test 1: req.signal is an AbortSignal and aborts on 'close'
8+
{
9+
const server = http.createServer(common.mustCall((req, res) => {
10+
assert.ok(req.signal instanceof AbortSignal);
11+
assert.strictEqual(req.signal.aborted, false);
12+
req.signal.onabort = common.mustCall(() => {
13+
assert.strictEqual(req.signal.aborted, true);
14+
});
15+
res.destroy();
16+
}));
17+
server.listen(0, common.mustCall(() => {
18+
http.get({ port: server.address().port }, () => {}).on('error', () => {
19+
server.close();
20+
});
21+
}));
22+
}
23+
24+
// Test 2: req.signal is aborted if accessed after destroy
25+
{
26+
const req = new http.IncomingMessage(null);
27+
req.destroy();
28+
assert.strictEqual(req.signal.aborted, true);
29+
}
30+
31+
// Test 3: Multiple accesses return the same signal
32+
{
33+
const req = new http.IncomingMessage(null);
34+
assert.strictEqual(req.signal, req.signal);
35+
}
36+
37+
38+
// Test 4: res.signal on a client-side http.request() response (IncomingMessage).
39+
{
40+
const server = http.createServer(common.mustCall((req, res) => {
41+
res.writeHead(200);
42+
res.write('partial');
43+
}));
44+
45+
server.listen(0, common.mustCall(() => {
46+
const clientReq = http.request(
47+
{ port: server.address().port },
48+
common.mustCall((res) => {
49+
assert.ok(res.signal instanceof AbortSignal);
50+
assert.strictEqual(res.signal.aborted, false);
51+
52+
res.signal.onabort = common.mustCall(() => {
53+
assert.strictEqual(res.signal.aborted, true);
54+
server.close();
55+
});
56+
clientReq.destroy();
57+
}),
58+
);
59+
clientReq.on('error', () => {});
60+
clientReq.end();
61+
}));
62+
}
63+
64+
// Test 5: Client cancels a pending request.
65+
{
66+
const server = http.createServer(common.mustCall((req, res) => {
67+
req.signal.onabort = common.mustCall(() => {
68+
assert.strictEqual(req.signal.aborted, true);
69+
server.close();
70+
});
71+
res.flushHeaders();
72+
}));
73+
74+
server.listen(0, common.mustCall(() => {
75+
const clientReq = http.request(
76+
{ port: server.address().port },
77+
common.mustCall((res) => {
78+
res.on('error', () => {});
79+
clientReq.destroy();
80+
}),
81+
);
82+
clientReq.on('error', () => {});
83+
clientReq.end();
84+
}));
85+
}

0 commit comments

Comments
 (0)