| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent b54aa83 commit ee54d9d
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3010,13 +3010,20 @@ Calls `message.socket.setTimeout(msecs, callback)`. | |||
| 3010 | 3010 | ||
| 3011 | 3011 | <!-- YAML | |
| 3012 | 3012 | added: v24.16.0 | |
| 3013 | + changes: | ||
| 3014 | + - version: REPLACEME | ||
| 3015 | + pr-url: https://github.com/nodejs/node/pull/64392 | ||
| 3016 | + description: The signal is no longer aborted after the message | ||
| 3017 | + completes normally. | ||
| 3013 | 3018 | --> | |
| 3014 | 3019 | ||
| 3015 | 3020 | * Type: {AbortSignal} | |
| 3016 | 3021 | ||
| 3017 | - An {AbortSignal} that is aborted when the underlying socket closes or the | ||
| 3018 | - request is destroyed. The signal is created lazily on first access — no | ||
| 3019 | - {AbortController} is allocated for requests that never use this property. | ||
| 3022 | + An {AbortSignal} that is aborted when the message is destroyed before | ||
| 3023 | + completion or when its underlying socket closes before request handling or | ||
| 3024 | + response reading completes. | ||
| 3025 | + The signal is created lazily on first access — no {AbortController} is allocated | ||
| 3026 | + for requests that never use this property. | ||
| 3020 | 3027 | ||
| 3021 | 3028 | This is useful for cancelling downstream asynchronous work such as database | |
| 3022 | 3029 | queries or `fetch` calls when a client disconnects mid-request. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -50,6 +50,7 @@ const { | |||
| 50 | 50 | prepareError, | |
| 51 | 51 | kSkipPendingData, | |
| 52 | 52 | } = require('_http_common'); | |
| 53 | + const { kDetachAbortSignal } = require('_http_incoming'); | ||
| 53 | 54 | const { | |
| 54 | 55 | kHighWaterMark, | |
| 55 | 56 | kUniqueHeaders, | |
@@ -1017,6 +1018,8 @@ function responseOnEnd() { | |||
| 1017 | 1018 | const req = this.req; | |
| 1018 | 1019 | const socket = req.socket; | |
| 1019 | 1020 | ||
| 1021 | + this[kDetachAbortSignal](); | ||
| 1022 | + | ||
| 1020 | 1023 | if (socket) { | |
| 1021 | 1024 | if (req.timeoutCb) socket.removeListener('timeout', emitRequestTimeout); | |
| 1022 | 1025 | socket.removeListener('timeout', responseOnTimeout); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -38,6 +38,11 @@ const kTrailers = Symbol('kTrailers'); | |||
| 38 | 38 | const kTrailersDistinct = Symbol('kTrailersDistinct'); | |
| 39 | 39 | const kTrailersCount = Symbol('kTrailersCount'); | |
| 40 | 40 | const kAbortController = Symbol('kAbortController'); | |
| 41 | + const kAbortSignalSocket = Symbol('kAbortSignalSocket'); | ||
| 42 | + const kAbortSignalListener = Symbol('kAbortSignalListener'); | ||
| 43 | + const kAbortSignalDetached = Symbol('kAbortSignalDetached'); | ||
| 44 | + const kAttachAbortSignal = Symbol('kAttachAbortSignal'); | ||
| 45 | + const kDetachAbortSignal = Symbol('kDetachAbortSignal'); | ||
| 41 | 46 | ||
| 42 | 47 | function readStart(socket) { | |
| 43 | 48 | if (socket && !socket._paused && socket.readable) | |
@@ -94,6 +99,9 @@ function IncomingMessage(socket) { | |||
| 94 | 99 | // read by the user, so there's no point continuing to handle it. | |
| 95 | 100 | this._dumped = false; | |
| 96 | 101 | this[kAbortController] = null; | |
| 102 | + this[kAbortSignalSocket] = null; | ||
| 103 | + this[kAbortSignalListener] = null; | ||
| 104 | + this[kAbortSignalDetached] = false; | ||
| 97 | 105 | } | |
| 98 | 106 | ObjectSetPrototypeOf(IncomingMessage.prototype, Readable.prototype); | |
| 99 | 107 | ObjectSetPrototypeOf(IncomingMessage, Readable); | |
@@ -195,18 +203,51 @@ ObjectDefineProperty(IncomingMessage.prototype, 'signal', { | |||
| 195 | 203 | if (this[kAbortController] === null) { | |
| 196 | 204 | const ac = new AbortController(); | |
| 197 | 205 | this[kAbortController] = ac; | |
| 198 | - if (this.destroyed) { | ||
| 206 | + if (this.destroyed && (!this.readableEnded || !this.complete)) { | ||
| 199 | 207 | ac.abort(); | |
| 200 | 208 | } else { | |
| 201 | - this.once('close', function() { | ||
| 202 | - ac.abort(); | ||
| 203 | - }); | ||
| 209 | + this[kAttachAbortSignal](); | ||
| 204 | 210 | } | |
| 205 | 211 | } | |
| 206 | 212 | return this[kAbortController].signal; | |
| 207 | 213 | }, | |
| 208 | 214 | }); | |
| 209 | 215 | ||
| 216 | + IncomingMessage.prototype[kAttachAbortSignal] = function() { | ||
| 217 | + if (this[kAbortController].signal.aborted || | ||
| 218 | + this[kAbortSignalDetached] || | ||
| 219 | + this[kAbortSignalListener] !== null) { | ||
| 220 | + return; | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + const socket = this.socket; | ||
| 224 | + if (!socket) { | ||
| 225 | + return; | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + if (socket.destroyed) { | ||
| 229 | + abortSignal(this); | ||
| 230 | + return; | ||
| 231 | + } | ||
| 232 | + | ||
| 233 | + this[kAbortSignalSocket] = socket; | ||
| 234 | + this[kAbortSignalListener] = () => { | ||
| 235 | + abortSignal(this); | ||
| 236 | + }; | ||
| 237 | + socket.once('close', this[kAbortSignalListener]); | ||
| 238 | + }; | ||
| 239 | + | ||
| 240 | + IncomingMessage.prototype[kDetachAbortSignal] = function() { | ||
| 241 | + const socket = this[kAbortSignalSocket]; | ||
| 242 | + const listener = this[kAbortSignalListener]; | ||
| 243 | + this[kAbortSignalDetached] = true; | ||
| 244 | + this[kAbortSignalSocket] = null; | ||
| 245 | + this[kAbortSignalListener] = null; | ||
| 246 | + if (socket !== null && listener !== null) { | ||
| 247 | + socket.removeListener('close', listener); | ||
| 248 | + } | ||
| 249 | + }; | ||
| 250 | + | ||
| 210 | 251 | IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { | |
| 211 | 252 | if (callback) | |
| 212 | 253 | this.on('timeout', callback); | |
@@ -234,6 +275,7 @@ IncomingMessage.prototype._destroy = function _destroy(err, cb) { | |||
| 234 | 275 | if (!this.readableEnded || !this.complete) { | |
| 235 | 276 | this.aborted = true; | |
| 236 | 277 | this.emit('aborted'); | |
| 278 | + abortSignal(this); | ||
| 237 | 279 | } | |
| 238 | 280 | ||
| 239 | 281 | // If aborted and the underlying socket is not already destroyed, | |
@@ -255,6 +297,13 @@ IncomingMessage.prototype._destroy = function _destroy(err, cb) { | |||
| 255 | 297 | } | |
| 256 | 298 | }; | |
| 257 | 299 | ||
| 300 | + function abortSignal(self) { | ||
| 301 | + self[kDetachAbortSignal](); | ||
| 302 | + if (self[kAbortController] !== null) { | ||
| 303 | + self[kAbortController].abort(); | ||
| 304 | + } | ||
| 305 | + } | ||
| 306 | + | ||
| 258 | 307 | IncomingMessage.prototype._addHeaderLines = _addHeaderLines; | |
| 259 | 308 | function _addHeaderLines(headers, n) { | |
| 260 | 309 | if (headers?.length) { | |
@@ -472,6 +521,7 @@ function onError(self, error, cb) { | |||
| 472 | 521 | ||
| 473 | 522 | module.exports = { | |
| 474 | 523 | IncomingMessage, | |
| 524 | + kDetachAbortSignal, | ||
| 475 | 525 | readStart, | |
| 476 | 526 | readStop, | |
| 477 | 527 | }; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -68,7 +68,10 @@ const { | |||
| 68 | 68 | defaultTriggerAsyncIdScope, | |
| 69 | 69 | getOrSetAsyncId, | |
| 70 | 70 | } = require('internal/async_hooks'); | |
| 71 | - const { IncomingMessage } = require('_http_incoming'); | ||
| 71 | + const { | ||
| 72 | + IncomingMessage, | ||
| 73 | + kDetachAbortSignal, | ||
| 74 | + } = require('_http_incoming'); | ||
| 72 | 75 | const { | |
| 73 | 76 | ConnResetException, | |
| 74 | 77 | codes: { | |
@@ -1105,6 +1108,7 @@ function resOnFinish(req, res, socket, state, server) { | |||
| 1105 | 1108 | // array will be empty. | |
| 1106 | 1109 | assert(state.incoming.length === 0 || state.incoming[0] === req); | |
| 1107 | 1110 | ||
| 1111 | + req[kDetachAbortSignal](); | ||
| 1108 | 1112 | state.incoming.shift(); | |
| 1109 | 1113 | ||
| 1110 | 1114 | // If the user never called req.read(), and didn't pipe() or | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,7 +4,7 @@ const common = require('../common'); | |||
| 4 | 4 | const assert = require('assert'); | |
| 5 | 5 | const http = require('http'); | |
| 6 | 6 | ||
| 7 | - // Test 1: req.signal is an AbortSignal and aborts on 'close' | ||
| 7 | + // Test 1: req.signal is an AbortSignal and aborts on socket close | ||
| 8 | 8 | { | |
| 9 | 9 | const server = http.createServer(common.mustCall((req, res) => { | |
| 10 | 10 | assert.ok(req.signal instanceof AbortSignal); | |
@@ -21,21 +21,68 @@ const http = require('http'); | |||
| 21 | 21 | })); | |
| 22 | 22 | } | |
| 23 | 23 | ||
| 24 | - // Test 2: req.signal is aborted if accessed after destroy | ||
| 24 | + // Test 2: req.signal is not aborted when a request body completes normally. | ||
| 25 | + { | ||
| 26 | + const body = JSON.stringify({ hello: 'world' }); | ||
| 27 | + const server = http.createServer(common.mustCall((req, res) => { | ||
| 28 | + assert.ok(req.signal instanceof AbortSignal); | ||
| 29 | + assert.strictEqual(req.signal.aborted, false); | ||
| 30 | + req.signal.onabort = common.mustNotCall(); | ||
| 31 | + | ||
| 32 | + req.on('close', common.mustCall(() => { | ||
| 33 | + assert.strictEqual(req.aborted, false); | ||
| 34 | + assert.strictEqual(req.complete, true); | ||
| 35 | + assert.strictEqual(req.signal.aborted, false); | ||
| 36 | + })); | ||
| 37 | + | ||
| 38 | + req.on('end', common.mustCall(() => { | ||
| 39 | + setTimeout(common.mustCall(() => { | ||
| 40 | + assert.strictEqual(req.aborted, false); | ||
| 41 | + assert.strictEqual(req.complete, true); | ||
| 42 | + assert.strictEqual(req.signal.aborted, false); | ||
| 43 | + res.end('ok'); | ||
| 44 | + }), 10); | ||
| 45 | + })); | ||
| 46 | + req.resume(); | ||
| 47 | + })); | ||
| 48 | + | ||
| 49 | + server.listen(0, common.mustCall(() => { | ||
| 50 | + const clientReq = http.request( | ||
| 51 | + { | ||
| 52 | + port: server.address().port, | ||
| 53 | + method: 'PATCH', | ||
| 54 | + path: '/tables/1', | ||
| 55 | + headers: { | ||
| 56 | + 'content-type': 'application/json', | ||
| 57 | + 'content-length': Buffer.byteLength(body), | ||
| 58 | + }, | ||
| 59 | + }, | ||
| 60 | + common.mustCall((res) => { | ||
| 61 | + res.resume(); | ||
| 62 | + res.on('end', common.mustCall(() => { | ||
| 63 | + server.close(); | ||
| 64 | + })); | ||
| 65 | + }), | ||
| 66 | + ); | ||
| 67 | + clientReq.end(body); | ||
| 68 | + })); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + // Test 3: req.signal is aborted if accessed after destroy | ||
| 25 | 72 | { | |
| 26 | 73 | const req = new http.IncomingMessage(null); | |
| 27 | 74 | req.destroy(); | |
| 28 | 75 | assert.strictEqual(req.signal.aborted, true); | |
| 29 | 76 | } | |
| 30 | 77 | ||
| 31 | - // Test 3: Multiple accesses return the same signal | ||
| 78 | + // Test 4: Multiple accesses return the same signal | ||
| 32 | 79 | { | |
| 33 | 80 | const req = new http.IncomingMessage(null); | |
| 34 | 81 | assert.strictEqual(req.signal, req.signal); | |
| 35 | 82 | } | |
| 36 | 83 | ||
| 37 | 84 | ||
| 38 | - // Test 4: res.signal on a client-side http.request() response (IncomingMessage). | ||
| 85 | + // Test 5: res.signal on a client-side http.request() response (IncomingMessage). | ||
| 39 | 86 | { | |
| 40 | 87 | const server = http.createServer(common.mustCall((req, res) => { | |
| 41 | 88 | res.writeHead(200); | |
@@ -61,7 +108,36 @@ const http = require('http'); | |||
| 61 | 108 | })); | |
| 62 | 109 | } | |
| 63 | 110 | ||
| 64 | - // Test 5: Client cancels a pending request. | ||
| 111 | + // Test 6: res.signal is not aborted when a response body completes normally. | ||
| 112 | + { | ||
| 113 | + const server = http.createServer(common.mustCall((req, res) => { | ||
| 114 | + res.end('ok'); | ||
| 115 | + })); | ||
| 116 | + | ||
| 117 | + server.listen(0, common.mustCall(() => { | ||
| 118 | + const clientReq = http.request( | ||
| 119 | + { port: server.address().port }, | ||
| 120 | + common.mustCall((res) => { | ||
| 121 | + assert.ok(res.signal instanceof AbortSignal); | ||
| 122 | + assert.strictEqual(res.signal.aborted, false); | ||
| 123 | + res.signal.onabort = common.mustNotCall(); | ||
| 124 | + | ||
| 125 | + res.resume(); | ||
| 126 | + res.on('end', common.mustCall(() => { | ||
| 127 | + assert.strictEqual(res.complete, true); | ||
| 128 | + assert.strictEqual(res.signal.aborted, false); | ||
| 129 | + })); | ||
| 130 | + res.on('close', common.mustCall(() => { | ||
| 131 | + assert.strictEqual(res.signal.aborted, false); | ||
| 132 | + server.close(); | ||
| 133 | + })); | ||
| 134 | + }), | ||
| 135 | + ); | ||
| 136 | + clientReq.end(); | ||
| 137 | + })); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + // Test 7: Client cancels a pending request. | ||
| 65 | 141 | { | |
| 66 | 142 | const server = http.createServer(common.mustCall((req, res) => { | |
| 67 | 143 | req.signal.onabort = common.mustCall(() => { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments