Commit dad9548

Browse files
trivenayaduh95
authored andcommitted
http: emit drain on socket takeover and avoid stale HWM reuse
When OutgoingMessage transitions from pre-socket buffering (Path B) to socket-connected writing (Path A), the backpressure domain changes β€” subsequent writes go directly to the socket, which enforces its own backpressure via socket.write() return values. The OM should emit drain at this transition point to signal that its buffer is clear and the caller can resume writing under the socket backpressure regime. Previously, _flush() gated drain emission on writableLength === 0 which included socket.writableLength. This conflated two independent backpressure domains: the OM pre-socket buffer and the socket kernel write queue. When the socket had a higher writableHighWaterMark than the OM (e.g. agent-reused socket from a prior request), the socket was never backpressured and never emitted drain, causing a permanent deadlock. Additionally, avoid reusing a pooled socket in http.Agent when its writableHighWaterMark differs from the request highWaterMark, so that the user backpressure threshold is respected for the common case of the built-in Agent. Signed-off-by: Naman Trivedi <trivenay@amazon.com> Fixes: #64680 Refs: #64653 Refs: #62936 PR-URL: #64991 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: GΓΌrgΓΌn DayΔ±oğlu <hey@gurgun.day>
1 parent 4674a10 commit dad9548

4 files changed

Lines changed: 127 additions & 2 deletions

File tree

β€Žlib/_http_agent.jsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
392392
constsockLen=freeLen+this.sockets[name].length;
393393

394394
// Reusing a socket from the pool.
395+
// If the caller specified a highWaterMark that differs from the pooled
396+
// socket's writableHighWaterMark, sync the socket's HWM so that
397+
// backpressure semantics match what the caller requested.
398+
if(socket&&options.highWaterMark!=null&&
399+
socket.writableHighWaterMark!==options.highWaterMark){
400+
debug('sync reused socket HWM (socket=%d, request=%d)',
401+
socket.writableHighWaterMark,options.highWaterMark);
402+
socket._writableState.highWaterMark=options.highWaterMark;
403+
}
404+
395405
if(socket){
396406
asyncResetHandle(socket);
397407
this.reuseSocket(socket,req);

β€Žlib/_http_outgoing.jsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,12 +1230,14 @@ OutgoingMessage.prototype._flush = function _flush() {
12301230

12311231
if(socket?.writable){
12321232
// There might be remaining data in this.output; write it out
1233-
this._flushOutput(socket);
1233+
constret=this._flushOutput(socket);
12341234

12351235
if(this.finished){
12361236
// This is a queue to the server or client to bring in the next this.
12371237
this._finish();
1238-
}elseif(this[kNeedDrain]&&this.writableLength===0){
1238+
}elseif(this[kNeedDrain]&&ret!==false){
1239+
// Socket accepted all data without backpressure - it won't emit
1240+
// drain, so we emit it since the OM buffer is now clear.
12391241
this[kNeedDrain]=false;
12401242
this.emit('drain');
12411243
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
// Regression test: when a pooled socket's writableHighWaterMark differs from
4+
// the new request's highWaterMark, the agent must sync the socket's HWM so
5+
// that backpressure semantics match what the caller requested.
6+
//
7+
// See: https://github.com/nodejs/node/issues/64680
8+
9+
constcommon=require('../common');
10+
constassert=require('assert');
11+
consthttp=require('http');
12+
13+
constserver=http.createServer(common.mustCall((req,res)=>{
14+
req.resume();
15+
req.on('end',()=>res.end('ok'));
16+
},2));
17+
18+
server.listen(0,common.mustCall(()=>{
19+
constport=server.address().port;
20+
constagent=newhttp.Agent({keepAlive: true});
21+
22+
// Request A: creates socket with HWM=1MB.
23+
http.request({
24+
host: 'localhost', port,method: 'POST', agent,
25+
highWaterMark: 1024*1024,
26+
},common.mustCall((res)=>{
27+
res.resume();
28+
res.on('end',common.mustCall(()=>{
29+
// Wait for socket to return to pool.
30+
setTimeout(common.mustCall(requestB),100);
31+
}));
32+
})).end('x');
33+
34+
functionrequestB(){
35+
constfreeCount=Object.values(agent.freeSockets).flat().length;
36+
assert.strictEqual(freeCount,1);
37+
38+
// Request B: HWM=10KB β€” agent must sync the reused socket's HWM.
39+
constreqB=http.request({
40+
host: 'localhost', port,method: 'POST', agent,
41+
highWaterMark: 10*1024,
42+
},common.mustCall((res)=>{
43+
res.resume();
44+
res.on('end',common.mustCall(()=>{
45+
server.close();
46+
}));
47+
}));
48+
49+
reqB.on('socket',common.mustCall((socket)=>{
50+
// Socket HWM must be synced to the request's value.
51+
assert.strictEqual(socket.writableHighWaterMark,10*1024);
52+
}));
53+
54+
reqB.end('y');
55+
}
56+
}));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
// Regression test: when _flush() hands buffered data to a socket whose
4+
// writableHighWaterMark is higher than the OutgoingMessage's kHighWaterMark,
5+
// drain must still fire. Previously, _flush() gated drain emission on
6+
// writableLength === 0, which included socket.writableLength β€” but the
7+
// socket was never backpressured (data < socket HWM), so drain never fired.
8+
//
9+
// See: https://github.com/nodejs/node/issues/64680
10+
11+
constcommon=require('../common');
12+
constassert=require('assert');
13+
consthttp=require('http');
14+
15+
// Server that delays reading to keep socket.writableLength > 0 during flush.
16+
constserver=http.createServer(common.mustCall((req,res)=>{
17+
setTimeout(()=>{
18+
req.resume();
19+
req.on('end',()=>res.end('ok'));
20+
},500);
21+
},2));
22+
23+
server.listen(0,common.mustCall(()=>{
24+
constport=server.address().port;
25+
constagent=newhttp.Agent({keepAlive: true});
26+
27+
// Request A: creates socket with HWM=2MB.
28+
http.request({
29+
host: 'localhost', port,method: 'POST', agent,
30+
highWaterMark: 2*1024*1024,
31+
},common.mustCall((res)=>{
32+
res.resume();
33+
res.on('end',common.mustCall(()=>{
34+
// Wait for socket to return to pool.
35+
setTimeout(common.mustCall(()=>{
36+
// Request B: default HWM (64KB), reuses socket (HWM=2MB).
37+
// Write 500KB: above OM HWM (64KB), below socket HWM (2MB).
38+
constreqB=http.request({
39+
host: 'localhost', port,method: 'POST', agent,
40+
},common.mustCall((res2)=>{
41+
res2.resume();
42+
res2.on('end',common.mustCall(()=>{
43+
server.close();
44+
}));
45+
}));
46+
47+
constresult=reqB.write(Buffer.alloc(500*1024));
48+
assert.strictEqual(result,false);
49+
50+
// Drain must fire β€” no deadlock.
51+
reqB.on('drain',common.mustCall(()=>{
52+
reqB.end();
53+
}));
54+
}),100);
55+
}));
56+
})).end('x');
57+
}));

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit dad9548

Browse files
trivenayaduh95
authored andcommitted
http: emit drain on socket takeover and avoid stale HWM reuse
When OutgoingMessage transitions from pre-socket buffering (Path B) to socket-connected writing (Path A), the backpressure domain changes β€” subsequent writes go directly to the socket, which enforces its own backpressure via socket.write() return values. The OM should emit drain at this transition point to signal that its buffer is clear and the caller can resume writing under the socket backpressure regime. Previously, _flush() gated drain emission on writableLength === 0 which included socket.writableLength. This conflated two independent backpressure domains: the OM pre-socket buffer and the socket kernel write queue. When the socket had a higher writableHighWaterMark than the OM (e.g. agent-reused socket from a prior request), the socket was never backpressured and never emitted drain, causing a permanent deadlock. Additionally, avoid reusing a pooled socket in http.Agent when its writableHighWaterMark differs from the request highWaterMark, so that the user backpressure threshold is respected for the common case of the built-in Agent. Signed-off-by: Naman Trivedi <trivenay@amazon.com> Fixes: #64680 Refs: #64653 Refs: #62936 PR-URL: #64991 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: GΓΌrgΓΌn DayΔ±oğlu <hey@gurgun.day>
1 parent 4674a10 commit dad9548

4 files changed

Lines changed: 127 additions & 2 deletions

File tree

β€Žlib/_http_agent.jsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
392392
constsockLen=freeLen+this.sockets[name].length;
393393

394394
// Reusing a socket from the pool.
395+
// If the caller specified a highWaterMark that differs from the pooled
396+
// socket's writableHighWaterMark, sync the socket's HWM so that
397+
// backpressure semantics match what the caller requested.
398+
if(socket&&options.highWaterMark!=null&&
399+
socket.writableHighWaterMark!==options.highWaterMark){
400+
debug('sync reused socket HWM (socket=%d, request=%d)',
401+
socket.writableHighWaterMark,options.highWaterMark);
402+
socket._writableState.highWaterMark=options.highWaterMark;
403+
}
404+
395405
if(socket){
396406
asyncResetHandle(socket);
397407
this.reuseSocket(socket,req);

β€Žlib/_http_outgoing.jsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,12 +1230,14 @@ OutgoingMessage.prototype._flush = function _flush() {
12301230

12311231
if(socket?.writable){
12321232
// There might be remaining data in this.output; write it out
1233-
this._flushOutput(socket);
1233+
constret=this._flushOutput(socket);
12341234

12351235
if(this.finished){
12361236
// This is a queue to the server or client to bring in the next this.
12371237
this._finish();
1238-
}elseif(this[kNeedDrain]&&this.writableLength===0){
1238+
}elseif(this[kNeedDrain]&&ret!==false){
1239+
// Socket accepted all data without backpressure - it won't emit
1240+
// drain, so we emit it since the OM buffer is now clear.
12391241
this[kNeedDrain]=false;
12401242
this.emit('drain');
12411243
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
// Regression test: when a pooled socket's writableHighWaterMark differs from
4+
// the new request's highWaterMark, the agent must sync the socket's HWM so
5+
// that backpressure semantics match what the caller requested.
6+
//
7+
// See: https://github.com/nodejs/node/issues/64680
8+
9+
constcommon=require('../common');
10+
constassert=require('assert');
11+
consthttp=require('http');
12+
13+
constserver=http.createServer(common.mustCall((req,res)=>{
14+
req.resume();
15+
req.on('end',()=>res.end('ok'));
16+
},2));
17+
18+
server.listen(0,common.mustCall(()=>{
19+
constport=server.address().port;
20+
constagent=newhttp.Agent({keepAlive: true});
21+
22+
// Request A: creates socket with HWM=1MB.
23+
http.request({
24+
host: 'localhost', port,method: 'POST', agent,
25+
highWaterMark: 1024*1024,
26+
},common.mustCall((res)=>{
27+
res.resume();
28+
res.on('end',common.mustCall(()=>{
29+
// Wait for socket to return to pool.
30+
setTimeout(common.mustCall(requestB),100);
31+
}));
32+
})).end('x');
33+
34+
functionrequestB(){
35+
constfreeCount=Object.values(agent.freeSockets).flat().length;
36+
assert.strictEqual(freeCount,1);
37+
38+
// Request B: HWM=10KB β€” agent must sync the reused socket's HWM.
39+
constreqB=http.request({
40+
host: 'localhost', port,method: 'POST', agent,
41+
highWaterMark: 10*1024,
42+
},common.mustCall((res)=>{
43+
res.resume();
44+
res.on('end',common.mustCall(()=>{
45+
server.close();
46+
}));
47+
}));
48+
49+
reqB.on('socket',common.mustCall((socket)=>{
50+
// Socket HWM must be synced to the request's value.
51+
assert.strictEqual(socket.writableHighWaterMark,10*1024);
52+
}));
53+
54+
reqB.end('y');
55+
}
56+
}));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
// Regression test: when _flush() hands buffered data to a socket whose
4+
// writableHighWaterMark is higher than the OutgoingMessage's kHighWaterMark,
5+
// drain must still fire. Previously, _flush() gated drain emission on
6+
// writableLength === 0, which included socket.writableLength β€” but the
7+
// socket was never backpressured (data < socket HWM), so drain never fired.
8+
//
9+
// See: https://github.com/nodejs/node/issues/64680
10+
11+
constcommon=require('../common');
12+
constassert=require('assert');
13+
consthttp=require('http');
14+
15+
// Server that delays reading to keep socket.writableLength > 0 during flush.
16+
constserver=http.createServer(common.mustCall((req,res)=>{
17+
setTimeout(()=>{
18+
req.resume();
19+
req.on('end',()=>res.end('ok'));
20+
},500);
21+
},2));
22+
23+
server.listen(0,common.mustCall(()=>{
24+
constport=server.address().port;
25+
constagent=newhttp.Agent({keepAlive: true});
26+
27+
// Request A: creates socket with HWM=2MB.
28+
http.request({
29+
host: 'localhost', port,method: 'POST', agent,
30+
highWaterMark: 2*1024*1024,
31+
},common.mustCall((res)=>{
32+
res.resume();
33+
res.on('end',common.mustCall(()=>{
34+
// Wait for socket to return to pool.
35+
setTimeout(common.mustCall(()=>{
36+
// Request B: default HWM (64KB), reuses socket (HWM=2MB).
37+
// Write 500KB: above OM HWM (64KB), below socket HWM (2MB).
38+
constreqB=http.request({
39+
host: 'localhost', port,method: 'POST', agent,
40+
},common.mustCall((res2)=>{
41+
res2.resume();
42+
res2.on('end',common.mustCall(()=>{
43+
server.close();
44+
}));
45+
}));
46+
47+
constresult=reqB.write(Buffer.alloc(500*1024));
48+
assert.strictEqual(result,false);
49+
50+
// Drain must fire β€” no deadlock.
51+
reqB.on('drain',common.mustCall(()=>{
52+
reqB.end();
53+
}));
54+
}),100);
55+
}));
56+
})).end('x');
57+
}));

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit dad9548

Browse files
trivenayaduh95
authored andcommitted
http: emit drain on socket takeover and avoid stale HWM reuse
When OutgoingMessage transitions from pre-socket buffering (Path B) to socket-connected writing (Path A), the backpressure domain changes β€” subsequent writes go directly to the socket, which enforces its own backpressure via socket.write() return values. The OM should emit drain at this transition point to signal that its buffer is clear and the caller can resume writing under the socket backpressure regime. Previously, _flush() gated drain emission on writableLength === 0 which included socket.writableLength. This conflated two independent backpressure domains: the OM pre-socket buffer and the socket kernel write queue. When the socket had a higher writableHighWaterMark than the OM (e.g. agent-reused socket from a prior request), the socket was never backpressured and never emitted drain, causing a permanent deadlock. Additionally, avoid reusing a pooled socket in http.Agent when its writableHighWaterMark differs from the request highWaterMark, so that the user backpressure threshold is respected for the common case of the built-in Agent. Signed-off-by: Naman Trivedi <trivenay@amazon.com> Fixes: #64680 Refs: #64653 Refs: #62936 PR-URL: #64991 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: GΓΌrgΓΌn DayΔ±oğlu <hey@gurgun.day>
1 parent 4674a10 commit dad9548

4 files changed

Lines changed: 127 additions & 2 deletions

File tree

β€Žlib/_http_agent.jsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
392392
constsockLen=freeLen+this.sockets[name].length;
393393

394394
// Reusing a socket from the pool.
395+
// If the caller specified a highWaterMark that differs from the pooled
396+
// socket's writableHighWaterMark, sync the socket's HWM so that
397+
// backpressure semantics match what the caller requested.
398+
if(socket&&options.highWaterMark!=null&&
399+
socket.writableHighWaterMark!==options.highWaterMark){
400+
debug('sync reused socket HWM (socket=%d, request=%d)',
401+
socket.writableHighWaterMark,options.highWaterMark);
402+
socket._writableState.highWaterMark=options.highWaterMark;
403+
}
404+
395405
if(socket){
396406
asyncResetHandle(socket);
397407
this.reuseSocket(socket,req);

β€Žlib/_http_outgoing.jsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,12 +1230,14 @@ OutgoingMessage.prototype._flush = function _flush() {
12301230

12311231
if(socket?.writable){
12321232
// There might be remaining data in this.output; write it out
1233-
this._flushOutput(socket);
1233+
constret=this._flushOutput(socket);
12341234

12351235
if(this.finished){
12361236
// This is a queue to the server or client to bring in the next this.
12371237
this._finish();
1238-
}elseif(this[kNeedDrain]&&this.writableLength===0){
1238+
}elseif(this[kNeedDrain]&&ret!==false){
1239+
// Socket accepted all data without backpressure - it won't emit
1240+
// drain, so we emit it since the OM buffer is now clear.
12391241
this[kNeedDrain]=false;
12401242
this.emit('drain');
12411243
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
// Regression test: when a pooled socket's writableHighWaterMark differs from
4+
// the new request's highWaterMark, the agent must sync the socket's HWM so
5+
// that backpressure semantics match what the caller requested.
6+
//
7+
// See: https://github.com/nodejs/node/issues/64680
8+
9+
constcommon=require('../common');
10+
constassert=require('assert');
11+
consthttp=require('http');
12+
13+
constserver=http.createServer(common.mustCall((req,res)=>{
14+
req.resume();
15+
req.on('end',()=>res.end('ok'));
16+
},2));
17+
18+
server.listen(0,common.mustCall(()=>{
19+
constport=server.address().port;
20+
constagent=newhttp.Agent({keepAlive: true});
21+
22+
// Request A: creates socket with HWM=1MB.
23+
http.request({
24+
host: 'localhost', port,method: 'POST', agent,
25+
highWaterMark: 1024*1024,
26+
},common.mustCall((res)=>{
27+
res.resume();
28+
res.on('end',common.mustCall(()=>{
29+
// Wait for socket to return to pool.
30+
setTimeout(common.mustCall(requestB),100);
31+
}));
32+
})).end('x');
33+
34+
functionrequestB(){
35+
constfreeCount=Object.values(agent.freeSockets).flat().length;
36+
assert.strictEqual(freeCount,1);
37+
38+
// Request B: HWM=10KB β€” agent must sync the reused socket's HWM.
39+
constreqB=http.request({
40+
host: 'localhost', port,method: 'POST', agent,
41+
highWaterMark: 10*1024,
42+
},common.mustCall((res)=>{
43+
res.resume();
44+
res.on('end',common.mustCall(()=>{
45+
server.close();
46+
}));
47+
}));
48+
49+
reqB.on('socket',common.mustCall((socket)=>{
50+
// Socket HWM must be synced to the request's value.
51+
assert.strictEqual(socket.writableHighWaterMark,10*1024);
52+
}));
53+
54+
reqB.end('y');
55+
}
56+
}));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
// Regression test: when _flush() hands buffered data to a socket whose
4+
// writableHighWaterMark is higher than the OutgoingMessage's kHighWaterMark,
5+
// drain must still fire. Previously, _flush() gated drain emission on
6+
// writableLength === 0, which included socket.writableLength β€” but the
7+
// socket was never backpressured (data < socket HWM), so drain never fired.
8+
//
9+
// See: https://github.com/nodejs/node/issues/64680
10+
11+
constcommon=require('../common');
12+
constassert=require('assert');
13+
consthttp=require('http');
14+
15+
// Server that delays reading to keep socket.writableLength > 0 during flush.
16+
constserver=http.createServer(common.mustCall((req,res)=>{
17+
setTimeout(()=>{
18+
req.resume();
19+
req.on('end',()=>res.end('ok'));
20+
},500);
21+
},2));
22+
23+
server.listen(0,common.mustCall(()=>{
24+
constport=server.address().port;
25+
constagent=newhttp.Agent({keepAlive: true});
26+
27+
// Request A: creates socket with HWM=2MB.
28+
http.request({
29+
host: 'localhost', port,method: 'POST', agent,
30+
highWaterMark: 2*1024*1024,
31+
},common.mustCall((res)=>{
32+
res.resume();
33+
res.on('end',common.mustCall(()=>{
34+
// Wait for socket to return to pool.
35+
setTimeout(common.mustCall(()=>{
36+
// Request B: default HWM (64KB), reuses socket (HWM=2MB).
37+
// Write 500KB: above OM HWM (64KB), below socket HWM (2MB).
38+
constreqB=http.request({
39+
host: 'localhost', port,method: 'POST', agent,
40+
},common.mustCall((res2)=>{
41+
res2.resume();
42+
res2.on('end',common.mustCall(()=>{
43+
server.close();
44+
}));
45+
}));
46+
47+
constresult=reqB.write(Buffer.alloc(500*1024));
48+
assert.strictEqual(result,false);
49+
50+
// Drain must fire β€” no deadlock.
51+
reqB.on('drain',common.mustCall(()=>{
52+
reqB.end();
53+
}));
54+
}),100);
55+
}));
56+
})).end('x');
57+
}));

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit dad9548

Browse files
trivenayaduh95
authored andcommitted
http: emit drain on socket takeover and avoid stale HWM reuse
When OutgoingMessage transitions from pre-socket buffering (Path B) to socket-connected writing (Path A), the backpressure domain changes β€” subsequent writes go directly to the socket, which enforces its own backpressure via socket.write() return values. The OM should emit drain at this transition point to signal that its buffer is clear and the caller can resume writing under the socket backpressure regime. Previously, _flush() gated drain emission on writableLength === 0 which included socket.writableLength. This conflated two independent backpressure domains: the OM pre-socket buffer and the socket kernel write queue. When the socket had a higher writableHighWaterMark than the OM (e.g. agent-reused socket from a prior request), the socket was never backpressured and never emitted drain, causing a permanent deadlock. Additionally, avoid reusing a pooled socket in http.Agent when its writableHighWaterMark differs from the request highWaterMark, so that the user backpressure threshold is respected for the common case of the built-in Agent. Signed-off-by: Naman Trivedi <trivenay@amazon.com> Fixes: #64680 Refs: #64653 Refs: #62936 PR-URL: #64991 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: GΓΌrgΓΌn DayΔ±oğlu <hey@gurgun.day>
1 parent 4674a10 commit dad9548

4 files changed

Lines changed: 127 additions & 2 deletions

File tree

β€Žlib/_http_agent.jsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
392392
constsockLen=freeLen+this.sockets[name].length;
393393

394394
// Reusing a socket from the pool.
395+
// If the caller specified a highWaterMark that differs from the pooled
396+
// socket's writableHighWaterMark, sync the socket's HWM so that
397+
// backpressure semantics match what the caller requested.
398+
if(socket&&options.highWaterMark!=null&&
399+
socket.writableHighWaterMark!==options.highWaterMark){
400+
debug('sync reused socket HWM (socket=%d, request=%d)',
401+
socket.writableHighWaterMark,options.highWaterMark);
402+
socket._writableState.highWaterMark=options.highWaterMark;
403+
}
404+
395405
if(socket){
396406
asyncResetHandle(socket);
397407
this.reuseSocket(socket,req);

β€Žlib/_http_outgoing.jsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,12 +1230,14 @@ OutgoingMessage.prototype._flush = function _flush() {
12301230

12311231
if(socket?.writable){
12321232
// There might be remaining data in this.output; write it out
1233-
this._flushOutput(socket);
1233+
constret=this._flushOutput(socket);
12341234

12351235
if(this.finished){
12361236
// This is a queue to the server or client to bring in the next this.
12371237
this._finish();
1238-
}elseif(this[kNeedDrain]&&this.writableLength===0){
1238+
}elseif(this[kNeedDrain]&&ret!==false){
1239+
// Socket accepted all data without backpressure - it won't emit
1240+
// drain, so we emit it since the OM buffer is now clear.
12391241
this[kNeedDrain]=false;
12401242
this.emit('drain');
12411243
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
// Regression test: when a pooled socket's writableHighWaterMark differs from
4+
// the new request's highWaterMark, the agent must sync the socket's HWM so
5+
// that backpressure semantics match what the caller requested.
6+
//
7+
// See: https://github.com/nodejs/node/issues/64680
8+
9+
constcommon=require('../common');
10+
constassert=require('assert');
11+
consthttp=require('http');
12+
13+
constserver=http.createServer(common.mustCall((req,res)=>{
14+
req.resume();
15+
req.on('end',()=>res.end('ok'));
16+
},2));
17+
18+
server.listen(0,common.mustCall(()=>{
19+
constport=server.address().port;
20+
constagent=newhttp.Agent({keepAlive: true});
21+
22+
// Request A: creates socket with HWM=1MB.
23+
http.request({
24+
host: 'localhost', port,method: 'POST', agent,
25+
highWaterMark: 1024*1024,
26+
},common.mustCall((res)=>{
27+
res.resume();
28+
res.on('end',common.mustCall(()=>{
29+
// Wait for socket to return to pool.
30+
setTimeout(common.mustCall(requestB),100);
31+
}));
32+
})).end('x');
33+
34+
functionrequestB(){
35+
constfreeCount=Object.values(agent.freeSockets).flat().length;
36+
assert.strictEqual(freeCount,1);
37+
38+
// Request B: HWM=10KB β€” agent must sync the reused socket's HWM.
39+
constreqB=http.request({
40+
host: 'localhost', port,method: 'POST', agent,
41+
highWaterMark: 10*1024,
42+
},common.mustCall((res)=>{
43+
res.resume();
44+
res.on('end',common.mustCall(()=>{
45+
server.close();
46+
}));
47+
}));
48+
49+
reqB.on('socket',common.mustCall((socket)=>{
50+
// Socket HWM must be synced to the request's value.
51+
assert.strictEqual(socket.writableHighWaterMark,10*1024);
52+
}));
53+
54+
reqB.end('y');
55+
}
56+
}));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
// Regression test: when _flush() hands buffered data to a socket whose
4+
// writableHighWaterMark is higher than the OutgoingMessage's kHighWaterMark,
5+
// drain must still fire. Previously, _flush() gated drain emission on
6+
// writableLength === 0, which included socket.writableLength β€” but the
7+
// socket was never backpressured (data < socket HWM), so drain never fired.
8+
//
9+
// See: https://github.com/nodejs/node/issues/64680
10+
11+
constcommon=require('../common');
12+
constassert=require('assert');
13+
consthttp=require('http');
14+
15+
// Server that delays reading to keep socket.writableLength > 0 during flush.
16+
constserver=http.createServer(common.mustCall((req,res)=>{
17+
setTimeout(()=>{
18+
req.resume();
19+
req.on('end',()=>res.end('ok'));
20+
},500);
21+
},2));
22+
23+
server.listen(0,common.mustCall(()=>{
24+
constport=server.address().port;
25+
constagent=newhttp.Agent({keepAlive: true});
26+
27+
// Request A: creates socket with HWM=2MB.
28+
http.request({
29+
host: 'localhost', port,method: 'POST', agent,
30+
highWaterMark: 2*1024*1024,
31+
},common.mustCall((res)=>{
32+
res.resume();
33+
res.on('end',common.mustCall(()=>{
34+
// Wait for socket to return to pool.
35+
setTimeout(common.mustCall(()=>{
36+
// Request B: default HWM (64KB), reuses socket (HWM=2MB).
37+
// Write 500KB: above OM HWM (64KB), below socket HWM (2MB).
38+
constreqB=http.request({
39+
host: 'localhost', port,method: 'POST', agent,
40+
},common.mustCall((res2)=>{
41+
res2.resume();
42+
res2.on('end',common.mustCall(()=>{
43+
server.close();
44+
}));
45+
}));
46+
47+
constresult=reqB.write(Buffer.alloc(500*1024));
48+
assert.strictEqual(result,false);
49+
50+
// Drain must fire β€” no deadlock.
51+
reqB.on('drain',common.mustCall(()=>{
52+
reqB.end();
53+
}));
54+
}),100);
55+
}));
56+
})).end('x');
57+
}));

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit dad9548

Browse files
trivenayaduh95
authored andcommitted
http: emit drain on socket takeover and avoid stale HWM reuse
When OutgoingMessage transitions from pre-socket buffering (Path B) to socket-connected writing (Path A), the backpressure domain changes β€” subsequent writes go directly to the socket, which enforces its own backpressure via socket.write() return values. The OM should emit drain at this transition point to signal that its buffer is clear and the caller can resume writing under the socket backpressure regime. Previously, _flush() gated drain emission on writableLength === 0 which included socket.writableLength. This conflated two independent backpressure domains: the OM pre-socket buffer and the socket kernel write queue. When the socket had a higher writableHighWaterMark than the OM (e.g. agent-reused socket from a prior request), the socket was never backpressured and never emitted drain, causing a permanent deadlock. Additionally, avoid reusing a pooled socket in http.Agent when its writableHighWaterMark differs from the request highWaterMark, so that the user backpressure threshold is respected for the common case of the built-in Agent. Signed-off-by: Naman Trivedi <trivenay@amazon.com> Fixes: #64680 Refs: #64653 Refs: #62936 PR-URL: #64991 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: GΓΌrgΓΌn DayΔ±oğlu <hey@gurgun.day>
1 parent 4674a10 commit dad9548

4 files changed

Lines changed: 127 additions & 2 deletions

File tree

β€Žlib/_http_agent.jsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
392392
constsockLen=freeLen+this.sockets[name].length;
393393

394394
// Reusing a socket from the pool.
395+
// If the caller specified a highWaterMark that differs from the pooled
396+
// socket's writableHighWaterMark, sync the socket's HWM so that
397+
// backpressure semantics match what the caller requested.
398+
if(socket&&options.highWaterMark!=null&&
399+
socket.writableHighWaterMark!==options.highWaterMark){
400+
debug('sync reused socket HWM (socket=%d, request=%d)',
401+
socket.writableHighWaterMark,options.highWaterMark);
402+
socket._writableState.highWaterMark=options.highWaterMark;
403+
}
404+
395405
if(socket){
396406
asyncResetHandle(socket);
397407
this.reuseSocket(socket,req);

β€Žlib/_http_outgoing.jsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,12 +1230,14 @@ OutgoingMessage.prototype._flush = function _flush() {
12301230

12311231
if(socket?.writable){
12321232
// There might be remaining data in this.output; write it out
1233-
this._flushOutput(socket);
1233+
constret=this._flushOutput(socket);
12341234

12351235
if(this.finished){
12361236
// This is a queue to the server or client to bring in the next this.
12371237
this._finish();
1238-
}elseif(this[kNeedDrain]&&this.writableLength===0){
1238+
}elseif(this[kNeedDrain]&&ret!==false){
1239+
// Socket accepted all data without backpressure - it won't emit
1240+
// drain, so we emit it since the OM buffer is now clear.
12391241
this[kNeedDrain]=false;
12401242
this.emit('drain');
12411243
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
// Regression test: when a pooled socket's writableHighWaterMark differs from
4+
// the new request's highWaterMark, the agent must sync the socket's HWM so
5+
// that backpressure semantics match what the caller requested.
6+
//
7+
// See: https://github.com/nodejs/node/issues/64680
8+
9+
constcommon=require('../common');
10+
constassert=require('assert');
11+
consthttp=require('http');
12+
13+
constserver=http.createServer(common.mustCall((req,res)=>{
14+
req.resume();
15+
req.on('end',()=>res.end('ok'));
16+
},2));
17+
18+
server.listen(0,common.mustCall(()=>{
19+
constport=server.address().port;
20+
constagent=newhttp.Agent({keepAlive: true});
21+
22+
// Request A: creates socket with HWM=1MB.
23+
http.request({
24+
host: 'localhost', port,method: 'POST', agent,
25+
highWaterMark: 1024*1024,
26+
},common.mustCall((res)=>{
27+
res.resume();
28+
res.on('end',common.mustCall(()=>{
29+
// Wait for socket to return to pool.
30+
setTimeout(common.mustCall(requestB),100);
31+
}));
32+
})).end('x');
33+
34+
functionrequestB(){
35+
constfreeCount=Object.values(agent.freeSockets).flat().length;
36+
assert.strictEqual(freeCount,1);
37+
38+
// Request B: HWM=10KB β€” agent must sync the reused socket's HWM.
39+
constreqB=http.request({
40+
host: 'localhost', port,method: 'POST', agent,
41+
highWaterMark: 10*1024,
42+
},common.mustCall((res)=>{
43+
res.resume();
44+
res.on('end',common.mustCall(()=>{
45+
server.close();
46+
}));
47+
}));
48+
49+
reqB.on('socket',common.mustCall((socket)=>{
50+
// Socket HWM must be synced to the request's value.
51+
assert.strictEqual(socket.writableHighWaterMark,10*1024);
52+
}));
53+
54+
reqB.end('y');
55+
}
56+
}));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
// Regression test: when _flush() hands buffered data to a socket whose
4+
// writableHighWaterMark is higher than the OutgoingMessage's kHighWaterMark,
5+
// drain must still fire. Previously, _flush() gated drain emission on
6+
// writableLength === 0, which included socket.writableLength β€” but the
7+
// socket was never backpressured (data < socket HWM), so drain never fired.
8+
//
9+
// See: https://github.com/nodejs/node/issues/64680
10+
11+
constcommon=require('../common');
12+
constassert=require('assert');
13+
consthttp=require('http');
14+
15+
// Server that delays reading to keep socket.writableLength > 0 during flush.
16+
constserver=http.createServer(common.mustCall((req,res)=>{
17+
setTimeout(()=>{
18+
req.resume();
19+
req.on('end',()=>res.end('ok'));
20+
},500);
21+
},2));
22+
23+
server.listen(0,common.mustCall(()=>{
24+
constport=server.address().port;
25+
constagent=newhttp.Agent({keepAlive: true});
26+
27+
// Request A: creates socket with HWM=2MB.
28+
http.request({
29+
host: 'localhost', port,method: 'POST', agent,
30+
highWaterMark: 2*1024*1024,
31+
},common.mustCall((res)=>{
32+
res.resume();
33+
res.on('end',common.mustCall(()=>{
34+
// Wait for socket to return to pool.
35+
setTimeout(common.mustCall(()=>{
36+
// Request B: default HWM (64KB), reuses socket (HWM=2MB).
37+
// Write 500KB: above OM HWM (64KB), below socket HWM (2MB).
38+
constreqB=http.request({
39+
host: 'localhost', port,method: 'POST', agent,
40+
},common.mustCall((res2)=>{
41+
res2.resume();
42+
res2.on('end',common.mustCall(()=>{
43+
server.close();
44+
}));
45+
}));
46+
47+
constresult=reqB.write(Buffer.alloc(500*1024));
48+
assert.strictEqual(result,false);
49+
50+
// Drain must fire β€” no deadlock.
51+
reqB.on('drain',common.mustCall(()=>{
52+
reqB.end();
53+
}));
54+
}),100);
55+
}));
56+
})).end('x');
57+
}));

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit dad9548

Browse files
trivenayaduh95
authored andcommitted
http: emit drain on socket takeover and avoid stale HWM reuse
When OutgoingMessage transitions from pre-socket buffering (Path B) to socket-connected writing (Path A), the backpressure domain changes β€” subsequent writes go directly to the socket, which enforces its own backpressure via socket.write() return values. The OM should emit drain at this transition point to signal that its buffer is clear and the caller can resume writing under the socket backpressure regime. Previously, _flush() gated drain emission on writableLength === 0 which included socket.writableLength. This conflated two independent backpressure domains: the OM pre-socket buffer and the socket kernel write queue. When the socket had a higher writableHighWaterMark than the OM (e.g. agent-reused socket from a prior request), the socket was never backpressured and never emitted drain, causing a permanent deadlock. Additionally, avoid reusing a pooled socket in http.Agent when its writableHighWaterMark differs from the request highWaterMark, so that the user backpressure threshold is respected for the common case of the built-in Agent. Signed-off-by: Naman Trivedi <trivenay@amazon.com> Fixes: #64680 Refs: #64653 Refs: #62936 PR-URL: #64991 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: GΓΌrgΓΌn DayΔ±oğlu <hey@gurgun.day>
1 parent 4674a10 commit dad9548

4 files changed

Lines changed: 127 additions & 2 deletions

File tree

β€Žlib/_http_agent.jsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
392392
constsockLen=freeLen+this.sockets[name].length;
393393

394394
// Reusing a socket from the pool.
395+
// If the caller specified a highWaterMark that differs from the pooled
396+
// socket's writableHighWaterMark, sync the socket's HWM so that
397+
// backpressure semantics match what the caller requested.
398+
if(socket&&options.highWaterMark!=null&&
399+
socket.writableHighWaterMark!==options.highWaterMark){
400+
debug('sync reused socket HWM (socket=%d, request=%d)',
401+
socket.writableHighWaterMark,options.highWaterMark);
402+
socket._writableState.highWaterMark=options.highWaterMark;
403+
}
404+
395405
if(socket){
396406
asyncResetHandle(socket);
397407
this.reuseSocket(socket,req);

β€Žlib/_http_outgoing.jsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,12 +1230,14 @@ OutgoingMessage.prototype._flush = function _flush() {
12301230

12311231
if(socket?.writable){
12321232
// There might be remaining data in this.output; write it out
1233-
this._flushOutput(socket);
1233+
constret=this._flushOutput(socket);
12341234

12351235
if(this.finished){
12361236
// This is a queue to the server or client to bring in the next this.
12371237
this._finish();
1238-
}elseif(this[kNeedDrain]&&this.writableLength===0){
1238+
}elseif(this[kNeedDrain]&&ret!==false){
1239+
// Socket accepted all data without backpressure - it won't emit
1240+
// drain, so we emit it since the OM buffer is now clear.
12391241
this[kNeedDrain]=false;
12401242
this.emit('drain');
12411243
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
// Regression test: when a pooled socket's writableHighWaterMark differs from
4+
// the new request's highWaterMark, the agent must sync the socket's HWM so
5+
// that backpressure semantics match what the caller requested.
6+
//
7+
// See: https://github.com/nodejs/node/issues/64680
8+
9+
constcommon=require('../common');
10+
constassert=require('assert');
11+
consthttp=require('http');
12+
13+
constserver=http.createServer(common.mustCall((req,res)=>{
14+
req.resume();
15+
req.on('end',()=>res.end('ok'));
16+
},2));
17+
18+
server.listen(0,common.mustCall(()=>{
19+
constport=server.address().port;
20+
constagent=newhttp.Agent({keepAlive: true});
21+
22+
// Request A: creates socket with HWM=1MB.
23+
http.request({
24+
host: 'localhost', port,method: 'POST', agent,
25+
highWaterMark: 1024*1024,
26+
},common.mustCall((res)=>{
27+
res.resume();
28+
res.on('end',common.mustCall(()=>{
29+
// Wait for socket to return to pool.
30+
setTimeout(common.mustCall(requestB),100);
31+
}));
32+
})).end('x');
33+
34+
functionrequestB(){
35+
constfreeCount=Object.values(agent.freeSockets).flat().length;
36+
assert.strictEqual(freeCount,1);
37+
38+
// Request B: HWM=10KB β€” agent must sync the reused socket's HWM.
39+
constreqB=http.request({
40+
host: 'localhost', port,method: 'POST', agent,
41+
highWaterMark: 10*1024,
42+
},common.mustCall((res)=>{
43+
res.resume();
44+
res.on('end',common.mustCall(()=>{
45+
server.close();
46+
}));
47+
}));
48+
49+
reqB.on('socket',common.mustCall((socket)=>{
50+
// Socket HWM must be synced to the request's value.
51+
assert.strictEqual(socket.writableHighWaterMark,10*1024);
52+
}));
53+
54+
reqB.end('y');
55+
}
56+
}));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
// Regression test: when _flush() hands buffered data to a socket whose
4+
// writableHighWaterMark is higher than the OutgoingMessage's kHighWaterMark,
5+
// drain must still fire. Previously, _flush() gated drain emission on
6+
// writableLength === 0, which included socket.writableLength β€” but the
7+
// socket was never backpressured (data < socket HWM), so drain never fired.
8+
//
9+
// See: https://github.com/nodejs/node/issues/64680
10+
11+
constcommon=require('../common');
12+
constassert=require('assert');
13+
consthttp=require('http');
14+
15+
// Server that delays reading to keep socket.writableLength > 0 during flush.
16+
constserver=http.createServer(common.mustCall((req,res)=>{
17+
setTimeout(()=>{
18+
req.resume();
19+
req.on('end',()=>res.end('ok'));
20+
},500);
21+
},2));
22+
23+
server.listen(0,common.mustCall(()=>{
24+
constport=server.address().port;
25+
constagent=newhttp.Agent({keepAlive: true});
26+
27+
// Request A: creates socket with HWM=2MB.
28+
http.request({
29+
host: 'localhost', port,method: 'POST', agent,
30+
highWaterMark: 2*1024*1024,
31+
},common.mustCall((res)=>{
32+
res.resume();
33+
res.on('end',common.mustCall(()=>{
34+
// Wait for socket to return to pool.
35+
setTimeout(common.mustCall(()=>{
36+
// Request B: default HWM (64KB), reuses socket (HWM=2MB).
37+
// Write 500KB: above OM HWM (64KB), below socket HWM (2MB).
38+
constreqB=http.request({
39+
host: 'localhost', port,method: 'POST', agent,
40+
},common.mustCall((res2)=>{
41+
res2.resume();
42+
res2.on('end',common.mustCall(()=>{
43+
server.close();
44+
}));
45+
}));
46+
47+
constresult=reqB.write(Buffer.alloc(500*1024));
48+
assert.strictEqual(result,false);
49+
50+
// Drain must fire β€” no deadlock.
51+
reqB.on('drain',common.mustCall(()=>{
52+
reqB.end();
53+
}));
54+
}),100);
55+
}));
56+
})).end('x');
57+
}));

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit dad9548

Browse files
trivenayaduh95
authored andcommitted
http: emit drain on socket takeover and avoid stale HWM reuse
When OutgoingMessage transitions from pre-socket buffering (Path B) to socket-connected writing (Path A), the backpressure domain changes β€” subsequent writes go directly to the socket, which enforces its own backpressure via socket.write() return values. The OM should emit drain at this transition point to signal that its buffer is clear and the caller can resume writing under the socket backpressure regime. Previously, _flush() gated drain emission on writableLength === 0 which included socket.writableLength. This conflated two independent backpressure domains: the OM pre-socket buffer and the socket kernel write queue. When the socket had a higher writableHighWaterMark than the OM (e.g. agent-reused socket from a prior request), the socket was never backpressured and never emitted drain, causing a permanent deadlock. Additionally, avoid reusing a pooled socket in http.Agent when its writableHighWaterMark differs from the request highWaterMark, so that the user backpressure threshold is respected for the common case of the built-in Agent. Signed-off-by: Naman Trivedi <trivenay@amazon.com> Fixes: #64680 Refs: #64653 Refs: #62936 PR-URL: #64991 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: GΓΌrgΓΌn DayΔ±oğlu <hey@gurgun.day>
1 parent 4674a10 commit dad9548

4 files changed

Lines changed: 127 additions & 2 deletions

File tree

β€Žlib/_http_agent.jsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
392392
constsockLen=freeLen+this.sockets[name].length;
393393

394394
// Reusing a socket from the pool.
395+
// If the caller specified a highWaterMark that differs from the pooled
396+
// socket's writableHighWaterMark, sync the socket's HWM so that
397+
// backpressure semantics match what the caller requested.
398+
if(socket&&options.highWaterMark!=null&&
399+
socket.writableHighWaterMark!==options.highWaterMark){
400+
debug('sync reused socket HWM (socket=%d, request=%d)',
401+
socket.writableHighWaterMark,options.highWaterMark);
402+
socket._writableState.highWaterMark=options.highWaterMark;
403+
}
404+
395405
if(socket){
396406
asyncResetHandle(socket);
397407
this.reuseSocket(socket,req);

β€Žlib/_http_outgoing.jsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,12 +1230,14 @@ OutgoingMessage.prototype._flush = function _flush() {
12301230

12311231
if(socket?.writable){
12321232
// There might be remaining data in this.output; write it out
1233-
this._flushOutput(socket);
1233+
constret=this._flushOutput(socket);
12341234

12351235
if(this.finished){
12361236
// This is a queue to the server or client to bring in the next this.
12371237
this._finish();
1238-
}elseif(this[kNeedDrain]&&this.writableLength===0){
1238+
}elseif(this[kNeedDrain]&&ret!==false){
1239+
// Socket accepted all data without backpressure - it won't emit
1240+
// drain, so we emit it since the OM buffer is now clear.
12391241
this[kNeedDrain]=false;
12401242
this.emit('drain');
12411243
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
// Regression test: when a pooled socket's writableHighWaterMark differs from
4+
// the new request's highWaterMark, the agent must sync the socket's HWM so
5+
// that backpressure semantics match what the caller requested.
6+
//
7+
// See: https://github.com/nodejs/node/issues/64680
8+
9+
constcommon=require('../common');
10+
constassert=require('assert');
11+
consthttp=require('http');
12+
13+
constserver=http.createServer(common.mustCall((req,res)=>{
14+
req.resume();
15+
req.on('end',()=>res.end('ok'));
16+
},2));
17+
18+
server.listen(0,common.mustCall(()=>{
19+
constport=server.address().port;
20+
constagent=newhttp.Agent({keepAlive: true});
21+
22+
// Request A: creates socket with HWM=1MB.
23+
http.request({
24+
host: 'localhost', port,method: 'POST', agent,
25+
highWaterMark: 1024*1024,
26+
},common.mustCall((res)=>{
27+
res.resume();
28+
res.on('end',common.mustCall(()=>{
29+
// Wait for socket to return to pool.
30+
setTimeout(common.mustCall(requestB),100);
31+
}));
32+
})).end('x');
33+
34+
functionrequestB(){
35+
constfreeCount=Object.values(agent.freeSockets).flat().length;
36+
assert.strictEqual(freeCount,1);
37+
38+
// Request B: HWM=10KB β€” agent must sync the reused socket's HWM.
39+
constreqB=http.request({
40+
host: 'localhost', port,method: 'POST', agent,
41+
highWaterMark: 10*1024,
42+
},common.mustCall((res)=>{
43+
res.resume();
44+
res.on('end',common.mustCall(()=>{
45+
server.close();
46+
}));
47+
}));
48+
49+
reqB.on('socket',common.mustCall((socket)=>{
50+
// Socket HWM must be synced to the request's value.
51+
assert.strictEqual(socket.writableHighWaterMark,10*1024);
52+
}));
53+
54+
reqB.end('y');
55+
}
56+
}));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
// Regression test: when _flush() hands buffered data to a socket whose
4+
// writableHighWaterMark is higher than the OutgoingMessage's kHighWaterMark,
5+
// drain must still fire. Previously, _flush() gated drain emission on
6+
// writableLength === 0, which included socket.writableLength β€” but the
7+
// socket was never backpressured (data < socket HWM), so drain never fired.
8+
//
9+
// See: https://github.com/nodejs/node/issues/64680
10+
11+
constcommon=require('../common');
12+
constassert=require('assert');
13+
consthttp=require('http');
14+
15+
// Server that delays reading to keep socket.writableLength > 0 during flush.
16+
constserver=http.createServer(common.mustCall((req,res)=>{
17+
setTimeout(()=>{
18+
req.resume();
19+
req.on('end',()=>res.end('ok'));
20+
},500);
21+
},2));
22+
23+
server.listen(0,common.mustCall(()=>{
24+
constport=server.address().port;
25+
constagent=newhttp.Agent({keepAlive: true});
26+
27+
// Request A: creates socket with HWM=2MB.
28+
http.request({
29+
host: 'localhost', port,method: 'POST', agent,
30+
highWaterMark: 2*1024*1024,
31+
},common.mustCall((res)=>{
32+
res.resume();
33+
res.on('end',common.mustCall(()=>{
34+
// Wait for socket to return to pool.
35+
setTimeout(common.mustCall(()=>{
36+
// Request B: default HWM (64KB), reuses socket (HWM=2MB).
37+
// Write 500KB: above OM HWM (64KB), below socket HWM (2MB).
38+
constreqB=http.request({
39+
host: 'localhost', port,method: 'POST', agent,
40+
},common.mustCall((res2)=>{
41+
res2.resume();
42+
res2.on('end',common.mustCall(()=>{
43+
server.close();
44+
}));
45+
}));
46+
47+
constresult=reqB.write(Buffer.alloc(500*1024));
48+
assert.strictEqual(result,false);
49+
50+
// Drain must fire β€” no deadlock.
51+
reqB.on('drain',common.mustCall(()=>{
52+
reqB.end();
53+
}));
54+
}),100);
55+
}));
56+
})).end('x');
57+
}));

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit dad9548

Browse files
trivenayaduh95
authored andcommitted
http: emit drain on socket takeover and avoid stale HWM reuse
When OutgoingMessage transitions from pre-socket buffering (Path B) to socket-connected writing (Path A), the backpressure domain changes β€” subsequent writes go directly to the socket, which enforces its own backpressure via socket.write() return values. The OM should emit drain at this transition point to signal that its buffer is clear and the caller can resume writing under the socket backpressure regime. Previously, _flush() gated drain emission on writableLength === 0 which included socket.writableLength. This conflated two independent backpressure domains: the OM pre-socket buffer and the socket kernel write queue. When the socket had a higher writableHighWaterMark than the OM (e.g. agent-reused socket from a prior request), the socket was never backpressured and never emitted drain, causing a permanent deadlock. Additionally, avoid reusing a pooled socket in http.Agent when its writableHighWaterMark differs from the request highWaterMark, so that the user backpressure threshold is respected for the common case of the built-in Agent. Signed-off-by: Naman Trivedi <trivenay@amazon.com> Fixes: #64680 Refs: #64653 Refs: #62936 PR-URL: #64991 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: GΓΌrgΓΌn DayΔ±oğlu <hey@gurgun.day>
1 parent 4674a10 commit dad9548

4 files changed

Lines changed: 127 additions & 2 deletions

File tree

β€Žlib/_http_agent.jsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
392392
constsockLen=freeLen+this.sockets[name].length;
393393

394394
// Reusing a socket from the pool.
395+
// If the caller specified a highWaterMark that differs from the pooled
396+
// socket's writableHighWaterMark, sync the socket's HWM so that
397+
// backpressure semantics match what the caller requested.
398+
if(socket&&options.highWaterMark!=null&&
399+
socket.writableHighWaterMark!==options.highWaterMark){
400+
debug('sync reused socket HWM (socket=%d, request=%d)',
401+
socket.writableHighWaterMark,options.highWaterMark);
402+
socket._writableState.highWaterMark=options.highWaterMark;
403+
}
404+
395405
if(socket){
396406
asyncResetHandle(socket);
397407
this.reuseSocket(socket,req);

β€Žlib/_http_outgoing.jsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,12 +1230,14 @@ OutgoingMessage.prototype._flush = function _flush() {
12301230

12311231
if(socket?.writable){
12321232
// There might be remaining data in this.output; write it out
1233-
this._flushOutput(socket);
1233+
constret=this._flushOutput(socket);
12341234

12351235
if(this.finished){
12361236
// This is a queue to the server or client to bring in the next this.
12371237
this._finish();
1238-
}elseif(this[kNeedDrain]&&this.writableLength===0){
1238+
}elseif(this[kNeedDrain]&&ret!==false){
1239+
// Socket accepted all data without backpressure - it won't emit
1240+
// drain, so we emit it since the OM buffer is now clear.
12391241
this[kNeedDrain]=false;
12401242
this.emit('drain');
12411243
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
// Regression test: when a pooled socket's writableHighWaterMark differs from
4+
// the new request's highWaterMark, the agent must sync the socket's HWM so
5+
// that backpressure semantics match what the caller requested.
6+
//
7+
// See: https://github.com/nodejs/node/issues/64680
8+
9+
constcommon=require('../common');
10+
constassert=require('assert');
11+
consthttp=require('http');
12+
13+
constserver=http.createServer(common.mustCall((req,res)=>{
14+
req.resume();
15+
req.on('end',()=>res.end('ok'));
16+
},2));
17+
18+
server.listen(0,common.mustCall(()=>{
19+
constport=server.address().port;
20+
constagent=newhttp.Agent({keepAlive: true});
21+
22+
// Request A: creates socket with HWM=1MB.
23+
http.request({
24+
host: 'localhost', port,method: 'POST', agent,
25+
highWaterMark: 1024*1024,
26+
},common.mustCall((res)=>{
27+
res.resume();
28+
res.on('end',common.mustCall(()=>{
29+
// Wait for socket to return to pool.
30+
setTimeout(common.mustCall(requestB),100);
31+
}));
32+
})).end('x');
33+
34+
functionrequestB(){
35+
constfreeCount=Object.values(agent.freeSockets).flat().length;
36+
assert.strictEqual(freeCount,1);
37+
38+
// Request B: HWM=10KB β€” agent must sync the reused socket's HWM.
39+
constreqB=http.request({
40+
host: 'localhost', port,method: 'POST', agent,
41+
highWaterMark: 10*1024,
42+
},common.mustCall((res)=>{
43+
res.resume();
44+
res.on('end',common.mustCall(()=>{
45+
server.close();
46+
}));
47+
}));
48+
49+
reqB.on('socket',common.mustCall((socket)=>{
50+
// Socket HWM must be synced to the request's value.
51+
assert.strictEqual(socket.writableHighWaterMark,10*1024);
52+
}));
53+
54+
reqB.end('y');
55+
}
56+
}));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
// Regression test: when _flush() hands buffered data to a socket whose
4+
// writableHighWaterMark is higher than the OutgoingMessage's kHighWaterMark,
5+
// drain must still fire. Previously, _flush() gated drain emission on
6+
// writableLength === 0, which included socket.writableLength β€” but the
7+
// socket was never backpressured (data < socket HWM), so drain never fired.
8+
//
9+
// See: https://github.com/nodejs/node/issues/64680
10+
11+
constcommon=require('../common');
12+
constassert=require('assert');
13+
consthttp=require('http');
14+
15+
// Server that delays reading to keep socket.writableLength > 0 during flush.
16+
constserver=http.createServer(common.mustCall((req,res)=>{
17+
setTimeout(()=>{
18+
req.resume();
19+
req.on('end',()=>res.end('ok'));
20+
},500);
21+
},2));
22+
23+
server.listen(0,common.mustCall(()=>{
24+
constport=server.address().port;
25+
constagent=newhttp.Agent({keepAlive: true});
26+
27+
// Request A: creates socket with HWM=2MB.
28+
http.request({
29+
host: 'localhost', port,method: 'POST', agent,
30+
highWaterMark: 2*1024*1024,
31+
},common.mustCall((res)=>{
32+
res.resume();
33+
res.on('end',common.mustCall(()=>{
34+
// Wait for socket to return to pool.
35+
setTimeout(common.mustCall(()=>{
36+
// Request B: default HWM (64KB), reuses socket (HWM=2MB).
37+
// Write 500KB: above OM HWM (64KB), below socket HWM (2MB).
38+
constreqB=http.request({
39+
host: 'localhost', port,method: 'POST', agent,
40+
},common.mustCall((res2)=>{
41+
res2.resume();
42+
res2.on('end',common.mustCall(()=>{
43+
server.close();
44+
}));
45+
}));
46+
47+
constresult=reqB.write(Buffer.alloc(500*1024));
48+
assert.strictEqual(result,false);
49+
50+
// Drain must fire β€” no deadlock.
51+
reqB.on('drain',common.mustCall(()=>{
52+
reqB.end();
53+
}));
54+
}),100);
55+
}));
56+
})).end('x');
57+
}));

0 commit comments

Comments
Β (0)