Commit 8a00872

Browse files
pimterryaduh95
authored andcommitted
quic: fix stop sending behaviour & callback
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #64710 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e59346b commit 8a00872

15 files changed

Lines changed: 193 additions & 164 deletions

‎doc/api/quic.md‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,8 +2013,7 @@ added: v23.8.0
20132013

20142014
The callback to invoke when the peer aborts a direction of the stream by
20152015
sending a `RESET_STREAM` frame (the peer abandons their writable side, so
2016-
no further data will arrive on our readable side) or a `STOP_SENDING`
2017-
frame (the peer asks us to stop writing on our writable side).
2016+
no further data will arrive on our readable side).
20182017

20192018
The callback receives a Node.js error whose `errorCode` (`bigint`)
20202019
property carries the application error code from the wire frame.
@@ -2025,6 +2024,21 @@ continue using the still-active direction on a bidirectional stream),
20252024
abort the other direction with [`writer.fail()`][], or tear down the
20262025
whole stream with [`stream.destroy()`][]. Read/write.
20272026

2027+
### `stream.onstopsending`
2028+
2029+
<!-- YAML
2030+
added: REPLACEME
2031+
-->
2032+
2033+
* Type: {quic.OnStreamErrorCallback}
2034+
2035+
The callback to invoke when the peer aborts a direction of the stream by
2036+
sending a `STOP_SENDING` frame (the peer asks us to stop writing on our
2037+
writable side).
2038+
2039+
The callback receives a Node.js error whose `errorCode` (`bigint`)
2040+
property carries the application error code from the wire frame. Read/write.
2041+
20282042
### `stream.headers`
20292043

20302044
<!-- YAML
@@ -3569,8 +3583,8 @@ functions. If a callback throws synchronously or returns a promise that
35693583
rejects, the error is caught and the owning session or stream is destroyed
35703584
with that error:
35713585

3572-
* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
3573-
`oninfo`, `onwanttrailers`): the stream is destroyed.
3586+
* Stream callbacks (`onblocked`, `onreset`, `onstopsending`, `onheaders`,
3587+
`ontrailers`, `oninfo`, `onwanttrailers`): the stream is destroyed.
35743588
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
35753589
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
35763590
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
@@ -4439,10 +4453,9 @@ added: REPLACEME
44394453
* `session` {quic.QuicSession}
44404454
* `error` {any} The QUIC error associated with the reset.
44414455
4442-
Published when a stream receives a STOP\_SENDING or RESET\_STREAM frame
4443-
from the peer, indicating the peer has aborted the stream. This is a
4444-
key signal for diagnosing application-level issues such as cancelled
4445-
requests.
4456+
Published when a stream receives a RESET\_STREAM frame from the peer,
4457+
indicating the peer has aborted its sending direction. This is a key signal
4458+
for diagnosing application-level issues such as cancelled requests.
44464459
44474460
### Channel: `quic.stream.blocked`
44484461

‎lib/internal/quic/quic.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
DataViewPrototypeGetByteLength,
1313
ErrorCaptureStackTrace,
1414
FunctionPrototypeBind,
15+
FunctionPrototypeCall,
1516
Number,
1617
ObjectDefineProperties,
1718
ObjectKeys,
@@ -210,6 +211,7 @@ const {
210211
kSendHeaders,
211212
kSessionApplication,
212213
kSessionTicket,
214+
kStopSending,
213215
kTrailers,
214216
kVersionNegotiation,
215217
kInspect,
@@ -991,6 +993,14 @@ setCallbacks({
991993
this[kOwner][kReset](error);
992994
},
993995

996+
onStreamStopSending(error){
997+
if(error!==undefined){
998+
error=convertQuicError(error);
999+
}
1000+
debug('stream stop sending callback',this[kOwner],error);
1001+
this[kOwner][kStopSending](error);
1002+
},
1003+
9941004
onStreamHeaders(headers,kind){
9951005
// Called when the stream C++ handle has received a full block of headers.
9961006
debug(`stream ${this[kOwner].id} headers callback`,headers,kind);
@@ -1576,6 +1586,7 @@ class QuicStream {
15761586
onerror: undefined,
15771587
onblocked: undefined,
15781588
onreset: undefined,
1589+
onstopsending: undefined,
15791590
onheaders: undefined,
15801591
ontrailers: undefined,
15811592
oninfo: undefined,
@@ -1781,6 +1792,25 @@ class QuicStream {
17811792
}
17821793
}
17831794

1795+
/** @type {OnStreamErrorCallback} */
1796+
getonstopsending(){
1797+
assertIsQuicStream(this);
1798+
returnthis.#inner.onstopsending;
1799+
}
1800+
1801+
setonstopsending(fn){
1802+
assertIsQuicStream(this);
1803+
constinner=this.#inner;
1804+
if(fn===undefined){
1805+
inner.onstopsending=undefined;
1806+
inner.state.wantsStopSending=false;
1807+
}else{
1808+
validateFunction(fn,'onstopsending');
1809+
inner.onstopsending=FunctionPrototypeBind(fn,this);
1810+
inner.state.wantsStopSending=true;
1811+
}
1812+
}
1813+
17841814
/** @type {OnHeadersCallback} */
17851815
getonheaders(){
17861816
assertIsQuicStream(this);
@@ -2143,6 +2173,19 @@ class QuicStream {
21432173
}
21442174
};
21452175

2176+
constonStopSending=stream[kStopSending];
2177+
stream[kStopSending]=(reason)=>{
2178+
if(!closed&&!errored){
2179+
errored=true;
2180+
error=reason;
2181+
if(drainWakeup!=null){
2182+
drainWakeup.reject(error);
2183+
drainWakeup=null;
2184+
}
2185+
}
2186+
FunctionPrototypeCall(onStopSending,stream,reason);
2187+
};
2188+
21462189
// A note on backpressure handling: per the stream/iter spec, the default
21472190
// backpressure policy for writers is strict, meaning that if the stream
21482191
// signals backpressure additional writes are rejected until the buffer has
@@ -2543,6 +2586,7 @@ class QuicStream {
25432586
inner.pendingClose.resolve=undefined;
25442587
inner.onblocked=undefined;
25452588
inner.onreset=undefined;
2589+
inner.onstopsending=undefined;
25462590
inner.onheaders=undefined;
25472591
inner.onerror=undefined;
25482592
inner.ontrailers=undefined;
@@ -2596,6 +2640,12 @@ class QuicStream {
25962640
safeCallbackInvoke(inner.onreset,this,error);
25972641
}
25982642

2643+
[kStopSending](error){
2644+
constinner=this.#inner;
2645+
assert(inner.onstopsending,'Unexpected stop sending event');
2646+
safeCallbackInvoke(inner.onstopsending,this,error);
2647+
}
2648+
25992649
[kHeaders](headers,kind){
26002650
constblock=parseHeaderPairs(headers);
26012651
constkindName=kHeadersKindName[kind]??kind;

‎lib/internal/quic/state.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
IDX_STATE_STREAM_WANTS_BLOCK,
102102
IDX_STATE_STREAM_WANTS_HEADERS,
103103
IDX_STATE_STREAM_WANTS_RESET,
104+
IDX_STATE_STREAM_WANTS_STOP_SENDING,
104105
IDX_STATE_STREAM_WANTS_TRAILERS,
105106
IDX_STATE_STREAM_RECEIVED_EARLY_DATA,
106107
IDX_STATE_STREAM_WRITE_DESIRED_SIZE,
@@ -142,6 +143,7 @@ assert(IDX_STATE_STREAM_HAS_READER !== undefined);
142143
assert(IDX_STATE_STREAM_WANTS_BLOCK!==undefined);
143144
assert(IDX_STATE_STREAM_WANTS_HEADERS!==undefined);
144145
assert(IDX_STATE_STREAM_WANTS_RESET!==undefined);
146+
assert(IDX_STATE_STREAM_WANTS_STOP_SENDING!==undefined);
145147
assert(IDX_STATE_STREAM_WANTS_TRAILERS!==undefined);
146148
assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE!==undefined);
147149
assert(IDX_STATE_STREAM_RESET_CODE!==undefined);
@@ -826,6 +828,24 @@ class QuicStreamState {
826828
DataViewPrototypeSetUint8(handle,this.#offset +IDX_STATE_STREAM_WANTS_RESET,val ? 1 : 0);
827829
}
828830

831+
/** @type {boolean} */
832+
getwantsStopSending(){
833+
consthandle=this.#handle;
834+
if(handle===undefined)returnundefined;
835+
returnDataViewPrototypeGetUint8(
836+
handle,this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING)!==0;
837+
}
838+
839+
/** @type {boolean} */
840+
setwantsStopSending(val){
841+
consthandle=this.#handle;
842+
if(handle===undefined)return;
843+
DataViewPrototypeSetUint8(
844+
handle,
845+
this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING,
846+
val ? 1 : 0);
847+
}
848+
829849
/** @type {boolean} */
830850
getwantsTrailers(){
831851
consthandle=this.#handle;
@@ -903,6 +923,7 @@ class QuicStreamState {
903923
hasReader,
904924
wantsBlock,
905925
wantsReset,
926+
wantsStopSending,
906927
wantsHeaders,
907928
wantsTrailers,
908929
early,
@@ -923,6 +944,7 @@ class QuicStreamState {
923944
hasReader,
924945
wantsBlock,
925946
wantsReset,
947+
wantsStopSending,
926948
wantsHeaders,
927949
wantsTrailers,
928950
early,
@@ -960,6 +982,7 @@ class QuicStreamState {
960982
hasReader,
961983
wantsBlock,
962984
wantsReset,
985+
wantsStopSending,
963986
wantsHeaders,
964987
wantsTrailers,
965988
early,
@@ -980,6 +1003,7 @@ class QuicStreamState {
9801003
hasReader,
9811004
wantsBlock,
9821005
wantsReset,
1006+
wantsStopSending,
9831007
wantsHeaders,
9841008
wantsTrailers,
9851009
early,

‎lib/internal/quic/symbols.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const kReset = Symbol('kReset');
5757
constkSendHeaders=Symbol('kSendHeaders');
5858
constkSessionApplication=Symbol('kSessionApplication');
5959
constkSessionTicket=Symbol('kSessionTicket');
60+
constkStopSending=Symbol('kStopSending');
6061
constkTrailers=Symbol('kTrailers');
6162
constkVersionNegotiation=Symbol('kVersionNegotiation');
6263

@@ -93,6 +94,7 @@ module.exports = {
9394
kSendHeaders,
9495
kSessionApplication,
9596
kSessionTicket,
97+
kStopSending,
9698
kTrailers,
9799
kVersionNegotiation,
98100
};

‎src/quic/bindingdata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SessionManager;
6161
V(stream_drain, StreamDrain) \
6262
V(stream_headers, StreamHeaders) \
6363
V(stream_reset, StreamReset) \
64+
V(stream_stop_sending, StreamStopSending) \
6465
V(stream_trailers, StreamTrailers)
6566

6667
// The various JS strings the implementation uses.

‎src/quic/session.cc‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,11 +1611,11 @@ struct Session::Impl final : public MemoryRetainer {
16111611
returnNGTCP2_SUCCESS;
16121612
}
16131613

1614-
staticinton_stream_stop_sending(ngtcp2_conn* conn,
1615-
stream_id stream_id,
1616-
error_code app_error_code,
1617-
void* user_data,
1618-
void* stream_user_data) {
1614+
staticinton_receive_stream_stop_sending(ngtcp2_conn* conn,
1615+
stream_id stream_id,
1616+
error_code app_error_code,
1617+
void* user_data,
1618+
void* stream_user_data) {
16191619
NGTCP2_CALLBACK_SCOPE(session)
16201620
auto* stream = Stream::From(stream_user_data);
16211621
if (stream == nullptr) returnNGTCP2_SUCCESS;
@@ -1652,7 +1652,7 @@ struct Session::Impl final : public MemoryRetainer {
16521652

16531653
staticconstexpr ngtcp2_callbacks CLIENT = {
16541654
ngtcp2_crypto_client_initial_cb,
1655-
nullptr,
1655+
nullptr,// stream_stop_sending
16561656
ngtcp2_crypto_recv_crypto_data_cb,
16571657
on_handshake_completed,
16581658
on_receive_version_negotiation,
@@ -1686,7 +1686,7 @@ struct Session::Impl final : public MemoryRetainer {
16861686
on_acknowledge_datagram,
16871687
on_lost_datagram,
16881688
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1689-
on_stream_stop_sending,
1689+
nullptr, // stream_stop_sending
16901690
ngtcp2_crypto_version_negotiation_cb,
16911691
on_receive_rx_key,
16921692
on_receive_tx_key,
@@ -1697,12 +1697,12 @@ struct Session::Impl final : public MemoryRetainer {
16971697
on_cid_status,
16981698
ngtcp2_crypto_get_path_challenge_data2_cb,
16991699
#ifdef NGTCP2_CALLBACKS_V4
1700-
nullptr,
1700+
on_receive_stream_stop_sending,
17011701
#endif
17021702
};
17031703

17041704
staticconstexpr ngtcp2_callbacks SERVER = {
1705-
nullptr,
1705+
nullptr,// stream_stop_sending
17061706
ngtcp2_crypto_recv_client_initial_cb,
17071707
ngtcp2_crypto_recv_crypto_data_cb,
17081708
on_handshake_completed,
@@ -1737,7 +1737,7 @@ struct Session::Impl final : public MemoryRetainer {
17371737
on_acknowledge_datagram,
17381738
on_lost_datagram,
17391739
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1740-
on_stream_stop_sending,
1740+
nullptr, // stream_stop_sending
17411741
ngtcp2_crypto_version_negotiation_cb,
17421742
nullptr,
17431743
on_receive_tx_key,
@@ -1748,7 +1748,7 @@ struct Session::Impl final : public MemoryRetainer {
17481748
on_cid_status,
17491749
ngtcp2_crypto_get_path_challenge_data2_cb,
17501750
#ifdef NGTCP2_CALLBACKS_V4
1751-
nullptr,
1751+
on_receive_stream_stop_sending,
17521752
#endif
17531753
};
17541754
};

‎src/quic/streams.cc‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ namespace quic {
6161
V(WANTS_HEADERS, wants_headers, uint8_t) \
6262
/* Set when the stream has a reset event handler */ \
6363
V(WANTS_RESET, wants_reset, uint8_t) \
64+
/* Set when the stream has a stop sending event handler */ \
65+
V(WANTS_STOP_SENDING, wants_stop_sending, uint8_t) \
6466
/* Set when the stream has a trailers event handler */ \
6567
V(WANTS_TRAILERS, wants_trailers, uint8_t) \
6668
/* True when 0-RTT early data was received */ \
@@ -1774,19 +1776,11 @@ void Stream::ReceiveData(const uint8_t* data,
17741776
}
17751777

17761778
voidStream::ReceiveStopSending(QuicError error) {
1777-
// STOP_SENDING from the peer asks us to stop sending. Per RFC 9000
1778-
// §3.5 the receiver SHOULD respond with RESET_STREAM, which is what
1779-
// ngtcp2_conn_shutdown_stream_write below schedules. If our
1780-
// writable side has already been shut down (e.g. we already sent
1781-
// RESET_STREAM ourselves or finished sending with FIN) there is
1782-
// nothing more to do here. The previous guard checked
1783-
// `state()->read_ended` which is unrelated to the writable side and
1784-
// suppressed STOP_SENDING handling whenever a sibling RESET_STREAM
1785-
// frame had been processed first within the same packet.
1786-
if (state()->write_ended) return;
1779+
// STOP_SENDING from the peer asks us to stop sending. The required
1780+
// RESET_STREAM response is scheduled automatically.
17871781
Debug(this, "Received stop sending with error %s", error);
1788-
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code());
17891782
EndWritable();
1783+
EmitStopSending(error);
17901784
}
17911785

17921786
voidStream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
@@ -1958,6 +1952,17 @@ void Stream::EmitReset(const QuicError& error) {
19581952
MakeCallback(BindingData::Get(env()).stream_reset_callback(), 1, &err);
19591953
}
19601954

1955+
voidStream::EmitStopSending(const QuicError& error) {
1956+
if (!env()->can_call_into_js() || !state()->wants_stop_sending) {
1957+
return;
1958+
}
1959+
CallbackScope<Stream> cb_scope(this);
1960+
Local<Value> err;
1961+
if (!error.ToV8Value(env()).ToLocal(&err)) return;
1962+
1963+
MakeCallback(BindingData::Get(env()).stream_stop_sending_callback(), 1, &err);
1964+
}
1965+
19611966
voidStream::EmitWantTrailers() {
19621967
// state()->wants_trailers will be set from the javascript side if the
19631968
// stream object has a handler for the trailers event.

‎src/quic/streams.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ class Stream final : public AsyncWrap,
417417
// Notifies the JavaScript side that the stream has been reset.
418418
voidEmitReset(const QuicError& error);
419419

420+
// Notifies the JavaScript side that the peer asked it to stop sending.
421+
voidEmitStopSending(const QuicError& error);
422+
420423
// Notifies the JavaScript side that the application is ready to receive
421424
// trailing headers. Any trailing headers must be sent immediately, and
422425
// synchronously when this callback is triggered.

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ assert.strictEqual(streamState.reset, false);
156156
assert.strictEqual(streamState.hasReader,false);
157157
assert.strictEqual(streamState.wantsBlock,false);
158158
assert.strictEqual(streamState.wantsReset,false);
159+
assert.strictEqual(streamState.wantsStopSending,false);
159160

160161
assert.strictEqual(sessionState.hasPathValidationListener,false);
161162
assert.strictEqual(sessionState.hasDatagramListener,false);

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 8a00872

Browse files
pimterryaduh95
authored andcommitted
quic: fix stop sending behaviour & callback
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #64710 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e59346b commit 8a00872

15 files changed

Lines changed: 193 additions & 164 deletions

‎doc/api/quic.md‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,8 +2013,7 @@ added: v23.8.0
20132013

20142014
The callback to invoke when the peer aborts a direction of the stream by
20152015
sending a `RESET_STREAM` frame (the peer abandons their writable side, so
2016-
no further data will arrive on our readable side) or a `STOP_SENDING`
2017-
frame (the peer asks us to stop writing on our writable side).
2016+
no further data will arrive on our readable side).
20182017

20192018
The callback receives a Node.js error whose `errorCode` (`bigint`)
20202019
property carries the application error code from the wire frame.
@@ -2025,6 +2024,21 @@ continue using the still-active direction on a bidirectional stream),
20252024
abort the other direction with [`writer.fail()`][], or tear down the
20262025
whole stream with [`stream.destroy()`][]. Read/write.
20272026

2027+
### `stream.onstopsending`
2028+
2029+
<!-- YAML
2030+
added: REPLACEME
2031+
-->
2032+
2033+
* Type: {quic.OnStreamErrorCallback}
2034+
2035+
The callback to invoke when the peer aborts a direction of the stream by
2036+
sending a `STOP_SENDING` frame (the peer asks us to stop writing on our
2037+
writable side).
2038+
2039+
The callback receives a Node.js error whose `errorCode` (`bigint`)
2040+
property carries the application error code from the wire frame. Read/write.
2041+
20282042
### `stream.headers`
20292043

20302044
<!-- YAML
@@ -3569,8 +3583,8 @@ functions. If a callback throws synchronously or returns a promise that
35693583
rejects, the error is caught and the owning session or stream is destroyed
35703584
with that error:
35713585

3572-
* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
3573-
`oninfo`, `onwanttrailers`): the stream is destroyed.
3586+
* Stream callbacks (`onblocked`, `onreset`, `onstopsending`, `onheaders`,
3587+
`ontrailers`, `oninfo`, `onwanttrailers`): the stream is destroyed.
35743588
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
35753589
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
35763590
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
@@ -4439,10 +4453,9 @@ added: REPLACEME
44394453
* `session` {quic.QuicSession}
44404454
* `error` {any} The QUIC error associated with the reset.
44414455
4442-
Published when a stream receives a STOP\_SENDING or RESET\_STREAM frame
4443-
from the peer, indicating the peer has aborted the stream. This is a
4444-
key signal for diagnosing application-level issues such as cancelled
4445-
requests.
4456+
Published when a stream receives a RESET\_STREAM frame from the peer,
4457+
indicating the peer has aborted its sending direction. This is a key signal
4458+
for diagnosing application-level issues such as cancelled requests.
44464459
44474460
### Channel: `quic.stream.blocked`
44484461

‎lib/internal/quic/quic.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
DataViewPrototypeGetByteLength,
1313
ErrorCaptureStackTrace,
1414
FunctionPrototypeBind,
15+
FunctionPrototypeCall,
1516
Number,
1617
ObjectDefineProperties,
1718
ObjectKeys,
@@ -210,6 +211,7 @@ const {
210211
kSendHeaders,
211212
kSessionApplication,
212213
kSessionTicket,
214+
kStopSending,
213215
kTrailers,
214216
kVersionNegotiation,
215217
kInspect,
@@ -991,6 +993,14 @@ setCallbacks({
991993
this[kOwner][kReset](error);
992994
},
993995

996+
onStreamStopSending(error){
997+
if(error!==undefined){
998+
error=convertQuicError(error);
999+
}
1000+
debug('stream stop sending callback',this[kOwner],error);
1001+
this[kOwner][kStopSending](error);
1002+
},
1003+
9941004
onStreamHeaders(headers,kind){
9951005
// Called when the stream C++ handle has received a full block of headers.
9961006
debug(`stream ${this[kOwner].id} headers callback`,headers,kind);
@@ -1576,6 +1586,7 @@ class QuicStream {
15761586
onerror: undefined,
15771587
onblocked: undefined,
15781588
onreset: undefined,
1589+
onstopsending: undefined,
15791590
onheaders: undefined,
15801591
ontrailers: undefined,
15811592
oninfo: undefined,
@@ -1781,6 +1792,25 @@ class QuicStream {
17811792
}
17821793
}
17831794

1795+
/** @type {OnStreamErrorCallback} */
1796+
getonstopsending(){
1797+
assertIsQuicStream(this);
1798+
returnthis.#inner.onstopsending;
1799+
}
1800+
1801+
setonstopsending(fn){
1802+
assertIsQuicStream(this);
1803+
constinner=this.#inner;
1804+
if(fn===undefined){
1805+
inner.onstopsending=undefined;
1806+
inner.state.wantsStopSending=false;
1807+
}else{
1808+
validateFunction(fn,'onstopsending');
1809+
inner.onstopsending=FunctionPrototypeBind(fn,this);
1810+
inner.state.wantsStopSending=true;
1811+
}
1812+
}
1813+
17841814
/** @type {OnHeadersCallback} */
17851815
getonheaders(){
17861816
assertIsQuicStream(this);
@@ -2143,6 +2173,19 @@ class QuicStream {
21432173
}
21442174
};
21452175

2176+
constonStopSending=stream[kStopSending];
2177+
stream[kStopSending]=(reason)=>{
2178+
if(!closed&&!errored){
2179+
errored=true;
2180+
error=reason;
2181+
if(drainWakeup!=null){
2182+
drainWakeup.reject(error);
2183+
drainWakeup=null;
2184+
}
2185+
}
2186+
FunctionPrototypeCall(onStopSending,stream,reason);
2187+
};
2188+
21462189
// A note on backpressure handling: per the stream/iter spec, the default
21472190
// backpressure policy for writers is strict, meaning that if the stream
21482191
// signals backpressure additional writes are rejected until the buffer has
@@ -2543,6 +2586,7 @@ class QuicStream {
25432586
inner.pendingClose.resolve=undefined;
25442587
inner.onblocked=undefined;
25452588
inner.onreset=undefined;
2589+
inner.onstopsending=undefined;
25462590
inner.onheaders=undefined;
25472591
inner.onerror=undefined;
25482592
inner.ontrailers=undefined;
@@ -2596,6 +2640,12 @@ class QuicStream {
25962640
safeCallbackInvoke(inner.onreset,this,error);
25972641
}
25982642

2643+
[kStopSending](error){
2644+
constinner=this.#inner;
2645+
assert(inner.onstopsending,'Unexpected stop sending event');
2646+
safeCallbackInvoke(inner.onstopsending,this,error);
2647+
}
2648+
25992649
[kHeaders](headers,kind){
26002650
constblock=parseHeaderPairs(headers);
26012651
constkindName=kHeadersKindName[kind]??kind;

‎lib/internal/quic/state.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
IDX_STATE_STREAM_WANTS_BLOCK,
102102
IDX_STATE_STREAM_WANTS_HEADERS,
103103
IDX_STATE_STREAM_WANTS_RESET,
104+
IDX_STATE_STREAM_WANTS_STOP_SENDING,
104105
IDX_STATE_STREAM_WANTS_TRAILERS,
105106
IDX_STATE_STREAM_RECEIVED_EARLY_DATA,
106107
IDX_STATE_STREAM_WRITE_DESIRED_SIZE,
@@ -142,6 +143,7 @@ assert(IDX_STATE_STREAM_HAS_READER !== undefined);
142143
assert(IDX_STATE_STREAM_WANTS_BLOCK!==undefined);
143144
assert(IDX_STATE_STREAM_WANTS_HEADERS!==undefined);
144145
assert(IDX_STATE_STREAM_WANTS_RESET!==undefined);
146+
assert(IDX_STATE_STREAM_WANTS_STOP_SENDING!==undefined);
145147
assert(IDX_STATE_STREAM_WANTS_TRAILERS!==undefined);
146148
assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE!==undefined);
147149
assert(IDX_STATE_STREAM_RESET_CODE!==undefined);
@@ -826,6 +828,24 @@ class QuicStreamState {
826828
DataViewPrototypeSetUint8(handle,this.#offset +IDX_STATE_STREAM_WANTS_RESET,val ? 1 : 0);
827829
}
828830

831+
/** @type {boolean} */
832+
getwantsStopSending(){
833+
consthandle=this.#handle;
834+
if(handle===undefined)returnundefined;
835+
returnDataViewPrototypeGetUint8(
836+
handle,this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING)!==0;
837+
}
838+
839+
/** @type {boolean} */
840+
setwantsStopSending(val){
841+
consthandle=this.#handle;
842+
if(handle===undefined)return;
843+
DataViewPrototypeSetUint8(
844+
handle,
845+
this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING,
846+
val ? 1 : 0);
847+
}
848+
829849
/** @type {boolean} */
830850
getwantsTrailers(){
831851
consthandle=this.#handle;
@@ -903,6 +923,7 @@ class QuicStreamState {
903923
hasReader,
904924
wantsBlock,
905925
wantsReset,
926+
wantsStopSending,
906927
wantsHeaders,
907928
wantsTrailers,
908929
early,
@@ -923,6 +944,7 @@ class QuicStreamState {
923944
hasReader,
924945
wantsBlock,
925946
wantsReset,
947+
wantsStopSending,
926948
wantsHeaders,
927949
wantsTrailers,
928950
early,
@@ -960,6 +982,7 @@ class QuicStreamState {
960982
hasReader,
961983
wantsBlock,
962984
wantsReset,
985+
wantsStopSending,
963986
wantsHeaders,
964987
wantsTrailers,
965988
early,
@@ -980,6 +1003,7 @@ class QuicStreamState {
9801003
hasReader,
9811004
wantsBlock,
9821005
wantsReset,
1006+
wantsStopSending,
9831007
wantsHeaders,
9841008
wantsTrailers,
9851009
early,

‎lib/internal/quic/symbols.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const kReset = Symbol('kReset');
5757
constkSendHeaders=Symbol('kSendHeaders');
5858
constkSessionApplication=Symbol('kSessionApplication');
5959
constkSessionTicket=Symbol('kSessionTicket');
60+
constkStopSending=Symbol('kStopSending');
6061
constkTrailers=Symbol('kTrailers');
6162
constkVersionNegotiation=Symbol('kVersionNegotiation');
6263

@@ -93,6 +94,7 @@ module.exports = {
9394
kSendHeaders,
9495
kSessionApplication,
9596
kSessionTicket,
97+
kStopSending,
9698
kTrailers,
9799
kVersionNegotiation,
98100
};

‎src/quic/bindingdata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SessionManager;
6161
V(stream_drain, StreamDrain) \
6262
V(stream_headers, StreamHeaders) \
6363
V(stream_reset, StreamReset) \
64+
V(stream_stop_sending, StreamStopSending) \
6465
V(stream_trailers, StreamTrailers)
6566

6667
// The various JS strings the implementation uses.

‎src/quic/session.cc‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,11 +1611,11 @@ struct Session::Impl final : public MemoryRetainer {
16111611
returnNGTCP2_SUCCESS;
16121612
}
16131613

1614-
staticinton_stream_stop_sending(ngtcp2_conn* conn,
1615-
stream_id stream_id,
1616-
error_code app_error_code,
1617-
void* user_data,
1618-
void* stream_user_data) {
1614+
staticinton_receive_stream_stop_sending(ngtcp2_conn* conn,
1615+
stream_id stream_id,
1616+
error_code app_error_code,
1617+
void* user_data,
1618+
void* stream_user_data) {
16191619
NGTCP2_CALLBACK_SCOPE(session)
16201620
auto* stream = Stream::From(stream_user_data);
16211621
if (stream == nullptr) returnNGTCP2_SUCCESS;
@@ -1652,7 +1652,7 @@ struct Session::Impl final : public MemoryRetainer {
16521652

16531653
staticconstexpr ngtcp2_callbacks CLIENT = {
16541654
ngtcp2_crypto_client_initial_cb,
1655-
nullptr,
1655+
nullptr,// stream_stop_sending
16561656
ngtcp2_crypto_recv_crypto_data_cb,
16571657
on_handshake_completed,
16581658
on_receive_version_negotiation,
@@ -1686,7 +1686,7 @@ struct Session::Impl final : public MemoryRetainer {
16861686
on_acknowledge_datagram,
16871687
on_lost_datagram,
16881688
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1689-
on_stream_stop_sending,
1689+
nullptr, // stream_stop_sending
16901690
ngtcp2_crypto_version_negotiation_cb,
16911691
on_receive_rx_key,
16921692
on_receive_tx_key,
@@ -1697,12 +1697,12 @@ struct Session::Impl final : public MemoryRetainer {
16971697
on_cid_status,
16981698
ngtcp2_crypto_get_path_challenge_data2_cb,
16991699
#ifdef NGTCP2_CALLBACKS_V4
1700-
nullptr,
1700+
on_receive_stream_stop_sending,
17011701
#endif
17021702
};
17031703

17041704
staticconstexpr ngtcp2_callbacks SERVER = {
1705-
nullptr,
1705+
nullptr,// stream_stop_sending
17061706
ngtcp2_crypto_recv_client_initial_cb,
17071707
ngtcp2_crypto_recv_crypto_data_cb,
17081708
on_handshake_completed,
@@ -1737,7 +1737,7 @@ struct Session::Impl final : public MemoryRetainer {
17371737
on_acknowledge_datagram,
17381738
on_lost_datagram,
17391739
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1740-
on_stream_stop_sending,
1740+
nullptr, // stream_stop_sending
17411741
ngtcp2_crypto_version_negotiation_cb,
17421742
nullptr,
17431743
on_receive_tx_key,
@@ -1748,7 +1748,7 @@ struct Session::Impl final : public MemoryRetainer {
17481748
on_cid_status,
17491749
ngtcp2_crypto_get_path_challenge_data2_cb,
17501750
#ifdef NGTCP2_CALLBACKS_V4
1751-
nullptr,
1751+
on_receive_stream_stop_sending,
17521752
#endif
17531753
};
17541754
};

‎src/quic/streams.cc‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ namespace quic {
6161
V(WANTS_HEADERS, wants_headers, uint8_t) \
6262
/* Set when the stream has a reset event handler */ \
6363
V(WANTS_RESET, wants_reset, uint8_t) \
64+
/* Set when the stream has a stop sending event handler */ \
65+
V(WANTS_STOP_SENDING, wants_stop_sending, uint8_t) \
6466
/* Set when the stream has a trailers event handler */ \
6567
V(WANTS_TRAILERS, wants_trailers, uint8_t) \
6668
/* True when 0-RTT early data was received */ \
@@ -1774,19 +1776,11 @@ void Stream::ReceiveData(const uint8_t* data,
17741776
}
17751777

17761778
voidStream::ReceiveStopSending(QuicError error) {
1777-
// STOP_SENDING from the peer asks us to stop sending. Per RFC 9000
1778-
// §3.5 the receiver SHOULD respond with RESET_STREAM, which is what
1779-
// ngtcp2_conn_shutdown_stream_write below schedules. If our
1780-
// writable side has already been shut down (e.g. we already sent
1781-
// RESET_STREAM ourselves or finished sending with FIN) there is
1782-
// nothing more to do here. The previous guard checked
1783-
// `state()->read_ended` which is unrelated to the writable side and
1784-
// suppressed STOP_SENDING handling whenever a sibling RESET_STREAM
1785-
// frame had been processed first within the same packet.
1786-
if (state()->write_ended) return;
1779+
// STOP_SENDING from the peer asks us to stop sending. The required
1780+
// RESET_STREAM response is scheduled automatically.
17871781
Debug(this, "Received stop sending with error %s", error);
1788-
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code());
17891782
EndWritable();
1783+
EmitStopSending(error);
17901784
}
17911785

17921786
voidStream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
@@ -1958,6 +1952,17 @@ void Stream::EmitReset(const QuicError& error) {
19581952
MakeCallback(BindingData::Get(env()).stream_reset_callback(), 1, &err);
19591953
}
19601954

1955+
voidStream::EmitStopSending(const QuicError& error) {
1956+
if (!env()->can_call_into_js() || !state()->wants_stop_sending) {
1957+
return;
1958+
}
1959+
CallbackScope<Stream> cb_scope(this);
1960+
Local<Value> err;
1961+
if (!error.ToV8Value(env()).ToLocal(&err)) return;
1962+
1963+
MakeCallback(BindingData::Get(env()).stream_stop_sending_callback(), 1, &err);
1964+
}
1965+
19611966
voidStream::EmitWantTrailers() {
19621967
// state()->wants_trailers will be set from the javascript side if the
19631968
// stream object has a handler for the trailers event.

‎src/quic/streams.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ class Stream final : public AsyncWrap,
417417
// Notifies the JavaScript side that the stream has been reset.
418418
voidEmitReset(const QuicError& error);
419419

420+
// Notifies the JavaScript side that the peer asked it to stop sending.
421+
voidEmitStopSending(const QuicError& error);
422+
420423
// Notifies the JavaScript side that the application is ready to receive
421424
// trailing headers. Any trailing headers must be sent immediately, and
422425
// synchronously when this callback is triggered.

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ assert.strictEqual(streamState.reset, false);
156156
assert.strictEqual(streamState.hasReader,false);
157157
assert.strictEqual(streamState.wantsBlock,false);
158158
assert.strictEqual(streamState.wantsReset,false);
159+
assert.strictEqual(streamState.wantsStopSending,false);
159160

160161
assert.strictEqual(sessionState.hasPathValidationListener,false);
161162
assert.strictEqual(sessionState.hasDatagramListener,false);

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 8a00872

Browse files
pimterryaduh95
authored andcommitted
quic: fix stop sending behaviour & callback
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #64710 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e59346b commit 8a00872

15 files changed

Lines changed: 193 additions & 164 deletions

‎doc/api/quic.md‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,8 +2013,7 @@ added: v23.8.0
20132013

20142014
The callback to invoke when the peer aborts a direction of the stream by
20152015
sending a `RESET_STREAM` frame (the peer abandons their writable side, so
2016-
no further data will arrive on our readable side) or a `STOP_SENDING`
2017-
frame (the peer asks us to stop writing on our writable side).
2016+
no further data will arrive on our readable side).
20182017

20192018
The callback receives a Node.js error whose `errorCode` (`bigint`)
20202019
property carries the application error code from the wire frame.
@@ -2025,6 +2024,21 @@ continue using the still-active direction on a bidirectional stream),
20252024
abort the other direction with [`writer.fail()`][], or tear down the
20262025
whole stream with [`stream.destroy()`][]. Read/write.
20272026

2027+
### `stream.onstopsending`
2028+
2029+
<!-- YAML
2030+
added: REPLACEME
2031+
-->
2032+
2033+
* Type: {quic.OnStreamErrorCallback}
2034+
2035+
The callback to invoke when the peer aborts a direction of the stream by
2036+
sending a `STOP_SENDING` frame (the peer asks us to stop writing on our
2037+
writable side).
2038+
2039+
The callback receives a Node.js error whose `errorCode` (`bigint`)
2040+
property carries the application error code from the wire frame. Read/write.
2041+
20282042
### `stream.headers`
20292043

20302044
<!-- YAML
@@ -3569,8 +3583,8 @@ functions. If a callback throws synchronously or returns a promise that
35693583
rejects, the error is caught and the owning session or stream is destroyed
35703584
with that error:
35713585

3572-
* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
3573-
`oninfo`, `onwanttrailers`): the stream is destroyed.
3586+
* Stream callbacks (`onblocked`, `onreset`, `onstopsending`, `onheaders`,
3587+
`ontrailers`, `oninfo`, `onwanttrailers`): the stream is destroyed.
35743588
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
35753589
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
35763590
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
@@ -4439,10 +4453,9 @@ added: REPLACEME
44394453
* `session` {quic.QuicSession}
44404454
* `error` {any} The QUIC error associated with the reset.
44414455
4442-
Published when a stream receives a STOP\_SENDING or RESET\_STREAM frame
4443-
from the peer, indicating the peer has aborted the stream. This is a
4444-
key signal for diagnosing application-level issues such as cancelled
4445-
requests.
4456+
Published when a stream receives a RESET\_STREAM frame from the peer,
4457+
indicating the peer has aborted its sending direction. This is a key signal
4458+
for diagnosing application-level issues such as cancelled requests.
44464459
44474460
### Channel: `quic.stream.blocked`
44484461

‎lib/internal/quic/quic.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
DataViewPrototypeGetByteLength,
1313
ErrorCaptureStackTrace,
1414
FunctionPrototypeBind,
15+
FunctionPrototypeCall,
1516
Number,
1617
ObjectDefineProperties,
1718
ObjectKeys,
@@ -210,6 +211,7 @@ const {
210211
kSendHeaders,
211212
kSessionApplication,
212213
kSessionTicket,
214+
kStopSending,
213215
kTrailers,
214216
kVersionNegotiation,
215217
kInspect,
@@ -991,6 +993,14 @@ setCallbacks({
991993
this[kOwner][kReset](error);
992994
},
993995

996+
onStreamStopSending(error){
997+
if(error!==undefined){
998+
error=convertQuicError(error);
999+
}
1000+
debug('stream stop sending callback',this[kOwner],error);
1001+
this[kOwner][kStopSending](error);
1002+
},
1003+
9941004
onStreamHeaders(headers,kind){
9951005
// Called when the stream C++ handle has received a full block of headers.
9961006
debug(`stream ${this[kOwner].id} headers callback`,headers,kind);
@@ -1576,6 +1586,7 @@ class QuicStream {
15761586
onerror: undefined,
15771587
onblocked: undefined,
15781588
onreset: undefined,
1589+
onstopsending: undefined,
15791590
onheaders: undefined,
15801591
ontrailers: undefined,
15811592
oninfo: undefined,
@@ -1781,6 +1792,25 @@ class QuicStream {
17811792
}
17821793
}
17831794

1795+
/** @type {OnStreamErrorCallback} */
1796+
getonstopsending(){
1797+
assertIsQuicStream(this);
1798+
returnthis.#inner.onstopsending;
1799+
}
1800+
1801+
setonstopsending(fn){
1802+
assertIsQuicStream(this);
1803+
constinner=this.#inner;
1804+
if(fn===undefined){
1805+
inner.onstopsending=undefined;
1806+
inner.state.wantsStopSending=false;
1807+
}else{
1808+
validateFunction(fn,'onstopsending');
1809+
inner.onstopsending=FunctionPrototypeBind(fn,this);
1810+
inner.state.wantsStopSending=true;
1811+
}
1812+
}
1813+
17841814
/** @type {OnHeadersCallback} */
17851815
getonheaders(){
17861816
assertIsQuicStream(this);
@@ -2143,6 +2173,19 @@ class QuicStream {
21432173
}
21442174
};
21452175

2176+
constonStopSending=stream[kStopSending];
2177+
stream[kStopSending]=(reason)=>{
2178+
if(!closed&&!errored){
2179+
errored=true;
2180+
error=reason;
2181+
if(drainWakeup!=null){
2182+
drainWakeup.reject(error);
2183+
drainWakeup=null;
2184+
}
2185+
}
2186+
FunctionPrototypeCall(onStopSending,stream,reason);
2187+
};
2188+
21462189
// A note on backpressure handling: per the stream/iter spec, the default
21472190
// backpressure policy for writers is strict, meaning that if the stream
21482191
// signals backpressure additional writes are rejected until the buffer has
@@ -2543,6 +2586,7 @@ class QuicStream {
25432586
inner.pendingClose.resolve=undefined;
25442587
inner.onblocked=undefined;
25452588
inner.onreset=undefined;
2589+
inner.onstopsending=undefined;
25462590
inner.onheaders=undefined;
25472591
inner.onerror=undefined;
25482592
inner.ontrailers=undefined;
@@ -2596,6 +2640,12 @@ class QuicStream {
25962640
safeCallbackInvoke(inner.onreset,this,error);
25972641
}
25982642

2643+
[kStopSending](error){
2644+
constinner=this.#inner;
2645+
assert(inner.onstopsending,'Unexpected stop sending event');
2646+
safeCallbackInvoke(inner.onstopsending,this,error);
2647+
}
2648+
25992649
[kHeaders](headers,kind){
26002650
constblock=parseHeaderPairs(headers);
26012651
constkindName=kHeadersKindName[kind]??kind;

‎lib/internal/quic/state.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
IDX_STATE_STREAM_WANTS_BLOCK,
102102
IDX_STATE_STREAM_WANTS_HEADERS,
103103
IDX_STATE_STREAM_WANTS_RESET,
104+
IDX_STATE_STREAM_WANTS_STOP_SENDING,
104105
IDX_STATE_STREAM_WANTS_TRAILERS,
105106
IDX_STATE_STREAM_RECEIVED_EARLY_DATA,
106107
IDX_STATE_STREAM_WRITE_DESIRED_SIZE,
@@ -142,6 +143,7 @@ assert(IDX_STATE_STREAM_HAS_READER !== undefined);
142143
assert(IDX_STATE_STREAM_WANTS_BLOCK!==undefined);
143144
assert(IDX_STATE_STREAM_WANTS_HEADERS!==undefined);
144145
assert(IDX_STATE_STREAM_WANTS_RESET!==undefined);
146+
assert(IDX_STATE_STREAM_WANTS_STOP_SENDING!==undefined);
145147
assert(IDX_STATE_STREAM_WANTS_TRAILERS!==undefined);
146148
assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE!==undefined);
147149
assert(IDX_STATE_STREAM_RESET_CODE!==undefined);
@@ -826,6 +828,24 @@ class QuicStreamState {
826828
DataViewPrototypeSetUint8(handle,this.#offset +IDX_STATE_STREAM_WANTS_RESET,val ? 1 : 0);
827829
}
828830

831+
/** @type {boolean} */
832+
getwantsStopSending(){
833+
consthandle=this.#handle;
834+
if(handle===undefined)returnundefined;
835+
returnDataViewPrototypeGetUint8(
836+
handle,this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING)!==0;
837+
}
838+
839+
/** @type {boolean} */
840+
setwantsStopSending(val){
841+
consthandle=this.#handle;
842+
if(handle===undefined)return;
843+
DataViewPrototypeSetUint8(
844+
handle,
845+
this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING,
846+
val ? 1 : 0);
847+
}
848+
829849
/** @type {boolean} */
830850
getwantsTrailers(){
831851
consthandle=this.#handle;
@@ -903,6 +923,7 @@ class QuicStreamState {
903923
hasReader,
904924
wantsBlock,
905925
wantsReset,
926+
wantsStopSending,
906927
wantsHeaders,
907928
wantsTrailers,
908929
early,
@@ -923,6 +944,7 @@ class QuicStreamState {
923944
hasReader,
924945
wantsBlock,
925946
wantsReset,
947+
wantsStopSending,
926948
wantsHeaders,
927949
wantsTrailers,
928950
early,
@@ -960,6 +982,7 @@ class QuicStreamState {
960982
hasReader,
961983
wantsBlock,
962984
wantsReset,
985+
wantsStopSending,
963986
wantsHeaders,
964987
wantsTrailers,
965988
early,
@@ -980,6 +1003,7 @@ class QuicStreamState {
9801003
hasReader,
9811004
wantsBlock,
9821005
wantsReset,
1006+
wantsStopSending,
9831007
wantsHeaders,
9841008
wantsTrailers,
9851009
early,

‎lib/internal/quic/symbols.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const kReset = Symbol('kReset');
5757
constkSendHeaders=Symbol('kSendHeaders');
5858
constkSessionApplication=Symbol('kSessionApplication');
5959
constkSessionTicket=Symbol('kSessionTicket');
60+
constkStopSending=Symbol('kStopSending');
6061
constkTrailers=Symbol('kTrailers');
6162
constkVersionNegotiation=Symbol('kVersionNegotiation');
6263

@@ -93,6 +94,7 @@ module.exports = {
9394
kSendHeaders,
9495
kSessionApplication,
9596
kSessionTicket,
97+
kStopSending,
9698
kTrailers,
9799
kVersionNegotiation,
98100
};

‎src/quic/bindingdata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SessionManager;
6161
V(stream_drain, StreamDrain) \
6262
V(stream_headers, StreamHeaders) \
6363
V(stream_reset, StreamReset) \
64+
V(stream_stop_sending, StreamStopSending) \
6465
V(stream_trailers, StreamTrailers)
6566

6667
// The various JS strings the implementation uses.

‎src/quic/session.cc‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,11 +1611,11 @@ struct Session::Impl final : public MemoryRetainer {
16111611
returnNGTCP2_SUCCESS;
16121612
}
16131613

1614-
staticinton_stream_stop_sending(ngtcp2_conn* conn,
1615-
stream_id stream_id,
1616-
error_code app_error_code,
1617-
void* user_data,
1618-
void* stream_user_data) {
1614+
staticinton_receive_stream_stop_sending(ngtcp2_conn* conn,
1615+
stream_id stream_id,
1616+
error_code app_error_code,
1617+
void* user_data,
1618+
void* stream_user_data) {
16191619
NGTCP2_CALLBACK_SCOPE(session)
16201620
auto* stream = Stream::From(stream_user_data);
16211621
if (stream == nullptr) returnNGTCP2_SUCCESS;
@@ -1652,7 +1652,7 @@ struct Session::Impl final : public MemoryRetainer {
16521652

16531653
staticconstexpr ngtcp2_callbacks CLIENT = {
16541654
ngtcp2_crypto_client_initial_cb,
1655-
nullptr,
1655+
nullptr,// stream_stop_sending
16561656
ngtcp2_crypto_recv_crypto_data_cb,
16571657
on_handshake_completed,
16581658
on_receive_version_negotiation,
@@ -1686,7 +1686,7 @@ struct Session::Impl final : public MemoryRetainer {
16861686
on_acknowledge_datagram,
16871687
on_lost_datagram,
16881688
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1689-
on_stream_stop_sending,
1689+
nullptr, // stream_stop_sending
16901690
ngtcp2_crypto_version_negotiation_cb,
16911691
on_receive_rx_key,
16921692
on_receive_tx_key,
@@ -1697,12 +1697,12 @@ struct Session::Impl final : public MemoryRetainer {
16971697
on_cid_status,
16981698
ngtcp2_crypto_get_path_challenge_data2_cb,
16991699
#ifdef NGTCP2_CALLBACKS_V4
1700-
nullptr,
1700+
on_receive_stream_stop_sending,
17011701
#endif
17021702
};
17031703

17041704
staticconstexpr ngtcp2_callbacks SERVER = {
1705-
nullptr,
1705+
nullptr,// stream_stop_sending
17061706
ngtcp2_crypto_recv_client_initial_cb,
17071707
ngtcp2_crypto_recv_crypto_data_cb,
17081708
on_handshake_completed,
@@ -1737,7 +1737,7 @@ struct Session::Impl final : public MemoryRetainer {
17371737
on_acknowledge_datagram,
17381738
on_lost_datagram,
17391739
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1740-
on_stream_stop_sending,
1740+
nullptr, // stream_stop_sending
17411741
ngtcp2_crypto_version_negotiation_cb,
17421742
nullptr,
17431743
on_receive_tx_key,
@@ -1748,7 +1748,7 @@ struct Session::Impl final : public MemoryRetainer {
17481748
on_cid_status,
17491749
ngtcp2_crypto_get_path_challenge_data2_cb,
17501750
#ifdef NGTCP2_CALLBACKS_V4
1751-
nullptr,
1751+
on_receive_stream_stop_sending,
17521752
#endif
17531753
};
17541754
};

‎src/quic/streams.cc‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ namespace quic {
6161
V(WANTS_HEADERS, wants_headers, uint8_t) \
6262
/* Set when the stream has a reset event handler */ \
6363
V(WANTS_RESET, wants_reset, uint8_t) \
64+
/* Set when the stream has a stop sending event handler */ \
65+
V(WANTS_STOP_SENDING, wants_stop_sending, uint8_t) \
6466
/* Set when the stream has a trailers event handler */ \
6567
V(WANTS_TRAILERS, wants_trailers, uint8_t) \
6668
/* True when 0-RTT early data was received */ \
@@ -1774,19 +1776,11 @@ void Stream::ReceiveData(const uint8_t* data,
17741776
}
17751777

17761778
voidStream::ReceiveStopSending(QuicError error) {
1777-
// STOP_SENDING from the peer asks us to stop sending. Per RFC 9000
1778-
// §3.5 the receiver SHOULD respond with RESET_STREAM, which is what
1779-
// ngtcp2_conn_shutdown_stream_write below schedules. If our
1780-
// writable side has already been shut down (e.g. we already sent
1781-
// RESET_STREAM ourselves or finished sending with FIN) there is
1782-
// nothing more to do here. The previous guard checked
1783-
// `state()->read_ended` which is unrelated to the writable side and
1784-
// suppressed STOP_SENDING handling whenever a sibling RESET_STREAM
1785-
// frame had been processed first within the same packet.
1786-
if (state()->write_ended) return;
1779+
// STOP_SENDING from the peer asks us to stop sending. The required
1780+
// RESET_STREAM response is scheduled automatically.
17871781
Debug(this, "Received stop sending with error %s", error);
1788-
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code());
17891782
EndWritable();
1783+
EmitStopSending(error);
17901784
}
17911785

17921786
voidStream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
@@ -1958,6 +1952,17 @@ void Stream::EmitReset(const QuicError& error) {
19581952
MakeCallback(BindingData::Get(env()).stream_reset_callback(), 1, &err);
19591953
}
19601954

1955+
voidStream::EmitStopSending(const QuicError& error) {
1956+
if (!env()->can_call_into_js() || !state()->wants_stop_sending) {
1957+
return;
1958+
}
1959+
CallbackScope<Stream> cb_scope(this);
1960+
Local<Value> err;
1961+
if (!error.ToV8Value(env()).ToLocal(&err)) return;
1962+
1963+
MakeCallback(BindingData::Get(env()).stream_stop_sending_callback(), 1, &err);
1964+
}
1965+
19611966
voidStream::EmitWantTrailers() {
19621967
// state()->wants_trailers will be set from the javascript side if the
19631968
// stream object has a handler for the trailers event.

‎src/quic/streams.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ class Stream final : public AsyncWrap,
417417
// Notifies the JavaScript side that the stream has been reset.
418418
voidEmitReset(const QuicError& error);
419419

420+
// Notifies the JavaScript side that the peer asked it to stop sending.
421+
voidEmitStopSending(const QuicError& error);
422+
420423
// Notifies the JavaScript side that the application is ready to receive
421424
// trailing headers. Any trailing headers must be sent immediately, and
422425
// synchronously when this callback is triggered.

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ assert.strictEqual(streamState.reset, false);
156156
assert.strictEqual(streamState.hasReader,false);
157157
assert.strictEqual(streamState.wantsBlock,false);
158158
assert.strictEqual(streamState.wantsReset,false);
159+
assert.strictEqual(streamState.wantsStopSending,false);
159160

160161
assert.strictEqual(sessionState.hasPathValidationListener,false);
161162
assert.strictEqual(sessionState.hasDatagramListener,false);

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 8a00872

Browse files
pimterryaduh95
authored andcommitted
quic: fix stop sending behaviour & callback
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #64710 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e59346b commit 8a00872

15 files changed

Lines changed: 193 additions & 164 deletions

‎doc/api/quic.md‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,8 +2013,7 @@ added: v23.8.0
20132013

20142014
The callback to invoke when the peer aborts a direction of the stream by
20152015
sending a `RESET_STREAM` frame (the peer abandons their writable side, so
2016-
no further data will arrive on our readable side) or a `STOP_SENDING`
2017-
frame (the peer asks us to stop writing on our writable side).
2016+
no further data will arrive on our readable side).
20182017

20192018
The callback receives a Node.js error whose `errorCode` (`bigint`)
20202019
property carries the application error code from the wire frame.
@@ -2025,6 +2024,21 @@ continue using the still-active direction on a bidirectional stream),
20252024
abort the other direction with [`writer.fail()`][], or tear down the
20262025
whole stream with [`stream.destroy()`][]. Read/write.
20272026

2027+
### `stream.onstopsending`
2028+
2029+
<!-- YAML
2030+
added: REPLACEME
2031+
-->
2032+
2033+
* Type: {quic.OnStreamErrorCallback}
2034+
2035+
The callback to invoke when the peer aborts a direction of the stream by
2036+
sending a `STOP_SENDING` frame (the peer asks us to stop writing on our
2037+
writable side).
2038+
2039+
The callback receives a Node.js error whose `errorCode` (`bigint`)
2040+
property carries the application error code from the wire frame. Read/write.
2041+
20282042
### `stream.headers`
20292043

20302044
<!-- YAML
@@ -3569,8 +3583,8 @@ functions. If a callback throws synchronously or returns a promise that
35693583
rejects, the error is caught and the owning session or stream is destroyed
35703584
with that error:
35713585

3572-
* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
3573-
`oninfo`, `onwanttrailers`): the stream is destroyed.
3586+
* Stream callbacks (`onblocked`, `onreset`, `onstopsending`, `onheaders`,
3587+
`ontrailers`, `oninfo`, `onwanttrailers`): the stream is destroyed.
35743588
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
35753589
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
35763590
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
@@ -4439,10 +4453,9 @@ added: REPLACEME
44394453
* `session` {quic.QuicSession}
44404454
* `error` {any} The QUIC error associated with the reset.
44414455
4442-
Published when a stream receives a STOP\_SENDING or RESET\_STREAM frame
4443-
from the peer, indicating the peer has aborted the stream. This is a
4444-
key signal for diagnosing application-level issues such as cancelled
4445-
requests.
4456+
Published when a stream receives a RESET\_STREAM frame from the peer,
4457+
indicating the peer has aborted its sending direction. This is a key signal
4458+
for diagnosing application-level issues such as cancelled requests.
44464459
44474460
### Channel: `quic.stream.blocked`
44484461

‎lib/internal/quic/quic.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
DataViewPrototypeGetByteLength,
1313
ErrorCaptureStackTrace,
1414
FunctionPrototypeBind,
15+
FunctionPrototypeCall,
1516
Number,
1617
ObjectDefineProperties,
1718
ObjectKeys,
@@ -210,6 +211,7 @@ const {
210211
kSendHeaders,
211212
kSessionApplication,
212213
kSessionTicket,
214+
kStopSending,
213215
kTrailers,
214216
kVersionNegotiation,
215217
kInspect,
@@ -991,6 +993,14 @@ setCallbacks({
991993
this[kOwner][kReset](error);
992994
},
993995

996+
onStreamStopSending(error){
997+
if(error!==undefined){
998+
error=convertQuicError(error);
999+
}
1000+
debug('stream stop sending callback',this[kOwner],error);
1001+
this[kOwner][kStopSending](error);
1002+
},
1003+
9941004
onStreamHeaders(headers,kind){
9951005
// Called when the stream C++ handle has received a full block of headers.
9961006
debug(`stream ${this[kOwner].id} headers callback`,headers,kind);
@@ -1576,6 +1586,7 @@ class QuicStream {
15761586
onerror: undefined,
15771587
onblocked: undefined,
15781588
onreset: undefined,
1589+
onstopsending: undefined,
15791590
onheaders: undefined,
15801591
ontrailers: undefined,
15811592
oninfo: undefined,
@@ -1781,6 +1792,25 @@ class QuicStream {
17811792
}
17821793
}
17831794

1795+
/** @type {OnStreamErrorCallback} */
1796+
getonstopsending(){
1797+
assertIsQuicStream(this);
1798+
returnthis.#inner.onstopsending;
1799+
}
1800+
1801+
setonstopsending(fn){
1802+
assertIsQuicStream(this);
1803+
constinner=this.#inner;
1804+
if(fn===undefined){
1805+
inner.onstopsending=undefined;
1806+
inner.state.wantsStopSending=false;
1807+
}else{
1808+
validateFunction(fn,'onstopsending');
1809+
inner.onstopsending=FunctionPrototypeBind(fn,this);
1810+
inner.state.wantsStopSending=true;
1811+
}
1812+
}
1813+
17841814
/** @type {OnHeadersCallback} */
17851815
getonheaders(){
17861816
assertIsQuicStream(this);
@@ -2143,6 +2173,19 @@ class QuicStream {
21432173
}
21442174
};
21452175

2176+
constonStopSending=stream[kStopSending];
2177+
stream[kStopSending]=(reason)=>{
2178+
if(!closed&&!errored){
2179+
errored=true;
2180+
error=reason;
2181+
if(drainWakeup!=null){
2182+
drainWakeup.reject(error);
2183+
drainWakeup=null;
2184+
}
2185+
}
2186+
FunctionPrototypeCall(onStopSending,stream,reason);
2187+
};
2188+
21462189
// A note on backpressure handling: per the stream/iter spec, the default
21472190
// backpressure policy for writers is strict, meaning that if the stream
21482191
// signals backpressure additional writes are rejected until the buffer has
@@ -2543,6 +2586,7 @@ class QuicStream {
25432586
inner.pendingClose.resolve=undefined;
25442587
inner.onblocked=undefined;
25452588
inner.onreset=undefined;
2589+
inner.onstopsending=undefined;
25462590
inner.onheaders=undefined;
25472591
inner.onerror=undefined;
25482592
inner.ontrailers=undefined;
@@ -2596,6 +2640,12 @@ class QuicStream {
25962640
safeCallbackInvoke(inner.onreset,this,error);
25972641
}
25982642

2643+
[kStopSending](error){
2644+
constinner=this.#inner;
2645+
assert(inner.onstopsending,'Unexpected stop sending event');
2646+
safeCallbackInvoke(inner.onstopsending,this,error);
2647+
}
2648+
25992649
[kHeaders](headers,kind){
26002650
constblock=parseHeaderPairs(headers);
26012651
constkindName=kHeadersKindName[kind]??kind;

‎lib/internal/quic/state.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
IDX_STATE_STREAM_WANTS_BLOCK,
102102
IDX_STATE_STREAM_WANTS_HEADERS,
103103
IDX_STATE_STREAM_WANTS_RESET,
104+
IDX_STATE_STREAM_WANTS_STOP_SENDING,
104105
IDX_STATE_STREAM_WANTS_TRAILERS,
105106
IDX_STATE_STREAM_RECEIVED_EARLY_DATA,
106107
IDX_STATE_STREAM_WRITE_DESIRED_SIZE,
@@ -142,6 +143,7 @@ assert(IDX_STATE_STREAM_HAS_READER !== undefined);
142143
assert(IDX_STATE_STREAM_WANTS_BLOCK!==undefined);
143144
assert(IDX_STATE_STREAM_WANTS_HEADERS!==undefined);
144145
assert(IDX_STATE_STREAM_WANTS_RESET!==undefined);
146+
assert(IDX_STATE_STREAM_WANTS_STOP_SENDING!==undefined);
145147
assert(IDX_STATE_STREAM_WANTS_TRAILERS!==undefined);
146148
assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE!==undefined);
147149
assert(IDX_STATE_STREAM_RESET_CODE!==undefined);
@@ -826,6 +828,24 @@ class QuicStreamState {
826828
DataViewPrototypeSetUint8(handle,this.#offset +IDX_STATE_STREAM_WANTS_RESET,val ? 1 : 0);
827829
}
828830

831+
/** @type {boolean} */
832+
getwantsStopSending(){
833+
consthandle=this.#handle;
834+
if(handle===undefined)returnundefined;
835+
returnDataViewPrototypeGetUint8(
836+
handle,this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING)!==0;
837+
}
838+
839+
/** @type {boolean} */
840+
setwantsStopSending(val){
841+
consthandle=this.#handle;
842+
if(handle===undefined)return;
843+
DataViewPrototypeSetUint8(
844+
handle,
845+
this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING,
846+
val ? 1 : 0);
847+
}
848+
829849
/** @type {boolean} */
830850
getwantsTrailers(){
831851
consthandle=this.#handle;
@@ -903,6 +923,7 @@ class QuicStreamState {
903923
hasReader,
904924
wantsBlock,
905925
wantsReset,
926+
wantsStopSending,
906927
wantsHeaders,
907928
wantsTrailers,
908929
early,
@@ -923,6 +944,7 @@ class QuicStreamState {
923944
hasReader,
924945
wantsBlock,
925946
wantsReset,
947+
wantsStopSending,
926948
wantsHeaders,
927949
wantsTrailers,
928950
early,
@@ -960,6 +982,7 @@ class QuicStreamState {
960982
hasReader,
961983
wantsBlock,
962984
wantsReset,
985+
wantsStopSending,
963986
wantsHeaders,
964987
wantsTrailers,
965988
early,
@@ -980,6 +1003,7 @@ class QuicStreamState {
9801003
hasReader,
9811004
wantsBlock,
9821005
wantsReset,
1006+
wantsStopSending,
9831007
wantsHeaders,
9841008
wantsTrailers,
9851009
early,

‎lib/internal/quic/symbols.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const kReset = Symbol('kReset');
5757
constkSendHeaders=Symbol('kSendHeaders');
5858
constkSessionApplication=Symbol('kSessionApplication');
5959
constkSessionTicket=Symbol('kSessionTicket');
60+
constkStopSending=Symbol('kStopSending');
6061
constkTrailers=Symbol('kTrailers');
6162
constkVersionNegotiation=Symbol('kVersionNegotiation');
6263

@@ -93,6 +94,7 @@ module.exports = {
9394
kSendHeaders,
9495
kSessionApplication,
9596
kSessionTicket,
97+
kStopSending,
9698
kTrailers,
9799
kVersionNegotiation,
98100
};

‎src/quic/bindingdata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SessionManager;
6161
V(stream_drain, StreamDrain) \
6262
V(stream_headers, StreamHeaders) \
6363
V(stream_reset, StreamReset) \
64+
V(stream_stop_sending, StreamStopSending) \
6465
V(stream_trailers, StreamTrailers)
6566

6667
// The various JS strings the implementation uses.

‎src/quic/session.cc‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,11 +1611,11 @@ struct Session::Impl final : public MemoryRetainer {
16111611
returnNGTCP2_SUCCESS;
16121612
}
16131613

1614-
staticinton_stream_stop_sending(ngtcp2_conn* conn,
1615-
stream_id stream_id,
1616-
error_code app_error_code,
1617-
void* user_data,
1618-
void* stream_user_data) {
1614+
staticinton_receive_stream_stop_sending(ngtcp2_conn* conn,
1615+
stream_id stream_id,
1616+
error_code app_error_code,
1617+
void* user_data,
1618+
void* stream_user_data) {
16191619
NGTCP2_CALLBACK_SCOPE(session)
16201620
auto* stream = Stream::From(stream_user_data);
16211621
if (stream == nullptr) returnNGTCP2_SUCCESS;
@@ -1652,7 +1652,7 @@ struct Session::Impl final : public MemoryRetainer {
16521652

16531653
staticconstexpr ngtcp2_callbacks CLIENT = {
16541654
ngtcp2_crypto_client_initial_cb,
1655-
nullptr,
1655+
nullptr,// stream_stop_sending
16561656
ngtcp2_crypto_recv_crypto_data_cb,
16571657
on_handshake_completed,
16581658
on_receive_version_negotiation,
@@ -1686,7 +1686,7 @@ struct Session::Impl final : public MemoryRetainer {
16861686
on_acknowledge_datagram,
16871687
on_lost_datagram,
16881688
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1689-
on_stream_stop_sending,
1689+
nullptr, // stream_stop_sending
16901690
ngtcp2_crypto_version_negotiation_cb,
16911691
on_receive_rx_key,
16921692
on_receive_tx_key,
@@ -1697,12 +1697,12 @@ struct Session::Impl final : public MemoryRetainer {
16971697
on_cid_status,
16981698
ngtcp2_crypto_get_path_challenge_data2_cb,
16991699
#ifdef NGTCP2_CALLBACKS_V4
1700-
nullptr,
1700+
on_receive_stream_stop_sending,
17011701
#endif
17021702
};
17031703

17041704
staticconstexpr ngtcp2_callbacks SERVER = {
1705-
nullptr,
1705+
nullptr,// stream_stop_sending
17061706
ngtcp2_crypto_recv_client_initial_cb,
17071707
ngtcp2_crypto_recv_crypto_data_cb,
17081708
on_handshake_completed,
@@ -1737,7 +1737,7 @@ struct Session::Impl final : public MemoryRetainer {
17371737
on_acknowledge_datagram,
17381738
on_lost_datagram,
17391739
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1740-
on_stream_stop_sending,
1740+
nullptr, // stream_stop_sending
17411741
ngtcp2_crypto_version_negotiation_cb,
17421742
nullptr,
17431743
on_receive_tx_key,
@@ -1748,7 +1748,7 @@ struct Session::Impl final : public MemoryRetainer {
17481748
on_cid_status,
17491749
ngtcp2_crypto_get_path_challenge_data2_cb,
17501750
#ifdef NGTCP2_CALLBACKS_V4
1751-
nullptr,
1751+
on_receive_stream_stop_sending,
17521752
#endif
17531753
};
17541754
};

‎src/quic/streams.cc‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ namespace quic {
6161
V(WANTS_HEADERS, wants_headers, uint8_t) \
6262
/* Set when the stream has a reset event handler */ \
6363
V(WANTS_RESET, wants_reset, uint8_t) \
64+
/* Set when the stream has a stop sending event handler */ \
65+
V(WANTS_STOP_SENDING, wants_stop_sending, uint8_t) \
6466
/* Set when the stream has a trailers event handler */ \
6567
V(WANTS_TRAILERS, wants_trailers, uint8_t) \
6668
/* True when 0-RTT early data was received */ \
@@ -1774,19 +1776,11 @@ void Stream::ReceiveData(const uint8_t* data,
17741776
}
17751777

17761778
voidStream::ReceiveStopSending(QuicError error) {
1777-
// STOP_SENDING from the peer asks us to stop sending. Per RFC 9000
1778-
// §3.5 the receiver SHOULD respond with RESET_STREAM, which is what
1779-
// ngtcp2_conn_shutdown_stream_write below schedules. If our
1780-
// writable side has already been shut down (e.g. we already sent
1781-
// RESET_STREAM ourselves or finished sending with FIN) there is
1782-
// nothing more to do here. The previous guard checked
1783-
// `state()->read_ended` which is unrelated to the writable side and
1784-
// suppressed STOP_SENDING handling whenever a sibling RESET_STREAM
1785-
// frame had been processed first within the same packet.
1786-
if (state()->write_ended) return;
1779+
// STOP_SENDING from the peer asks us to stop sending. The required
1780+
// RESET_STREAM response is scheduled automatically.
17871781
Debug(this, "Received stop sending with error %s", error);
1788-
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code());
17891782
EndWritable();
1783+
EmitStopSending(error);
17901784
}
17911785

17921786
voidStream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
@@ -1958,6 +1952,17 @@ void Stream::EmitReset(const QuicError& error) {
19581952
MakeCallback(BindingData::Get(env()).stream_reset_callback(), 1, &err);
19591953
}
19601954

1955+
voidStream::EmitStopSending(const QuicError& error) {
1956+
if (!env()->can_call_into_js() || !state()->wants_stop_sending) {
1957+
return;
1958+
}
1959+
CallbackScope<Stream> cb_scope(this);
1960+
Local<Value> err;
1961+
if (!error.ToV8Value(env()).ToLocal(&err)) return;
1962+
1963+
MakeCallback(BindingData::Get(env()).stream_stop_sending_callback(), 1, &err);
1964+
}
1965+
19611966
voidStream::EmitWantTrailers() {
19621967
// state()->wants_trailers will be set from the javascript side if the
19631968
// stream object has a handler for the trailers event.

‎src/quic/streams.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ class Stream final : public AsyncWrap,
417417
// Notifies the JavaScript side that the stream has been reset.
418418
voidEmitReset(const QuicError& error);
419419

420+
// Notifies the JavaScript side that the peer asked it to stop sending.
421+
voidEmitStopSending(const QuicError& error);
422+
420423
// Notifies the JavaScript side that the application is ready to receive
421424
// trailing headers. Any trailing headers must be sent immediately, and
422425
// synchronously when this callback is triggered.

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ assert.strictEqual(streamState.reset, false);
156156
assert.strictEqual(streamState.hasReader,false);
157157
assert.strictEqual(streamState.wantsBlock,false);
158158
assert.strictEqual(streamState.wantsReset,false);
159+
assert.strictEqual(streamState.wantsStopSending,false);
159160

160161
assert.strictEqual(sessionState.hasPathValidationListener,false);
161162
assert.strictEqual(sessionState.hasDatagramListener,false);

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 8a00872

Browse files
pimterryaduh95
authored andcommitted
quic: fix stop sending behaviour & callback
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #64710 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e59346b commit 8a00872

15 files changed

Lines changed: 193 additions & 164 deletions

‎doc/api/quic.md‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,8 +2013,7 @@ added: v23.8.0
20132013

20142014
The callback to invoke when the peer aborts a direction of the stream by
20152015
sending a `RESET_STREAM` frame (the peer abandons their writable side, so
2016-
no further data will arrive on our readable side) or a `STOP_SENDING`
2017-
frame (the peer asks us to stop writing on our writable side).
2016+
no further data will arrive on our readable side).
20182017

20192018
The callback receives a Node.js error whose `errorCode` (`bigint`)
20202019
property carries the application error code from the wire frame.
@@ -2025,6 +2024,21 @@ continue using the still-active direction on a bidirectional stream),
20252024
abort the other direction with [`writer.fail()`][], or tear down the
20262025
whole stream with [`stream.destroy()`][]. Read/write.
20272026

2027+
### `stream.onstopsending`
2028+
2029+
<!-- YAML
2030+
added: REPLACEME
2031+
-->
2032+
2033+
* Type: {quic.OnStreamErrorCallback}
2034+
2035+
The callback to invoke when the peer aborts a direction of the stream by
2036+
sending a `STOP_SENDING` frame (the peer asks us to stop writing on our
2037+
writable side).
2038+
2039+
The callback receives a Node.js error whose `errorCode` (`bigint`)
2040+
property carries the application error code from the wire frame. Read/write.
2041+
20282042
### `stream.headers`
20292043

20302044
<!-- YAML
@@ -3569,8 +3583,8 @@ functions. If a callback throws synchronously or returns a promise that
35693583
rejects, the error is caught and the owning session or stream is destroyed
35703584
with that error:
35713585

3572-
* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
3573-
`oninfo`, `onwanttrailers`): the stream is destroyed.
3586+
* Stream callbacks (`onblocked`, `onreset`, `onstopsending`, `onheaders`,
3587+
`ontrailers`, `oninfo`, `onwanttrailers`): the stream is destroyed.
35743588
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
35753589
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
35763590
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
@@ -4439,10 +4453,9 @@ added: REPLACEME
44394453
* `session` {quic.QuicSession}
44404454
* `error` {any} The QUIC error associated with the reset.
44414455
4442-
Published when a stream receives a STOP\_SENDING or RESET\_STREAM frame
4443-
from the peer, indicating the peer has aborted the stream. This is a
4444-
key signal for diagnosing application-level issues such as cancelled
4445-
requests.
4456+
Published when a stream receives a RESET\_STREAM frame from the peer,
4457+
indicating the peer has aborted its sending direction. This is a key signal
4458+
for diagnosing application-level issues such as cancelled requests.
44464459
44474460
### Channel: `quic.stream.blocked`
44484461

‎lib/internal/quic/quic.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
DataViewPrototypeGetByteLength,
1313
ErrorCaptureStackTrace,
1414
FunctionPrototypeBind,
15+
FunctionPrototypeCall,
1516
Number,
1617
ObjectDefineProperties,
1718
ObjectKeys,
@@ -210,6 +211,7 @@ const {
210211
kSendHeaders,
211212
kSessionApplication,
212213
kSessionTicket,
214+
kStopSending,
213215
kTrailers,
214216
kVersionNegotiation,
215217
kInspect,
@@ -991,6 +993,14 @@ setCallbacks({
991993
this[kOwner][kReset](error);
992994
},
993995

996+
onStreamStopSending(error){
997+
if(error!==undefined){
998+
error=convertQuicError(error);
999+
}
1000+
debug('stream stop sending callback',this[kOwner],error);
1001+
this[kOwner][kStopSending](error);
1002+
},
1003+
9941004
onStreamHeaders(headers,kind){
9951005
// Called when the stream C++ handle has received a full block of headers.
9961006
debug(`stream ${this[kOwner].id} headers callback`,headers,kind);
@@ -1576,6 +1586,7 @@ class QuicStream {
15761586
onerror: undefined,
15771587
onblocked: undefined,
15781588
onreset: undefined,
1589+
onstopsending: undefined,
15791590
onheaders: undefined,
15801591
ontrailers: undefined,
15811592
oninfo: undefined,
@@ -1781,6 +1792,25 @@ class QuicStream {
17811792
}
17821793
}
17831794

1795+
/** @type {OnStreamErrorCallback} */
1796+
getonstopsending(){
1797+
assertIsQuicStream(this);
1798+
returnthis.#inner.onstopsending;
1799+
}
1800+
1801+
setonstopsending(fn){
1802+
assertIsQuicStream(this);
1803+
constinner=this.#inner;
1804+
if(fn===undefined){
1805+
inner.onstopsending=undefined;
1806+
inner.state.wantsStopSending=false;
1807+
}else{
1808+
validateFunction(fn,'onstopsending');
1809+
inner.onstopsending=FunctionPrototypeBind(fn,this);
1810+
inner.state.wantsStopSending=true;
1811+
}
1812+
}
1813+
17841814
/** @type {OnHeadersCallback} */
17851815
getonheaders(){
17861816
assertIsQuicStream(this);
@@ -2143,6 +2173,19 @@ class QuicStream {
21432173
}
21442174
};
21452175

2176+
constonStopSending=stream[kStopSending];
2177+
stream[kStopSending]=(reason)=>{
2178+
if(!closed&&!errored){
2179+
errored=true;
2180+
error=reason;
2181+
if(drainWakeup!=null){
2182+
drainWakeup.reject(error);
2183+
drainWakeup=null;
2184+
}
2185+
}
2186+
FunctionPrototypeCall(onStopSending,stream,reason);
2187+
};
2188+
21462189
// A note on backpressure handling: per the stream/iter spec, the default
21472190
// backpressure policy for writers is strict, meaning that if the stream
21482191
// signals backpressure additional writes are rejected until the buffer has
@@ -2543,6 +2586,7 @@ class QuicStream {
25432586
inner.pendingClose.resolve=undefined;
25442587
inner.onblocked=undefined;
25452588
inner.onreset=undefined;
2589+
inner.onstopsending=undefined;
25462590
inner.onheaders=undefined;
25472591
inner.onerror=undefined;
25482592
inner.ontrailers=undefined;
@@ -2596,6 +2640,12 @@ class QuicStream {
25962640
safeCallbackInvoke(inner.onreset,this,error);
25972641
}
25982642

2643+
[kStopSending](error){
2644+
constinner=this.#inner;
2645+
assert(inner.onstopsending,'Unexpected stop sending event');
2646+
safeCallbackInvoke(inner.onstopsending,this,error);
2647+
}
2648+
25992649
[kHeaders](headers,kind){
26002650
constblock=parseHeaderPairs(headers);
26012651
constkindName=kHeadersKindName[kind]??kind;

‎lib/internal/quic/state.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
IDX_STATE_STREAM_WANTS_BLOCK,
102102
IDX_STATE_STREAM_WANTS_HEADERS,
103103
IDX_STATE_STREAM_WANTS_RESET,
104+
IDX_STATE_STREAM_WANTS_STOP_SENDING,
104105
IDX_STATE_STREAM_WANTS_TRAILERS,
105106
IDX_STATE_STREAM_RECEIVED_EARLY_DATA,
106107
IDX_STATE_STREAM_WRITE_DESIRED_SIZE,
@@ -142,6 +143,7 @@ assert(IDX_STATE_STREAM_HAS_READER !== undefined);
142143
assert(IDX_STATE_STREAM_WANTS_BLOCK!==undefined);
143144
assert(IDX_STATE_STREAM_WANTS_HEADERS!==undefined);
144145
assert(IDX_STATE_STREAM_WANTS_RESET!==undefined);
146+
assert(IDX_STATE_STREAM_WANTS_STOP_SENDING!==undefined);
145147
assert(IDX_STATE_STREAM_WANTS_TRAILERS!==undefined);
146148
assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE!==undefined);
147149
assert(IDX_STATE_STREAM_RESET_CODE!==undefined);
@@ -826,6 +828,24 @@ class QuicStreamState {
826828
DataViewPrototypeSetUint8(handle,this.#offset +IDX_STATE_STREAM_WANTS_RESET,val ? 1 : 0);
827829
}
828830

831+
/** @type {boolean} */
832+
getwantsStopSending(){
833+
consthandle=this.#handle;
834+
if(handle===undefined)returnundefined;
835+
returnDataViewPrototypeGetUint8(
836+
handle,this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING)!==0;
837+
}
838+
839+
/** @type {boolean} */
840+
setwantsStopSending(val){
841+
consthandle=this.#handle;
842+
if(handle===undefined)return;
843+
DataViewPrototypeSetUint8(
844+
handle,
845+
this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING,
846+
val ? 1 : 0);
847+
}
848+
829849
/** @type {boolean} */
830850
getwantsTrailers(){
831851
consthandle=this.#handle;
@@ -903,6 +923,7 @@ class QuicStreamState {
903923
hasReader,
904924
wantsBlock,
905925
wantsReset,
926+
wantsStopSending,
906927
wantsHeaders,
907928
wantsTrailers,
908929
early,
@@ -923,6 +944,7 @@ class QuicStreamState {
923944
hasReader,
924945
wantsBlock,
925946
wantsReset,
947+
wantsStopSending,
926948
wantsHeaders,
927949
wantsTrailers,
928950
early,
@@ -960,6 +982,7 @@ class QuicStreamState {
960982
hasReader,
961983
wantsBlock,
962984
wantsReset,
985+
wantsStopSending,
963986
wantsHeaders,
964987
wantsTrailers,
965988
early,
@@ -980,6 +1003,7 @@ class QuicStreamState {
9801003
hasReader,
9811004
wantsBlock,
9821005
wantsReset,
1006+
wantsStopSending,
9831007
wantsHeaders,
9841008
wantsTrailers,
9851009
early,

‎lib/internal/quic/symbols.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const kReset = Symbol('kReset');
5757
constkSendHeaders=Symbol('kSendHeaders');
5858
constkSessionApplication=Symbol('kSessionApplication');
5959
constkSessionTicket=Symbol('kSessionTicket');
60+
constkStopSending=Symbol('kStopSending');
6061
constkTrailers=Symbol('kTrailers');
6162
constkVersionNegotiation=Symbol('kVersionNegotiation');
6263

@@ -93,6 +94,7 @@ module.exports = {
9394
kSendHeaders,
9495
kSessionApplication,
9596
kSessionTicket,
97+
kStopSending,
9698
kTrailers,
9799
kVersionNegotiation,
98100
};

‎src/quic/bindingdata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SessionManager;
6161
V(stream_drain, StreamDrain) \
6262
V(stream_headers, StreamHeaders) \
6363
V(stream_reset, StreamReset) \
64+
V(stream_stop_sending, StreamStopSending) \
6465
V(stream_trailers, StreamTrailers)
6566

6667
// The various JS strings the implementation uses.

‎src/quic/session.cc‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,11 +1611,11 @@ struct Session::Impl final : public MemoryRetainer {
16111611
returnNGTCP2_SUCCESS;
16121612
}
16131613

1614-
staticinton_stream_stop_sending(ngtcp2_conn* conn,
1615-
stream_id stream_id,
1616-
error_code app_error_code,
1617-
void* user_data,
1618-
void* stream_user_data) {
1614+
staticinton_receive_stream_stop_sending(ngtcp2_conn* conn,
1615+
stream_id stream_id,
1616+
error_code app_error_code,
1617+
void* user_data,
1618+
void* stream_user_data) {
16191619
NGTCP2_CALLBACK_SCOPE(session)
16201620
auto* stream = Stream::From(stream_user_data);
16211621
if (stream == nullptr) returnNGTCP2_SUCCESS;
@@ -1652,7 +1652,7 @@ struct Session::Impl final : public MemoryRetainer {
16521652

16531653
staticconstexpr ngtcp2_callbacks CLIENT = {
16541654
ngtcp2_crypto_client_initial_cb,
1655-
nullptr,
1655+
nullptr,// stream_stop_sending
16561656
ngtcp2_crypto_recv_crypto_data_cb,
16571657
on_handshake_completed,
16581658
on_receive_version_negotiation,
@@ -1686,7 +1686,7 @@ struct Session::Impl final : public MemoryRetainer {
16861686
on_acknowledge_datagram,
16871687
on_lost_datagram,
16881688
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1689-
on_stream_stop_sending,
1689+
nullptr, // stream_stop_sending
16901690
ngtcp2_crypto_version_negotiation_cb,
16911691
on_receive_rx_key,
16921692
on_receive_tx_key,
@@ -1697,12 +1697,12 @@ struct Session::Impl final : public MemoryRetainer {
16971697
on_cid_status,
16981698
ngtcp2_crypto_get_path_challenge_data2_cb,
16991699
#ifdef NGTCP2_CALLBACKS_V4
1700-
nullptr,
1700+
on_receive_stream_stop_sending,
17011701
#endif
17021702
};
17031703

17041704
staticconstexpr ngtcp2_callbacks SERVER = {
1705-
nullptr,
1705+
nullptr,// stream_stop_sending
17061706
ngtcp2_crypto_recv_client_initial_cb,
17071707
ngtcp2_crypto_recv_crypto_data_cb,
17081708
on_handshake_completed,
@@ -1737,7 +1737,7 @@ struct Session::Impl final : public MemoryRetainer {
17371737
on_acknowledge_datagram,
17381738
on_lost_datagram,
17391739
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1740-
on_stream_stop_sending,
1740+
nullptr, // stream_stop_sending
17411741
ngtcp2_crypto_version_negotiation_cb,
17421742
nullptr,
17431743
on_receive_tx_key,
@@ -1748,7 +1748,7 @@ struct Session::Impl final : public MemoryRetainer {
17481748
on_cid_status,
17491749
ngtcp2_crypto_get_path_challenge_data2_cb,
17501750
#ifdef NGTCP2_CALLBACKS_V4
1751-
nullptr,
1751+
on_receive_stream_stop_sending,
17521752
#endif
17531753
};
17541754
};

‎src/quic/streams.cc‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ namespace quic {
6161
V(WANTS_HEADERS, wants_headers, uint8_t) \
6262
/* Set when the stream has a reset event handler */ \
6363
V(WANTS_RESET, wants_reset, uint8_t) \
64+
/* Set when the stream has a stop sending event handler */ \
65+
V(WANTS_STOP_SENDING, wants_stop_sending, uint8_t) \
6466
/* Set when the stream has a trailers event handler */ \
6567
V(WANTS_TRAILERS, wants_trailers, uint8_t) \
6668
/* True when 0-RTT early data was received */ \
@@ -1774,19 +1776,11 @@ void Stream::ReceiveData(const uint8_t* data,
17741776
}
17751777

17761778
voidStream::ReceiveStopSending(QuicError error) {
1777-
// STOP_SENDING from the peer asks us to stop sending. Per RFC 9000
1778-
// §3.5 the receiver SHOULD respond with RESET_STREAM, which is what
1779-
// ngtcp2_conn_shutdown_stream_write below schedules. If our
1780-
// writable side has already been shut down (e.g. we already sent
1781-
// RESET_STREAM ourselves or finished sending with FIN) there is
1782-
// nothing more to do here. The previous guard checked
1783-
// `state()->read_ended` which is unrelated to the writable side and
1784-
// suppressed STOP_SENDING handling whenever a sibling RESET_STREAM
1785-
// frame had been processed first within the same packet.
1786-
if (state()->write_ended) return;
1779+
// STOP_SENDING from the peer asks us to stop sending. The required
1780+
// RESET_STREAM response is scheduled automatically.
17871781
Debug(this, "Received stop sending with error %s", error);
1788-
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code());
17891782
EndWritable();
1783+
EmitStopSending(error);
17901784
}
17911785

17921786
voidStream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
@@ -1958,6 +1952,17 @@ void Stream::EmitReset(const QuicError& error) {
19581952
MakeCallback(BindingData::Get(env()).stream_reset_callback(), 1, &err);
19591953
}
19601954

1955+
voidStream::EmitStopSending(const QuicError& error) {
1956+
if (!env()->can_call_into_js() || !state()->wants_stop_sending) {
1957+
return;
1958+
}
1959+
CallbackScope<Stream> cb_scope(this);
1960+
Local<Value> err;
1961+
if (!error.ToV8Value(env()).ToLocal(&err)) return;
1962+
1963+
MakeCallback(BindingData::Get(env()).stream_stop_sending_callback(), 1, &err);
1964+
}
1965+
19611966
voidStream::EmitWantTrailers() {
19621967
// state()->wants_trailers will be set from the javascript side if the
19631968
// stream object has a handler for the trailers event.

‎src/quic/streams.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ class Stream final : public AsyncWrap,
417417
// Notifies the JavaScript side that the stream has been reset.
418418
voidEmitReset(const QuicError& error);
419419

420+
// Notifies the JavaScript side that the peer asked it to stop sending.
421+
voidEmitStopSending(const QuicError& error);
422+
420423
// Notifies the JavaScript side that the application is ready to receive
421424
// trailing headers. Any trailing headers must be sent immediately, and
422425
// synchronously when this callback is triggered.

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ assert.strictEqual(streamState.reset, false);
156156
assert.strictEqual(streamState.hasReader,false);
157157
assert.strictEqual(streamState.wantsBlock,false);
158158
assert.strictEqual(streamState.wantsReset,false);
159+
assert.strictEqual(streamState.wantsStopSending,false);
159160

160161
assert.strictEqual(sessionState.hasPathValidationListener,false);
161162
assert.strictEqual(sessionState.hasDatagramListener,false);

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 8a00872

Browse files
pimterryaduh95
authored andcommitted
quic: fix stop sending behaviour & callback
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #64710 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e59346b commit 8a00872

15 files changed

Lines changed: 193 additions & 164 deletions

‎doc/api/quic.md‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,8 +2013,7 @@ added: v23.8.0
20132013

20142014
The callback to invoke when the peer aborts a direction of the stream by
20152015
sending a `RESET_STREAM` frame (the peer abandons their writable side, so
2016-
no further data will arrive on our readable side) or a `STOP_SENDING`
2017-
frame (the peer asks us to stop writing on our writable side).
2016+
no further data will arrive on our readable side).
20182017

20192018
The callback receives a Node.js error whose `errorCode` (`bigint`)
20202019
property carries the application error code from the wire frame.
@@ -2025,6 +2024,21 @@ continue using the still-active direction on a bidirectional stream),
20252024
abort the other direction with [`writer.fail()`][], or tear down the
20262025
whole stream with [`stream.destroy()`][]. Read/write.
20272026

2027+
### `stream.onstopsending`
2028+
2029+
<!-- YAML
2030+
added: REPLACEME
2031+
-->
2032+
2033+
* Type: {quic.OnStreamErrorCallback}
2034+
2035+
The callback to invoke when the peer aborts a direction of the stream by
2036+
sending a `STOP_SENDING` frame (the peer asks us to stop writing on our
2037+
writable side).
2038+
2039+
The callback receives a Node.js error whose `errorCode` (`bigint`)
2040+
property carries the application error code from the wire frame. Read/write.
2041+
20282042
### `stream.headers`
20292043

20302044
<!-- YAML
@@ -3569,8 +3583,8 @@ functions. If a callback throws synchronously or returns a promise that
35693583
rejects, the error is caught and the owning session or stream is destroyed
35703584
with that error:
35713585

3572-
* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
3573-
`oninfo`, `onwanttrailers`): the stream is destroyed.
3586+
* Stream callbacks (`onblocked`, `onreset`, `onstopsending`, `onheaders`,
3587+
`ontrailers`, `oninfo`, `onwanttrailers`): the stream is destroyed.
35743588
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
35753589
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
35763590
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
@@ -4439,10 +4453,9 @@ added: REPLACEME
44394453
* `session` {quic.QuicSession}
44404454
* `error` {any} The QUIC error associated with the reset.
44414455
4442-
Published when a stream receives a STOP\_SENDING or RESET\_STREAM frame
4443-
from the peer, indicating the peer has aborted the stream. This is a
4444-
key signal for diagnosing application-level issues such as cancelled
4445-
requests.
4456+
Published when a stream receives a RESET\_STREAM frame from the peer,
4457+
indicating the peer has aborted its sending direction. This is a key signal
4458+
for diagnosing application-level issues such as cancelled requests.
44464459
44474460
### Channel: `quic.stream.blocked`
44484461

‎lib/internal/quic/quic.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
DataViewPrototypeGetByteLength,
1313
ErrorCaptureStackTrace,
1414
FunctionPrototypeBind,
15+
FunctionPrototypeCall,
1516
Number,
1617
ObjectDefineProperties,
1718
ObjectKeys,
@@ -210,6 +211,7 @@ const {
210211
kSendHeaders,
211212
kSessionApplication,
212213
kSessionTicket,
214+
kStopSending,
213215
kTrailers,
214216
kVersionNegotiation,
215217
kInspect,
@@ -991,6 +993,14 @@ setCallbacks({
991993
this[kOwner][kReset](error);
992994
},
993995

996+
onStreamStopSending(error){
997+
if(error!==undefined){
998+
error=convertQuicError(error);
999+
}
1000+
debug('stream stop sending callback',this[kOwner],error);
1001+
this[kOwner][kStopSending](error);
1002+
},
1003+
9941004
onStreamHeaders(headers,kind){
9951005
// Called when the stream C++ handle has received a full block of headers.
9961006
debug(`stream ${this[kOwner].id} headers callback`,headers,kind);
@@ -1576,6 +1586,7 @@ class QuicStream {
15761586
onerror: undefined,
15771587
onblocked: undefined,
15781588
onreset: undefined,
1589+
onstopsending: undefined,
15791590
onheaders: undefined,
15801591
ontrailers: undefined,
15811592
oninfo: undefined,
@@ -1781,6 +1792,25 @@ class QuicStream {
17811792
}
17821793
}
17831794

1795+
/** @type {OnStreamErrorCallback} */
1796+
getonstopsending(){
1797+
assertIsQuicStream(this);
1798+
returnthis.#inner.onstopsending;
1799+
}
1800+
1801+
setonstopsending(fn){
1802+
assertIsQuicStream(this);
1803+
constinner=this.#inner;
1804+
if(fn===undefined){
1805+
inner.onstopsending=undefined;
1806+
inner.state.wantsStopSending=false;
1807+
}else{
1808+
validateFunction(fn,'onstopsending');
1809+
inner.onstopsending=FunctionPrototypeBind(fn,this);
1810+
inner.state.wantsStopSending=true;
1811+
}
1812+
}
1813+
17841814
/** @type {OnHeadersCallback} */
17851815
getonheaders(){
17861816
assertIsQuicStream(this);
@@ -2143,6 +2173,19 @@ class QuicStream {
21432173
}
21442174
};
21452175

2176+
constonStopSending=stream[kStopSending];
2177+
stream[kStopSending]=(reason)=>{
2178+
if(!closed&&!errored){
2179+
errored=true;
2180+
error=reason;
2181+
if(drainWakeup!=null){
2182+
drainWakeup.reject(error);
2183+
drainWakeup=null;
2184+
}
2185+
}
2186+
FunctionPrototypeCall(onStopSending,stream,reason);
2187+
};
2188+
21462189
// A note on backpressure handling: per the stream/iter spec, the default
21472190
// backpressure policy for writers is strict, meaning that if the stream
21482191
// signals backpressure additional writes are rejected until the buffer has
@@ -2543,6 +2586,7 @@ class QuicStream {
25432586
inner.pendingClose.resolve=undefined;
25442587
inner.onblocked=undefined;
25452588
inner.onreset=undefined;
2589+
inner.onstopsending=undefined;
25462590
inner.onheaders=undefined;
25472591
inner.onerror=undefined;
25482592
inner.ontrailers=undefined;
@@ -2596,6 +2640,12 @@ class QuicStream {
25962640
safeCallbackInvoke(inner.onreset,this,error);
25972641
}
25982642

2643+
[kStopSending](error){
2644+
constinner=this.#inner;
2645+
assert(inner.onstopsending,'Unexpected stop sending event');
2646+
safeCallbackInvoke(inner.onstopsending,this,error);
2647+
}
2648+
25992649
[kHeaders](headers,kind){
26002650
constblock=parseHeaderPairs(headers);
26012651
constkindName=kHeadersKindName[kind]??kind;

‎lib/internal/quic/state.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
IDX_STATE_STREAM_WANTS_BLOCK,
102102
IDX_STATE_STREAM_WANTS_HEADERS,
103103
IDX_STATE_STREAM_WANTS_RESET,
104+
IDX_STATE_STREAM_WANTS_STOP_SENDING,
104105
IDX_STATE_STREAM_WANTS_TRAILERS,
105106
IDX_STATE_STREAM_RECEIVED_EARLY_DATA,
106107
IDX_STATE_STREAM_WRITE_DESIRED_SIZE,
@@ -142,6 +143,7 @@ assert(IDX_STATE_STREAM_HAS_READER !== undefined);
142143
assert(IDX_STATE_STREAM_WANTS_BLOCK!==undefined);
143144
assert(IDX_STATE_STREAM_WANTS_HEADERS!==undefined);
144145
assert(IDX_STATE_STREAM_WANTS_RESET!==undefined);
146+
assert(IDX_STATE_STREAM_WANTS_STOP_SENDING!==undefined);
145147
assert(IDX_STATE_STREAM_WANTS_TRAILERS!==undefined);
146148
assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE!==undefined);
147149
assert(IDX_STATE_STREAM_RESET_CODE!==undefined);
@@ -826,6 +828,24 @@ class QuicStreamState {
826828
DataViewPrototypeSetUint8(handle,this.#offset +IDX_STATE_STREAM_WANTS_RESET,val ? 1 : 0);
827829
}
828830

831+
/** @type {boolean} */
832+
getwantsStopSending(){
833+
consthandle=this.#handle;
834+
if(handle===undefined)returnundefined;
835+
returnDataViewPrototypeGetUint8(
836+
handle,this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING)!==0;
837+
}
838+
839+
/** @type {boolean} */
840+
setwantsStopSending(val){
841+
consthandle=this.#handle;
842+
if(handle===undefined)return;
843+
DataViewPrototypeSetUint8(
844+
handle,
845+
this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING,
846+
val ? 1 : 0);
847+
}
848+
829849
/** @type {boolean} */
830850
getwantsTrailers(){
831851
consthandle=this.#handle;
@@ -903,6 +923,7 @@ class QuicStreamState {
903923
hasReader,
904924
wantsBlock,
905925
wantsReset,
926+
wantsStopSending,
906927
wantsHeaders,
907928
wantsTrailers,
908929
early,
@@ -923,6 +944,7 @@ class QuicStreamState {
923944
hasReader,
924945
wantsBlock,
925946
wantsReset,
947+
wantsStopSending,
926948
wantsHeaders,
927949
wantsTrailers,
928950
early,
@@ -960,6 +982,7 @@ class QuicStreamState {
960982
hasReader,
961983
wantsBlock,
962984
wantsReset,
985+
wantsStopSending,
963986
wantsHeaders,
964987
wantsTrailers,
965988
early,
@@ -980,6 +1003,7 @@ class QuicStreamState {
9801003
hasReader,
9811004
wantsBlock,
9821005
wantsReset,
1006+
wantsStopSending,
9831007
wantsHeaders,
9841008
wantsTrailers,
9851009
early,

‎lib/internal/quic/symbols.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const kReset = Symbol('kReset');
5757
constkSendHeaders=Symbol('kSendHeaders');
5858
constkSessionApplication=Symbol('kSessionApplication');
5959
constkSessionTicket=Symbol('kSessionTicket');
60+
constkStopSending=Symbol('kStopSending');
6061
constkTrailers=Symbol('kTrailers');
6162
constkVersionNegotiation=Symbol('kVersionNegotiation');
6263

@@ -93,6 +94,7 @@ module.exports = {
9394
kSendHeaders,
9495
kSessionApplication,
9596
kSessionTicket,
97+
kStopSending,
9698
kTrailers,
9799
kVersionNegotiation,
98100
};

‎src/quic/bindingdata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SessionManager;
6161
V(stream_drain, StreamDrain) \
6262
V(stream_headers, StreamHeaders) \
6363
V(stream_reset, StreamReset) \
64+
V(stream_stop_sending, StreamStopSending) \
6465
V(stream_trailers, StreamTrailers)
6566

6667
// The various JS strings the implementation uses.

‎src/quic/session.cc‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,11 +1611,11 @@ struct Session::Impl final : public MemoryRetainer {
16111611
returnNGTCP2_SUCCESS;
16121612
}
16131613

1614-
staticinton_stream_stop_sending(ngtcp2_conn* conn,
1615-
stream_id stream_id,
1616-
error_code app_error_code,
1617-
void* user_data,
1618-
void* stream_user_data) {
1614+
staticinton_receive_stream_stop_sending(ngtcp2_conn* conn,
1615+
stream_id stream_id,
1616+
error_code app_error_code,
1617+
void* user_data,
1618+
void* stream_user_data) {
16191619
NGTCP2_CALLBACK_SCOPE(session)
16201620
auto* stream = Stream::From(stream_user_data);
16211621
if (stream == nullptr) returnNGTCP2_SUCCESS;
@@ -1652,7 +1652,7 @@ struct Session::Impl final : public MemoryRetainer {
16521652

16531653
staticconstexpr ngtcp2_callbacks CLIENT = {
16541654
ngtcp2_crypto_client_initial_cb,
1655-
nullptr,
1655+
nullptr,// stream_stop_sending
16561656
ngtcp2_crypto_recv_crypto_data_cb,
16571657
on_handshake_completed,
16581658
on_receive_version_negotiation,
@@ -1686,7 +1686,7 @@ struct Session::Impl final : public MemoryRetainer {
16861686
on_acknowledge_datagram,
16871687
on_lost_datagram,
16881688
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1689-
on_stream_stop_sending,
1689+
nullptr, // stream_stop_sending
16901690
ngtcp2_crypto_version_negotiation_cb,
16911691
on_receive_rx_key,
16921692
on_receive_tx_key,
@@ -1697,12 +1697,12 @@ struct Session::Impl final : public MemoryRetainer {
16971697
on_cid_status,
16981698
ngtcp2_crypto_get_path_challenge_data2_cb,
16991699
#ifdef NGTCP2_CALLBACKS_V4
1700-
nullptr,
1700+
on_receive_stream_stop_sending,
17011701
#endif
17021702
};
17031703

17041704
staticconstexpr ngtcp2_callbacks SERVER = {
1705-
nullptr,
1705+
nullptr,// stream_stop_sending
17061706
ngtcp2_crypto_recv_client_initial_cb,
17071707
ngtcp2_crypto_recv_crypto_data_cb,
17081708
on_handshake_completed,
@@ -1737,7 +1737,7 @@ struct Session::Impl final : public MemoryRetainer {
17371737
on_acknowledge_datagram,
17381738
on_lost_datagram,
17391739
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1740-
on_stream_stop_sending,
1740+
nullptr, // stream_stop_sending
17411741
ngtcp2_crypto_version_negotiation_cb,
17421742
nullptr,
17431743
on_receive_tx_key,
@@ -1748,7 +1748,7 @@ struct Session::Impl final : public MemoryRetainer {
17481748
on_cid_status,
17491749
ngtcp2_crypto_get_path_challenge_data2_cb,
17501750
#ifdef NGTCP2_CALLBACKS_V4
1751-
nullptr,
1751+
on_receive_stream_stop_sending,
17521752
#endif
17531753
};
17541754
};

‎src/quic/streams.cc‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ namespace quic {
6161
V(WANTS_HEADERS, wants_headers, uint8_t) \
6262
/* Set when the stream has a reset event handler */ \
6363
V(WANTS_RESET, wants_reset, uint8_t) \
64+
/* Set when the stream has a stop sending event handler */ \
65+
V(WANTS_STOP_SENDING, wants_stop_sending, uint8_t) \
6466
/* Set when the stream has a trailers event handler */ \
6567
V(WANTS_TRAILERS, wants_trailers, uint8_t) \
6668
/* True when 0-RTT early data was received */ \
@@ -1774,19 +1776,11 @@ void Stream::ReceiveData(const uint8_t* data,
17741776
}
17751777

17761778
voidStream::ReceiveStopSending(QuicError error) {
1777-
// STOP_SENDING from the peer asks us to stop sending. Per RFC 9000
1778-
// §3.5 the receiver SHOULD respond with RESET_STREAM, which is what
1779-
// ngtcp2_conn_shutdown_stream_write below schedules. If our
1780-
// writable side has already been shut down (e.g. we already sent
1781-
// RESET_STREAM ourselves or finished sending with FIN) there is
1782-
// nothing more to do here. The previous guard checked
1783-
// `state()->read_ended` which is unrelated to the writable side and
1784-
// suppressed STOP_SENDING handling whenever a sibling RESET_STREAM
1785-
// frame had been processed first within the same packet.
1786-
if (state()->write_ended) return;
1779+
// STOP_SENDING from the peer asks us to stop sending. The required
1780+
// RESET_STREAM response is scheduled automatically.
17871781
Debug(this, "Received stop sending with error %s", error);
1788-
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code());
17891782
EndWritable();
1783+
EmitStopSending(error);
17901784
}
17911785

17921786
voidStream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
@@ -1958,6 +1952,17 @@ void Stream::EmitReset(const QuicError& error) {
19581952
MakeCallback(BindingData::Get(env()).stream_reset_callback(), 1, &err);
19591953
}
19601954

1955+
voidStream::EmitStopSending(const QuicError& error) {
1956+
if (!env()->can_call_into_js() || !state()->wants_stop_sending) {
1957+
return;
1958+
}
1959+
CallbackScope<Stream> cb_scope(this);
1960+
Local<Value> err;
1961+
if (!error.ToV8Value(env()).ToLocal(&err)) return;
1962+
1963+
MakeCallback(BindingData::Get(env()).stream_stop_sending_callback(), 1, &err);
1964+
}
1965+
19611966
voidStream::EmitWantTrailers() {
19621967
// state()->wants_trailers will be set from the javascript side if the
19631968
// stream object has a handler for the trailers event.

‎src/quic/streams.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ class Stream final : public AsyncWrap,
417417
// Notifies the JavaScript side that the stream has been reset.
418418
voidEmitReset(const QuicError& error);
419419

420+
// Notifies the JavaScript side that the peer asked it to stop sending.
421+
voidEmitStopSending(const QuicError& error);
422+
420423
// Notifies the JavaScript side that the application is ready to receive
421424
// trailing headers. Any trailing headers must be sent immediately, and
422425
// synchronously when this callback is triggered.

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ assert.strictEqual(streamState.reset, false);
156156
assert.strictEqual(streamState.hasReader,false);
157157
assert.strictEqual(streamState.wantsBlock,false);
158158
assert.strictEqual(streamState.wantsReset,false);
159+
assert.strictEqual(streamState.wantsStopSending,false);
159160

160161
assert.strictEqual(sessionState.hasPathValidationListener,false);
161162
assert.strictEqual(sessionState.hasDatagramListener,false);

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 8a00872

Browse files
pimterryaduh95
authored andcommitted
quic: fix stop sending behaviour & callback
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #64710 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e59346b commit 8a00872

15 files changed

Lines changed: 193 additions & 164 deletions

‎doc/api/quic.md‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,8 +2013,7 @@ added: v23.8.0
20132013

20142014
The callback to invoke when the peer aborts a direction of the stream by
20152015
sending a `RESET_STREAM` frame (the peer abandons their writable side, so
2016-
no further data will arrive on our readable side) or a `STOP_SENDING`
2017-
frame (the peer asks us to stop writing on our writable side).
2016+
no further data will arrive on our readable side).
20182017

20192018
The callback receives a Node.js error whose `errorCode` (`bigint`)
20202019
property carries the application error code from the wire frame.
@@ -2025,6 +2024,21 @@ continue using the still-active direction on a bidirectional stream),
20252024
abort the other direction with [`writer.fail()`][], or tear down the
20262025
whole stream with [`stream.destroy()`][]. Read/write.
20272026

2027+
### `stream.onstopsending`
2028+
2029+
<!-- YAML
2030+
added: REPLACEME
2031+
-->
2032+
2033+
* Type: {quic.OnStreamErrorCallback}
2034+
2035+
The callback to invoke when the peer aborts a direction of the stream by
2036+
sending a `STOP_SENDING` frame (the peer asks us to stop writing on our
2037+
writable side).
2038+
2039+
The callback receives a Node.js error whose `errorCode` (`bigint`)
2040+
property carries the application error code from the wire frame. Read/write.
2041+
20282042
### `stream.headers`
20292043

20302044
<!-- YAML
@@ -3569,8 +3583,8 @@ functions. If a callback throws synchronously or returns a promise that
35693583
rejects, the error is caught and the owning session or stream is destroyed
35703584
with that error:
35713585

3572-
* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
3573-
`oninfo`, `onwanttrailers`): the stream is destroyed.
3586+
* Stream callbacks (`onblocked`, `onreset`, `onstopsending`, `onheaders`,
3587+
`ontrailers`, `oninfo`, `onwanttrailers`): the stream is destroyed.
35743588
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
35753589
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
35763590
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
@@ -4439,10 +4453,9 @@ added: REPLACEME
44394453
* `session` {quic.QuicSession}
44404454
* `error` {any} The QUIC error associated with the reset.
44414455
4442-
Published when a stream receives a STOP\_SENDING or RESET\_STREAM frame
4443-
from the peer, indicating the peer has aborted the stream. This is a
4444-
key signal for diagnosing application-level issues such as cancelled
4445-
requests.
4456+
Published when a stream receives a RESET\_STREAM frame from the peer,
4457+
indicating the peer has aborted its sending direction. This is a key signal
4458+
for diagnosing application-level issues such as cancelled requests.
44464459
44474460
### Channel: `quic.stream.blocked`
44484461

‎lib/internal/quic/quic.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
DataViewPrototypeGetByteLength,
1313
ErrorCaptureStackTrace,
1414
FunctionPrototypeBind,
15+
FunctionPrototypeCall,
1516
Number,
1617
ObjectDefineProperties,
1718
ObjectKeys,
@@ -210,6 +211,7 @@ const {
210211
kSendHeaders,
211212
kSessionApplication,
212213
kSessionTicket,
214+
kStopSending,
213215
kTrailers,
214216
kVersionNegotiation,
215217
kInspect,
@@ -991,6 +993,14 @@ setCallbacks({
991993
this[kOwner][kReset](error);
992994
},
993995

996+
onStreamStopSending(error){
997+
if(error!==undefined){
998+
error=convertQuicError(error);
999+
}
1000+
debug('stream stop sending callback',this[kOwner],error);
1001+
this[kOwner][kStopSending](error);
1002+
},
1003+
9941004
onStreamHeaders(headers,kind){
9951005
// Called when the stream C++ handle has received a full block of headers.
9961006
debug(`stream ${this[kOwner].id} headers callback`,headers,kind);
@@ -1576,6 +1586,7 @@ class QuicStream {
15761586
onerror: undefined,
15771587
onblocked: undefined,
15781588
onreset: undefined,
1589+
onstopsending: undefined,
15791590
onheaders: undefined,
15801591
ontrailers: undefined,
15811592
oninfo: undefined,
@@ -1781,6 +1792,25 @@ class QuicStream {
17811792
}
17821793
}
17831794

1795+
/** @type {OnStreamErrorCallback} */
1796+
getonstopsending(){
1797+
assertIsQuicStream(this);
1798+
returnthis.#inner.onstopsending;
1799+
}
1800+
1801+
setonstopsending(fn){
1802+
assertIsQuicStream(this);
1803+
constinner=this.#inner;
1804+
if(fn===undefined){
1805+
inner.onstopsending=undefined;
1806+
inner.state.wantsStopSending=false;
1807+
}else{
1808+
validateFunction(fn,'onstopsending');
1809+
inner.onstopsending=FunctionPrototypeBind(fn,this);
1810+
inner.state.wantsStopSending=true;
1811+
}
1812+
}
1813+
17841814
/** @type {OnHeadersCallback} */
17851815
getonheaders(){
17861816
assertIsQuicStream(this);
@@ -2143,6 +2173,19 @@ class QuicStream {
21432173
}
21442174
};
21452175

2176+
constonStopSending=stream[kStopSending];
2177+
stream[kStopSending]=(reason)=>{
2178+
if(!closed&&!errored){
2179+
errored=true;
2180+
error=reason;
2181+
if(drainWakeup!=null){
2182+
drainWakeup.reject(error);
2183+
drainWakeup=null;
2184+
}
2185+
}
2186+
FunctionPrototypeCall(onStopSending,stream,reason);
2187+
};
2188+
21462189
// A note on backpressure handling: per the stream/iter spec, the default
21472190
// backpressure policy for writers is strict, meaning that if the stream
21482191
// signals backpressure additional writes are rejected until the buffer has
@@ -2543,6 +2586,7 @@ class QuicStream {
25432586
inner.pendingClose.resolve=undefined;
25442587
inner.onblocked=undefined;
25452588
inner.onreset=undefined;
2589+
inner.onstopsending=undefined;
25462590
inner.onheaders=undefined;
25472591
inner.onerror=undefined;
25482592
inner.ontrailers=undefined;
@@ -2596,6 +2640,12 @@ class QuicStream {
25962640
safeCallbackInvoke(inner.onreset,this,error);
25972641
}
25982642

2643+
[kStopSending](error){
2644+
constinner=this.#inner;
2645+
assert(inner.onstopsending,'Unexpected stop sending event');
2646+
safeCallbackInvoke(inner.onstopsending,this,error);
2647+
}
2648+
25992649
[kHeaders](headers,kind){
26002650
constblock=parseHeaderPairs(headers);
26012651
constkindName=kHeadersKindName[kind]??kind;

‎lib/internal/quic/state.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
IDX_STATE_STREAM_WANTS_BLOCK,
102102
IDX_STATE_STREAM_WANTS_HEADERS,
103103
IDX_STATE_STREAM_WANTS_RESET,
104+
IDX_STATE_STREAM_WANTS_STOP_SENDING,
104105
IDX_STATE_STREAM_WANTS_TRAILERS,
105106
IDX_STATE_STREAM_RECEIVED_EARLY_DATA,
106107
IDX_STATE_STREAM_WRITE_DESIRED_SIZE,
@@ -142,6 +143,7 @@ assert(IDX_STATE_STREAM_HAS_READER !== undefined);
142143
assert(IDX_STATE_STREAM_WANTS_BLOCK!==undefined);
143144
assert(IDX_STATE_STREAM_WANTS_HEADERS!==undefined);
144145
assert(IDX_STATE_STREAM_WANTS_RESET!==undefined);
146+
assert(IDX_STATE_STREAM_WANTS_STOP_SENDING!==undefined);
145147
assert(IDX_STATE_STREAM_WANTS_TRAILERS!==undefined);
146148
assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE!==undefined);
147149
assert(IDX_STATE_STREAM_RESET_CODE!==undefined);
@@ -826,6 +828,24 @@ class QuicStreamState {
826828
DataViewPrototypeSetUint8(handle,this.#offset +IDX_STATE_STREAM_WANTS_RESET,val ? 1 : 0);
827829
}
828830

831+
/** @type {boolean} */
832+
getwantsStopSending(){
833+
consthandle=this.#handle;
834+
if(handle===undefined)returnundefined;
835+
returnDataViewPrototypeGetUint8(
836+
handle,this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING)!==0;
837+
}
838+
839+
/** @type {boolean} */
840+
setwantsStopSending(val){
841+
consthandle=this.#handle;
842+
if(handle===undefined)return;
843+
DataViewPrototypeSetUint8(
844+
handle,
845+
this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING,
846+
val ? 1 : 0);
847+
}
848+
829849
/** @type {boolean} */
830850
getwantsTrailers(){
831851
consthandle=this.#handle;
@@ -903,6 +923,7 @@ class QuicStreamState {
903923
hasReader,
904924
wantsBlock,
905925
wantsReset,
926+
wantsStopSending,
906927
wantsHeaders,
907928
wantsTrailers,
908929
early,
@@ -923,6 +944,7 @@ class QuicStreamState {
923944
hasReader,
924945
wantsBlock,
925946
wantsReset,
947+
wantsStopSending,
926948
wantsHeaders,
927949
wantsTrailers,
928950
early,
@@ -960,6 +982,7 @@ class QuicStreamState {
960982
hasReader,
961983
wantsBlock,
962984
wantsReset,
985+
wantsStopSending,
963986
wantsHeaders,
964987
wantsTrailers,
965988
early,
@@ -980,6 +1003,7 @@ class QuicStreamState {
9801003
hasReader,
9811004
wantsBlock,
9821005
wantsReset,
1006+
wantsStopSending,
9831007
wantsHeaders,
9841008
wantsTrailers,
9851009
early,

‎lib/internal/quic/symbols.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const kReset = Symbol('kReset');
5757
constkSendHeaders=Symbol('kSendHeaders');
5858
constkSessionApplication=Symbol('kSessionApplication');
5959
constkSessionTicket=Symbol('kSessionTicket');
60+
constkStopSending=Symbol('kStopSending');
6061
constkTrailers=Symbol('kTrailers');
6162
constkVersionNegotiation=Symbol('kVersionNegotiation');
6263

@@ -93,6 +94,7 @@ module.exports = {
9394
kSendHeaders,
9495
kSessionApplication,
9596
kSessionTicket,
97+
kStopSending,
9698
kTrailers,
9799
kVersionNegotiation,
98100
};

‎src/quic/bindingdata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SessionManager;
6161
V(stream_drain, StreamDrain) \
6262
V(stream_headers, StreamHeaders) \
6363
V(stream_reset, StreamReset) \
64+
V(stream_stop_sending, StreamStopSending) \
6465
V(stream_trailers, StreamTrailers)
6566

6667
// The various JS strings the implementation uses.

‎src/quic/session.cc‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,11 +1611,11 @@ struct Session::Impl final : public MemoryRetainer {
16111611
returnNGTCP2_SUCCESS;
16121612
}
16131613

1614-
staticinton_stream_stop_sending(ngtcp2_conn* conn,
1615-
stream_id stream_id,
1616-
error_code app_error_code,
1617-
void* user_data,
1618-
void* stream_user_data) {
1614+
staticinton_receive_stream_stop_sending(ngtcp2_conn* conn,
1615+
stream_id stream_id,
1616+
error_code app_error_code,
1617+
void* user_data,
1618+
void* stream_user_data) {
16191619
NGTCP2_CALLBACK_SCOPE(session)
16201620
auto* stream = Stream::From(stream_user_data);
16211621
if (stream == nullptr) returnNGTCP2_SUCCESS;
@@ -1652,7 +1652,7 @@ struct Session::Impl final : public MemoryRetainer {
16521652

16531653
staticconstexpr ngtcp2_callbacks CLIENT = {
16541654
ngtcp2_crypto_client_initial_cb,
1655-
nullptr,
1655+
nullptr,// stream_stop_sending
16561656
ngtcp2_crypto_recv_crypto_data_cb,
16571657
on_handshake_completed,
16581658
on_receive_version_negotiation,
@@ -1686,7 +1686,7 @@ struct Session::Impl final : public MemoryRetainer {
16861686
on_acknowledge_datagram,
16871687
on_lost_datagram,
16881688
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1689-
on_stream_stop_sending,
1689+
nullptr, // stream_stop_sending
16901690
ngtcp2_crypto_version_negotiation_cb,
16911691
on_receive_rx_key,
16921692
on_receive_tx_key,
@@ -1697,12 +1697,12 @@ struct Session::Impl final : public MemoryRetainer {
16971697
on_cid_status,
16981698
ngtcp2_crypto_get_path_challenge_data2_cb,
16991699
#ifdef NGTCP2_CALLBACKS_V4
1700-
nullptr,
1700+
on_receive_stream_stop_sending,
17011701
#endif
17021702
};
17031703

17041704
staticconstexpr ngtcp2_callbacks SERVER = {
1705-
nullptr,
1705+
nullptr,// stream_stop_sending
17061706
ngtcp2_crypto_recv_client_initial_cb,
17071707
ngtcp2_crypto_recv_crypto_data_cb,
17081708
on_handshake_completed,
@@ -1737,7 +1737,7 @@ struct Session::Impl final : public MemoryRetainer {
17371737
on_acknowledge_datagram,
17381738
on_lost_datagram,
17391739
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1740-
on_stream_stop_sending,
1740+
nullptr, // stream_stop_sending
17411741
ngtcp2_crypto_version_negotiation_cb,
17421742
nullptr,
17431743
on_receive_tx_key,
@@ -1748,7 +1748,7 @@ struct Session::Impl final : public MemoryRetainer {
17481748
on_cid_status,
17491749
ngtcp2_crypto_get_path_challenge_data2_cb,
17501750
#ifdef NGTCP2_CALLBACKS_V4
1751-
nullptr,
1751+
on_receive_stream_stop_sending,
17521752
#endif
17531753
};
17541754
};

‎src/quic/streams.cc‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ namespace quic {
6161
V(WANTS_HEADERS, wants_headers, uint8_t) \
6262
/* Set when the stream has a reset event handler */ \
6363
V(WANTS_RESET, wants_reset, uint8_t) \
64+
/* Set when the stream has a stop sending event handler */ \
65+
V(WANTS_STOP_SENDING, wants_stop_sending, uint8_t) \
6466
/* Set when the stream has a trailers event handler */ \
6567
V(WANTS_TRAILERS, wants_trailers, uint8_t) \
6668
/* True when 0-RTT early data was received */ \
@@ -1774,19 +1776,11 @@ void Stream::ReceiveData(const uint8_t* data,
17741776
}
17751777

17761778
voidStream::ReceiveStopSending(QuicError error) {
1777-
// STOP_SENDING from the peer asks us to stop sending. Per RFC 9000
1778-
// §3.5 the receiver SHOULD respond with RESET_STREAM, which is what
1779-
// ngtcp2_conn_shutdown_stream_write below schedules. If our
1780-
// writable side has already been shut down (e.g. we already sent
1781-
// RESET_STREAM ourselves or finished sending with FIN) there is
1782-
// nothing more to do here. The previous guard checked
1783-
// `state()->read_ended` which is unrelated to the writable side and
1784-
// suppressed STOP_SENDING handling whenever a sibling RESET_STREAM
1785-
// frame had been processed first within the same packet.
1786-
if (state()->write_ended) return;
1779+
// STOP_SENDING from the peer asks us to stop sending. The required
1780+
// RESET_STREAM response is scheduled automatically.
17871781
Debug(this, "Received stop sending with error %s", error);
1788-
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code());
17891782
EndWritable();
1783+
EmitStopSending(error);
17901784
}
17911785

17921786
voidStream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
@@ -1958,6 +1952,17 @@ void Stream::EmitReset(const QuicError& error) {
19581952
MakeCallback(BindingData::Get(env()).stream_reset_callback(), 1, &err);
19591953
}
19601954

1955+
voidStream::EmitStopSending(const QuicError& error) {
1956+
if (!env()->can_call_into_js() || !state()->wants_stop_sending) {
1957+
return;
1958+
}
1959+
CallbackScope<Stream> cb_scope(this);
1960+
Local<Value> err;
1961+
if (!error.ToV8Value(env()).ToLocal(&err)) return;
1962+
1963+
MakeCallback(BindingData::Get(env()).stream_stop_sending_callback(), 1, &err);
1964+
}
1965+
19611966
voidStream::EmitWantTrailers() {
19621967
// state()->wants_trailers will be set from the javascript side if the
19631968
// stream object has a handler for the trailers event.

‎src/quic/streams.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ class Stream final : public AsyncWrap,
417417
// Notifies the JavaScript side that the stream has been reset.
418418
voidEmitReset(const QuicError& error);
419419

420+
// Notifies the JavaScript side that the peer asked it to stop sending.
421+
voidEmitStopSending(const QuicError& error);
422+
420423
// Notifies the JavaScript side that the application is ready to receive
421424
// trailing headers. Any trailing headers must be sent immediately, and
422425
// synchronously when this callback is triggered.

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ assert.strictEqual(streamState.reset, false);
156156
assert.strictEqual(streamState.hasReader,false);
157157
assert.strictEqual(streamState.wantsBlock,false);
158158
assert.strictEqual(streamState.wantsReset,false);
159+
assert.strictEqual(streamState.wantsStopSending,false);
159160

160161
assert.strictEqual(sessionState.hasPathValidationListener,false);
161162
assert.strictEqual(sessionState.hasDatagramListener,false);

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 8a00872

Browse files
pimterryaduh95
authored andcommitted
quic: fix stop sending behaviour & callback
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #64710 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent e59346b commit 8a00872

15 files changed

Lines changed: 193 additions & 164 deletions

‎doc/api/quic.md‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,8 +2013,7 @@ added: v23.8.0
20132013

20142014
The callback to invoke when the peer aborts a direction of the stream by
20152015
sending a `RESET_STREAM` frame (the peer abandons their writable side, so
2016-
no further data will arrive on our readable side) or a `STOP_SENDING`
2017-
frame (the peer asks us to stop writing on our writable side).
2016+
no further data will arrive on our readable side).
20182017

20192018
The callback receives a Node.js error whose `errorCode` (`bigint`)
20202019
property carries the application error code from the wire frame.
@@ -2025,6 +2024,21 @@ continue using the still-active direction on a bidirectional stream),
20252024
abort the other direction with [`writer.fail()`][], or tear down the
20262025
whole stream with [`stream.destroy()`][]. Read/write.
20272026

2027+
### `stream.onstopsending`
2028+
2029+
<!-- YAML
2030+
added: REPLACEME
2031+
-->
2032+
2033+
* Type: {quic.OnStreamErrorCallback}
2034+
2035+
The callback to invoke when the peer aborts a direction of the stream by
2036+
sending a `STOP_SENDING` frame (the peer asks us to stop writing on our
2037+
writable side).
2038+
2039+
The callback receives a Node.js error whose `errorCode` (`bigint`)
2040+
property carries the application error code from the wire frame. Read/write.
2041+
20282042
### `stream.headers`
20292043

20302044
<!-- YAML
@@ -3569,8 +3583,8 @@ functions. If a callback throws synchronously or returns a promise that
35693583
rejects, the error is caught and the owning session or stream is destroyed
35703584
with that error:
35713585

3572-
* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
3573-
`oninfo`, `onwanttrailers`): the stream is destroyed.
3586+
* Stream callbacks (`onblocked`, `onreset`, `onstopsending`, `onheaders`,
3587+
`ontrailers`, `oninfo`, `onwanttrailers`): the stream is destroyed.
35743588
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
35753589
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
35763590
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
@@ -4439,10 +4453,9 @@ added: REPLACEME
44394453
* `session` {quic.QuicSession}
44404454
* `error` {any} The QUIC error associated with the reset.
44414455
4442-
Published when a stream receives a STOP\_SENDING or RESET\_STREAM frame
4443-
from the peer, indicating the peer has aborted the stream. This is a
4444-
key signal for diagnosing application-level issues such as cancelled
4445-
requests.
4456+
Published when a stream receives a RESET\_STREAM frame from the peer,
4457+
indicating the peer has aborted its sending direction. This is a key signal
4458+
for diagnosing application-level issues such as cancelled requests.
44464459
44474460
### Channel: `quic.stream.blocked`
44484461

‎lib/internal/quic/quic.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
DataViewPrototypeGetByteLength,
1313
ErrorCaptureStackTrace,
1414
FunctionPrototypeBind,
15+
FunctionPrototypeCall,
1516
Number,
1617
ObjectDefineProperties,
1718
ObjectKeys,
@@ -210,6 +211,7 @@ const {
210211
kSendHeaders,
211212
kSessionApplication,
212213
kSessionTicket,
214+
kStopSending,
213215
kTrailers,
214216
kVersionNegotiation,
215217
kInspect,
@@ -991,6 +993,14 @@ setCallbacks({
991993
this[kOwner][kReset](error);
992994
},
993995

996+
onStreamStopSending(error){
997+
if(error!==undefined){
998+
error=convertQuicError(error);
999+
}
1000+
debug('stream stop sending callback',this[kOwner],error);
1001+
this[kOwner][kStopSending](error);
1002+
},
1003+
9941004
onStreamHeaders(headers,kind){
9951005
// Called when the stream C++ handle has received a full block of headers.
9961006
debug(`stream ${this[kOwner].id} headers callback`,headers,kind);
@@ -1576,6 +1586,7 @@ class QuicStream {
15761586
onerror: undefined,
15771587
onblocked: undefined,
15781588
onreset: undefined,
1589+
onstopsending: undefined,
15791590
onheaders: undefined,
15801591
ontrailers: undefined,
15811592
oninfo: undefined,
@@ -1781,6 +1792,25 @@ class QuicStream {
17811792
}
17821793
}
17831794

1795+
/** @type {OnStreamErrorCallback} */
1796+
getonstopsending(){
1797+
assertIsQuicStream(this);
1798+
returnthis.#inner.onstopsending;
1799+
}
1800+
1801+
setonstopsending(fn){
1802+
assertIsQuicStream(this);
1803+
constinner=this.#inner;
1804+
if(fn===undefined){
1805+
inner.onstopsending=undefined;
1806+
inner.state.wantsStopSending=false;
1807+
}else{
1808+
validateFunction(fn,'onstopsending');
1809+
inner.onstopsending=FunctionPrototypeBind(fn,this);
1810+
inner.state.wantsStopSending=true;
1811+
}
1812+
}
1813+
17841814
/** @type {OnHeadersCallback} */
17851815
getonheaders(){
17861816
assertIsQuicStream(this);
@@ -2143,6 +2173,19 @@ class QuicStream {
21432173
}
21442174
};
21452175

2176+
constonStopSending=stream[kStopSending];
2177+
stream[kStopSending]=(reason)=>{
2178+
if(!closed&&!errored){
2179+
errored=true;
2180+
error=reason;
2181+
if(drainWakeup!=null){
2182+
drainWakeup.reject(error);
2183+
drainWakeup=null;
2184+
}
2185+
}
2186+
FunctionPrototypeCall(onStopSending,stream,reason);
2187+
};
2188+
21462189
// A note on backpressure handling: per the stream/iter spec, the default
21472190
// backpressure policy for writers is strict, meaning that if the stream
21482191
// signals backpressure additional writes are rejected until the buffer has
@@ -2543,6 +2586,7 @@ class QuicStream {
25432586
inner.pendingClose.resolve=undefined;
25442587
inner.onblocked=undefined;
25452588
inner.onreset=undefined;
2589+
inner.onstopsending=undefined;
25462590
inner.onheaders=undefined;
25472591
inner.onerror=undefined;
25482592
inner.ontrailers=undefined;
@@ -2596,6 +2640,12 @@ class QuicStream {
25962640
safeCallbackInvoke(inner.onreset,this,error);
25972641
}
25982642

2643+
[kStopSending](error){
2644+
constinner=this.#inner;
2645+
assert(inner.onstopsending,'Unexpected stop sending event');
2646+
safeCallbackInvoke(inner.onstopsending,this,error);
2647+
}
2648+
25992649
[kHeaders](headers,kind){
26002650
constblock=parseHeaderPairs(headers);
26012651
constkindName=kHeadersKindName[kind]??kind;

‎lib/internal/quic/state.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
IDX_STATE_STREAM_WANTS_BLOCK,
102102
IDX_STATE_STREAM_WANTS_HEADERS,
103103
IDX_STATE_STREAM_WANTS_RESET,
104+
IDX_STATE_STREAM_WANTS_STOP_SENDING,
104105
IDX_STATE_STREAM_WANTS_TRAILERS,
105106
IDX_STATE_STREAM_RECEIVED_EARLY_DATA,
106107
IDX_STATE_STREAM_WRITE_DESIRED_SIZE,
@@ -142,6 +143,7 @@ assert(IDX_STATE_STREAM_HAS_READER !== undefined);
142143
assert(IDX_STATE_STREAM_WANTS_BLOCK!==undefined);
143144
assert(IDX_STATE_STREAM_WANTS_HEADERS!==undefined);
144145
assert(IDX_STATE_STREAM_WANTS_RESET!==undefined);
146+
assert(IDX_STATE_STREAM_WANTS_STOP_SENDING!==undefined);
145147
assert(IDX_STATE_STREAM_WANTS_TRAILERS!==undefined);
146148
assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE!==undefined);
147149
assert(IDX_STATE_STREAM_RESET_CODE!==undefined);
@@ -826,6 +828,24 @@ class QuicStreamState {
826828
DataViewPrototypeSetUint8(handle,this.#offset +IDX_STATE_STREAM_WANTS_RESET,val ? 1 : 0);
827829
}
828830

831+
/** @type {boolean} */
832+
getwantsStopSending(){
833+
consthandle=this.#handle;
834+
if(handle===undefined)returnundefined;
835+
returnDataViewPrototypeGetUint8(
836+
handle,this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING)!==0;
837+
}
838+
839+
/** @type {boolean} */
840+
setwantsStopSending(val){
841+
consthandle=this.#handle;
842+
if(handle===undefined)return;
843+
DataViewPrototypeSetUint8(
844+
handle,
845+
this.#offset +IDX_STATE_STREAM_WANTS_STOP_SENDING,
846+
val ? 1 : 0);
847+
}
848+
829849
/** @type {boolean} */
830850
getwantsTrailers(){
831851
consthandle=this.#handle;
@@ -903,6 +923,7 @@ class QuicStreamState {
903923
hasReader,
904924
wantsBlock,
905925
wantsReset,
926+
wantsStopSending,
906927
wantsHeaders,
907928
wantsTrailers,
908929
early,
@@ -923,6 +944,7 @@ class QuicStreamState {
923944
hasReader,
924945
wantsBlock,
925946
wantsReset,
947+
wantsStopSending,
926948
wantsHeaders,
927949
wantsTrailers,
928950
early,
@@ -960,6 +982,7 @@ class QuicStreamState {
960982
hasReader,
961983
wantsBlock,
962984
wantsReset,
985+
wantsStopSending,
963986
wantsHeaders,
964987
wantsTrailers,
965988
early,
@@ -980,6 +1003,7 @@ class QuicStreamState {
9801003
hasReader,
9811004
wantsBlock,
9821005
wantsReset,
1006+
wantsStopSending,
9831007
wantsHeaders,
9841008
wantsTrailers,
9851009
early,

‎lib/internal/quic/symbols.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const kReset = Symbol('kReset');
5757
constkSendHeaders=Symbol('kSendHeaders');
5858
constkSessionApplication=Symbol('kSessionApplication');
5959
constkSessionTicket=Symbol('kSessionTicket');
60+
constkStopSending=Symbol('kStopSending');
6061
constkTrailers=Symbol('kTrailers');
6162
constkVersionNegotiation=Symbol('kVersionNegotiation');
6263

@@ -93,6 +94,7 @@ module.exports = {
9394
kSendHeaders,
9495
kSessionApplication,
9596
kSessionTicket,
97+
kStopSending,
9698
kTrailers,
9799
kVersionNegotiation,
98100
};

‎src/quic/bindingdata.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SessionManager;
6161
V(stream_drain, StreamDrain) \
6262
V(stream_headers, StreamHeaders) \
6363
V(stream_reset, StreamReset) \
64+
V(stream_stop_sending, StreamStopSending) \
6465
V(stream_trailers, StreamTrailers)
6566

6667
// The various JS strings the implementation uses.

‎src/quic/session.cc‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,11 +1611,11 @@ struct Session::Impl final : public MemoryRetainer {
16111611
returnNGTCP2_SUCCESS;
16121612
}
16131613

1614-
staticinton_stream_stop_sending(ngtcp2_conn* conn,
1615-
stream_id stream_id,
1616-
error_code app_error_code,
1617-
void* user_data,
1618-
void* stream_user_data) {
1614+
staticinton_receive_stream_stop_sending(ngtcp2_conn* conn,
1615+
stream_id stream_id,
1616+
error_code app_error_code,
1617+
void* user_data,
1618+
void* stream_user_data) {
16191619
NGTCP2_CALLBACK_SCOPE(session)
16201620
auto* stream = Stream::From(stream_user_data);
16211621
if (stream == nullptr) returnNGTCP2_SUCCESS;
@@ -1652,7 +1652,7 @@ struct Session::Impl final : public MemoryRetainer {
16521652

16531653
staticconstexpr ngtcp2_callbacks CLIENT = {
16541654
ngtcp2_crypto_client_initial_cb,
1655-
nullptr,
1655+
nullptr,// stream_stop_sending
16561656
ngtcp2_crypto_recv_crypto_data_cb,
16571657
on_handshake_completed,
16581658
on_receive_version_negotiation,
@@ -1686,7 +1686,7 @@ struct Session::Impl final : public MemoryRetainer {
16861686
on_acknowledge_datagram,
16871687
on_lost_datagram,
16881688
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1689-
on_stream_stop_sending,
1689+
nullptr, // stream_stop_sending
16901690
ngtcp2_crypto_version_negotiation_cb,
16911691
on_receive_rx_key,
16921692
on_receive_tx_key,
@@ -1697,12 +1697,12 @@ struct Session::Impl final : public MemoryRetainer {
16971697
on_cid_status,
16981698
ngtcp2_crypto_get_path_challenge_data2_cb,
16991699
#ifdef NGTCP2_CALLBACKS_V4
1700-
nullptr,
1700+
on_receive_stream_stop_sending,
17011701
#endif
17021702
};
17031703

17041704
staticconstexpr ngtcp2_callbacks SERVER = {
1705-
nullptr,
1705+
nullptr,// stream_stop_sending
17061706
ngtcp2_crypto_recv_client_initial_cb,
17071707
ngtcp2_crypto_recv_crypto_data_cb,
17081708
on_handshake_completed,
@@ -1737,7 +1737,7 @@ struct Session::Impl final : public MemoryRetainer {
17371737
on_acknowledge_datagram,
17381738
on_lost_datagram,
17391739
nullptr, // get_path_challenge_data (deprecated, use v2 below)
1740-
on_stream_stop_sending,
1740+
nullptr, // stream_stop_sending
17411741
ngtcp2_crypto_version_negotiation_cb,
17421742
nullptr,
17431743
on_receive_tx_key,
@@ -1748,7 +1748,7 @@ struct Session::Impl final : public MemoryRetainer {
17481748
on_cid_status,
17491749
ngtcp2_crypto_get_path_challenge_data2_cb,
17501750
#ifdef NGTCP2_CALLBACKS_V4
1751-
nullptr,
1751+
on_receive_stream_stop_sending,
17521752
#endif
17531753
};
17541754
};

‎src/quic/streams.cc‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ namespace quic {
6161
V(WANTS_HEADERS, wants_headers, uint8_t) \
6262
/* Set when the stream has a reset event handler */ \
6363
V(WANTS_RESET, wants_reset, uint8_t) \
64+
/* Set when the stream has a stop sending event handler */ \
65+
V(WANTS_STOP_SENDING, wants_stop_sending, uint8_t) \
6466
/* Set when the stream has a trailers event handler */ \
6567
V(WANTS_TRAILERS, wants_trailers, uint8_t) \
6668
/* True when 0-RTT early data was received */ \
@@ -1774,19 +1776,11 @@ void Stream::ReceiveData(const uint8_t* data,
17741776
}
17751777

17761778
voidStream::ReceiveStopSending(QuicError error) {
1777-
// STOP_SENDING from the peer asks us to stop sending. Per RFC 9000
1778-
// §3.5 the receiver SHOULD respond with RESET_STREAM, which is what
1779-
// ngtcp2_conn_shutdown_stream_write below schedules. If our
1780-
// writable side has already been shut down (e.g. we already sent
1781-
// RESET_STREAM ourselves or finished sending with FIN) there is
1782-
// nothing more to do here. The previous guard checked
1783-
// `state()->read_ended` which is unrelated to the writable side and
1784-
// suppressed STOP_SENDING handling whenever a sibling RESET_STREAM
1785-
// frame had been processed first within the same packet.
1786-
if (state()->write_ended) return;
1779+
// STOP_SENDING from the peer asks us to stop sending. The required
1780+
// RESET_STREAM response is scheduled automatically.
17871781
Debug(this, "Received stop sending with error %s", error);
1788-
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code());
17891782
EndWritable();
1783+
EmitStopSending(error);
17901784
}
17911785

17921786
voidStream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
@@ -1958,6 +1952,17 @@ void Stream::EmitReset(const QuicError& error) {
19581952
MakeCallback(BindingData::Get(env()).stream_reset_callback(), 1, &err);
19591953
}
19601954

1955+
voidStream::EmitStopSending(const QuicError& error) {
1956+
if (!env()->can_call_into_js() || !state()->wants_stop_sending) {
1957+
return;
1958+
}
1959+
CallbackScope<Stream> cb_scope(this);
1960+
Local<Value> err;
1961+
if (!error.ToV8Value(env()).ToLocal(&err)) return;
1962+
1963+
MakeCallback(BindingData::Get(env()).stream_stop_sending_callback(), 1, &err);
1964+
}
1965+
19611966
voidStream::EmitWantTrailers() {
19621967
// state()->wants_trailers will be set from the javascript side if the
19631968
// stream object has a handler for the trailers event.

‎src/quic/streams.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ class Stream final : public AsyncWrap,
417417
// Notifies the JavaScript side that the stream has been reset.
418418
voidEmitReset(const QuicError& error);
419419

420+
// Notifies the JavaScript side that the peer asked it to stop sending.
421+
voidEmitStopSending(const QuicError& error);
422+
420423
// Notifies the JavaScript side that the application is ready to receive
421424
// trailing headers. Any trailing headers must be sent immediately, and
422425
// synchronously when this callback is triggered.

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ assert.strictEqual(streamState.reset, false);
156156
assert.strictEqual(streamState.hasReader,false);
157157
assert.strictEqual(streamState.wantsBlock,false);
158158
assert.strictEqual(streamState.wantsReset,false);
159+
assert.strictEqual(streamState.wantsStopSending,false);
159160

160161
assert.strictEqual(sessionState.hasPathValidationListener,false);
161162
assert.strictEqual(sessionState.hasDatagramListener,false);

0 commit comments

Comments
 (0)