Commit 3b59d12

Browse files
pimterryaduh95
authored andcommitted
quic: add proper error codes & messages for QUIC failures
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #63198 Backport-PR-URL: #64675 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent b68f8ea commit 3b59d12

13 files changed

Lines changed: 186 additions & 58 deletions

‎lib/eslint.config_partial.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const noRestrictedSyntax = [
2323
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
2424
},
2525
{
26-
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
26+
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError)$/])',
2727
message: "Use an error exported by 'internal/errors' instead.",
2828
},
2929
{

‎lib/internal/errors.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,14 +1676,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
16761676
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS','%s',TypeError);
16771677
E('ERR_PROXY_INVALID_CONFIG','%s',Error);
16781678
E('ERR_PROXY_TUNNEL','%s',Error);
1679-
E('ERR_QUIC_APPLICATION_ERROR','A QUIC application error occurred. %d [%s]',Error);
16801679
E('ERR_QUIC_CONNECTION_FAILED','QUIC connection failed',Error);
16811680
E('ERR_QUIC_ENDPOINT_CLOSED','QUIC endpoint closed: %s (%d)',Error);
16821681
E('ERR_QUIC_OPEN_STREAM_FAILED','Failed to open QUIC stream',Error);
16831682
E('ERR_QUIC_STREAM_ABORTED','%s',Error);
16841683
E('ERR_QUIC_STREAM_RESET',
16851684
'The QUIC stream was reset by the peer with error code %d',Error);
1686-
E('ERR_QUIC_TRANSPORT_ERROR','A QUIC transport error occurred. %d [%s]',Error);
16871685
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR','The QUIC session requires version negotiation',Error);
16881686
E('ERR_REQUIRE_ASYNC_MODULE',function(filename,parentFilename){
16891687
letmessage='require() cannot be used on an ESM '+

‎lib/internal/quic/quic.js‎

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
ArrayPrototypePush,
1010
BigInt,
1111
DataViewPrototypeGetByteLength,
12+
ErrorCaptureStackTrace,
1213
FunctionPrototypeBind,
1314
Number,
1415
ObjectDefineProperties,
@@ -108,13 +109,11 @@ const {
108109
ERR_INVALID_THIS,
109110
ERR_MISSING_ARGS,
110111
ERR_OUT_OF_RANGE,
111-
ERR_QUIC_APPLICATION_ERROR,
112112
ERR_QUIC_CONNECTION_FAILED,
113113
ERR_QUIC_ENDPOINT_CLOSED,
114114
ERR_QUIC_OPEN_STREAM_FAILED,
115115
ERR_QUIC_STREAM_ABORTED,
116116
ERR_QUIC_STREAM_RESET,
117-
ERR_QUIC_TRANSPORT_ERROR,
118117
ERR_QUIC_VERSION_NEGOTIATION_ERROR,
119118
},
120119
}=require('internal/errors');
@@ -738,10 +737,12 @@ setCallbacks({
738737
* @param {number} errorType
739738
* @param {number} code
740739
* @param {string} [reason]
740+
* @param {string} [errorName] Decoded TLS alert name when `code` is a
741+
* CRYPTO_ERROR; otherwise undefined.
741742
*/
742-
onSessionClose(errorType,code,reason){
743-
debug('session close callback',errorType,code,reason);
744-
this[kOwner][kFinishClose](errorType,code,reason);
743+
onSessionClose(errorType,code,reason,errorName){
744+
debug('session close callback',errorType,code,reason,errorName);
745+
this[kOwner][kFinishClose](errorType,code,reason,errorName);
745746
},
746747

747748
/**
@@ -931,8 +932,12 @@ setCallbacks({
931932
// was an abnormal termination even if the session closed cleanly.
932933
constresetCode=getQuicStreamState(this[kOwner]).resetCode;
933934
if(resetCode!==undefined&&resetCode>0n){
934-
error=newERR_QUIC_APPLICATION_ERROR(
935-
resetCode,`stream reset with code ${resetCode}`);
935+
error=makeQuicError(
936+
'ERR_QUIC_APPLICATION_ERROR',
937+
'QUIC application error',
938+
'application',
939+
resetCode,
940+
`stream reset with code ${resetCode}`);
936941
}
937942
}
938943
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
@@ -1054,21 +1059,50 @@ class QuicError extends Error {
10541059
}
10551060
}
10561061

1057-
// Converts a raw QuicError array [type, code, reason] from C++ into a
1058-
// proper Node.js Error object.
1062+
// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or
1063+
// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for
1064+
// the wire code when known: either the OpenSSL-decoded TLS alert
1065+
// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes
1066+
// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined.
1067+
// `reason` is the peer-supplied UTF-8 reason string from the
1068+
// CONNECTION_CLOSE / RESET_STREAM frame, often empty.
1069+
functionquicErrorMessage(prefix,errorCode,reason,errorName){
1070+
letmsg=`${prefix} `;
1071+
msg+=errorName ? `${errorName} (${errorCode})` : `${errorCode}`;
1072+
if(reason)msg+=`: ${reason}`;
1073+
returnmsg;
1074+
}
1075+
1076+
functionmakeQuicError(code,prefix,type,errorCode,reason,errorName){
1077+
consterr=newQuicError(
1078+
quicErrorMessage(prefix,errorCode,reason,errorName),
1079+
{ errorCode, code, type });
1080+
ErrorCaptureStackTrace(err,makeQuicError);
1081+
if(reason)err.reason=reason;
1082+
if(errorName)err.errorName=errorName;
1083+
returnerr;
1084+
}
1085+
10591086
functionconvertQuicError(error){
10601087
consttype=error[0];
10611088
constcode=error[1];
10621089
constreason=error[2];
1090+
consterrorName=error[3];
10631091
switch(type){
10641092
case'transport':
1065-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1093+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1094+
'QUIC transport error',
1095+
'transport',code,reason,errorName);
10661096
case'application':
1067-
returnnewERR_QUIC_APPLICATION_ERROR(code,reason);
1097+
returnmakeQuicError('ERR_QUIC_APPLICATION_ERROR',
1098+
'QUIC application error',
1099+
'application',code,reason,errorName);
10681100
case'version_negotiation':
10691101
returnnewERR_QUIC_VERSION_NEGOTIATION_ERROR();
10701102
default:
1071-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1103+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1104+
'QUIC transport error',
1105+
'transport',code,reason,errorName);
10721106
}
10731107
}
10741108

@@ -3575,7 +3609,7 @@ class QuicSession {
35753609
* @param {number} code
35763610
* @param {string} [reason]
35773611
*/
3578-
[kFinishClose](errorType,code,reason){
3612+
[kFinishClose](errorType,code,reason,errorName){
35793613
// If code is zero, then we closed without an error. Yay! We can destroy
35803614
// safely without specifying an error.
35813615
if(code===0n){
@@ -3584,7 +3618,8 @@ class QuicSession {
35843618
return;
35853619
}
35863620

3587-
debug('finishing closing the session with an error',errorType,code,reason);
3621+
debug('finishing closing the session with an error',
3622+
errorType,code,reason,errorName);
35883623

35893624
// If the local side initiated this close with an error code (via
35903625
// close({ code })), this is an intentional shutdown; not an error.
@@ -3611,10 +3646,14 @@ class QuicSession {
36113646
// session would leak with `closed` hanging forever.
36123647
switch(errorType){
36133648
case0: /* Transport Error */
3614-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3649+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3650+
'QUIC transport error',
3651+
'transport',code,reason,errorName));
36153652
break;
36163653
case1: /* Application Error */
3617-
this.destroy(newERR_QUIC_APPLICATION_ERROR(code,reason));
3654+
this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR',
3655+
'QUIC application error',
3656+
'application',code,reason,errorName));
36183657
break;
36193658
case2: /* Version Negotiation Error */
36203659
this.destroy(newERR_QUIC_VERSION_NEGOTIATION_ERROR());
@@ -3623,7 +3662,9 @@ class QuicSession {
36233662
this.destroy();
36243663
break;
36253664
default:
3626-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3665+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3666+
'QUIC transport error',
3667+
'transport',code,reason,errorName));
36273668
break;
36283669
}
36293670
}
@@ -3874,9 +3915,13 @@ class QuicSession {
38743915
// decide. In 'strict' mode, the handshake already failed at the C++
38753916
// level (SSL_VERIFY_PEER) so we won't reach here.
38763917
if(inner.verifyPeer==='auto'&&validationErrorReason!==undefined){
3877-
consterr=newERR_QUIC_TRANSPORT_ERROR(
3878-
0,`Peer certificate validation failed: ${validationErrorReason}`+
3879-
` [${validationErrorCode}]`);
3918+
consterr=makeQuicError(
3919+
'ERR_QUIC_TRANSPORT_ERROR',
3920+
'QUIC transport error',
3921+
'transport',
3922+
0n,
3923+
`Peer certificate validation failed: ${validationErrorReason}`+
3924+
` [${validationErrorCode}]`);
38803925
inner.pendingOpen.reject?.(err);
38813926
inner.pendingOpen.resolve=undefined;
38823927
inner.pendingOpen.reject=undefined;

‎src/quic/bindingdata.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V)
435435

436436
#undef V
437437

438+
Local<String> BindingData::error_name_string(constchar* name) {
439+
auto& slot = error_name_strings_[name];
440+
if (slot.IsEmpty()) {
441+
slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
442+
}
443+
return slot.Get(env()->isolate());
444+
}
445+
438446
JS_METHOD_IMPL(BindingData::SetCallbacks) {
439447
auto env = Environment::GetCurrent(args);
440448
auto isolate = env->isolate();

‎src/quic/bindingdata.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ class BindingData final
305305

306306
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
307307

308+
v8::Local<v8::String> error_name_string(constchar* name);
309+
308310
size_t current_ngtcp2_memory_ = 0;
309311

310312
// The following set up various storage and accessors for common strings,
@@ -357,6 +359,9 @@ class BindingData final
357359
QUIC_JS_CALLBACKS(V)
358360
#undef V
359361

362+
// Lazy cache backing error_name_string()
363+
std::unordered_map<constchar*, v8::Eternal<v8::String>> error_name_strings_;
364+
360365
std::unique_ptr<SessionManager> session_manager_;
361366

362367
// Type-erased arena storage. The concrete AliasedStructArena<T> types

‎src/quic/data.cc‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#if HAVE_OPENSSL && HAVE_QUIC
22
#include"guard.h"
33
#ifndef OPENSSL_NO_QUIC
4-
#include"data.h"
54
#include<env-inl.h>
65
#include<memory_tracker-inl.h>
76
#include<ngtcp2/ngtcp2.h>
87
#include<node_sockaddr-inl.h>
8+
#include<openssl/ssl.h>
99
#include<string_bytes.h>
1010
#include<v8.h>
11+
#include"bindingdata.h"
12+
#include"data.h"
1113
#include"defs.h"
1214
#include"util.h"
1315

@@ -363,6 +365,62 @@ std::optional<int> QuicError::get_crypto_error() const {
363365
returncode() & ~NGTCP2_CRYPTO_ERROR;
364366
}
365367

368+
constchar* QuicError::name() const {
369+
// CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8).
370+
// OpenSSL's SSL_alert_desc_string_long owns a stable string for every
371+
// alert it knows about; we filter out the "unknown" placeholder so the
372+
// JS side can present `errorName` as undefined for unrecognised alerts.
373+
if (auto alert = get_crypto_error()) {
374+
constchar* n = SSL_alert_desc_string_long(*alert);
375+
if (n != nullptr && std::string_view(n) != "unknown") return n;
376+
returnnullptr;
377+
}
378+
// Named transport-layer error codes from RFC 9000 sec. 20.1 (and the
379+
// RFC 9368 version-negotiation extension). Application error codes are
380+
// opaque to QUIC, so we only decode for transport.
381+
if (type() != Type::TRANSPORT) returnnullptr;
382+
switch (code()) {
383+
caseNGTCP2_NO_ERROR:
384+
return"NO_ERROR";
385+
caseNGTCP2_INTERNAL_ERROR:
386+
return"INTERNAL_ERROR";
387+
caseNGTCP2_CONNECTION_REFUSED:
388+
return"CONNECTION_REFUSED";
389+
caseNGTCP2_FLOW_CONTROL_ERROR:
390+
return"FLOW_CONTROL_ERROR";
391+
caseNGTCP2_STREAM_LIMIT_ERROR:
392+
return"STREAM_LIMIT_ERROR";
393+
caseNGTCP2_STREAM_STATE_ERROR:
394+
return"STREAM_STATE_ERROR";
395+
caseNGTCP2_FINAL_SIZE_ERROR:
396+
return"FINAL_SIZE_ERROR";
397+
caseNGTCP2_FRAME_ENCODING_ERROR:
398+
return"FRAME_ENCODING_ERROR";
399+
caseNGTCP2_TRANSPORT_PARAMETER_ERROR:
400+
return"TRANSPORT_PARAMETER_ERROR";
401+
caseNGTCP2_CONNECTION_ID_LIMIT_ERROR:
402+
return"CONNECTION_ID_LIMIT_ERROR";
403+
caseNGTCP2_PROTOCOL_VIOLATION:
404+
return"PROTOCOL_VIOLATION";
405+
caseNGTCP2_INVALID_TOKEN:
406+
return"INVALID_TOKEN";
407+
caseNGTCP2_APPLICATION_ERROR:
408+
return"APPLICATION_ERROR";
409+
caseNGTCP2_CRYPTO_BUFFER_EXCEEDED:
410+
return"CRYPTO_BUFFER_EXCEEDED";
411+
caseNGTCP2_KEY_UPDATE_ERROR:
412+
return"KEY_UPDATE_ERROR";
413+
caseNGTCP2_AEAD_LIMIT_REACHED:
414+
return"AEAD_LIMIT_REACHED";
415+
caseNGTCP2_NO_VIABLE_PATH:
416+
return"NO_VIABLE_PATH";
417+
caseNGTCP2_VERSION_NEGOTIATION_ERROR:
418+
return"VERSION_NEGOTIATION_ERROR";
419+
default:
420+
returnnullptr;
421+
}
422+
}
423+
366424
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
367425
if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
368426
(type() == Type::APPLICATION &&
@@ -384,6 +442,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
384442
type_str,
385443
BigInt::NewFromUnsigned(env->isolate(), code()),
386444
Undefined(env->isolate()),
445+
Undefined(env->isolate()),
387446
};
388447

389448
// Note that per the QUIC specification, the reason, if present, is
@@ -397,6 +456,13 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
397456
return {};
398457
}
399458

459+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
460+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
461+
// codes leave the slot as undefined.
462+
if (constchar* n = name()) {
463+
argv[3] = BindingData::Get(env).error_name_string(n);
464+
}
465+
400466
returnArray::New(env->isolate(), argv, arraysize(argv)).As<Value>();
401467
}
402468

‎src/quic/data.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ class QuicError final : public MemoryRetainer {
265265
boolis_crypto_error() const;
266266
std::optional<int> get_crypto_error() const;
267267

268+
// Returns a human-readable name for this error if known, or nullptr
269+
constchar* name() const;
270+
268271
// Note that since application errors are application-specific and we
269272
// don't know which application is being used here, it is possible that
270273
// the comparing two different QuicError instances from different applications

‎src/quic/session.cc‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3073,7 +3073,7 @@ void Session::CheckStreamIdleTimeout(uint64_t now) {
30733073
// Without this, the peer's stream sits orphaned until the
30743074
// session closes.
30753075
auto error =
3076-
QuicError::ForTransport(NGTCP2_ERR_PROTO, "stream idle timeout");
3076+
QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout");
30773077
ShutdownStream(id, error);
30783078
stream->Destroy(error);
30793079
STAT_INCREMENT(Stats, streams_idle_timed_out);
@@ -3459,12 +3459,21 @@ void Session::EmitClose(const QuicError& error) {
34593459
Integer::New(env()->isolate(), static_cast<int>(error.type())),
34603460
BigInt::NewFromUnsigned(env()->isolate(), error.code()),
34613461
Undefined(env()->isolate()),
3462+
Undefined(env()->isolate()),
34623463
};
34633464
if (error.reason().length() > 0 &&
34643465
!ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) {
34653466
return;
34663467
}
34673468

3469+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
3470+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
3471+
// codes leave the slot as undefined. See QuicError::name() for the
3472+
// matching path on stream-level errors.
3473+
if (constchar* n = error.name()) {
3474+
argv[3] = BindingData::Get(env()).error_name_string(n);
3475+
}
3476+
34683477
MakeCallback(
34693478
BindingData::Get(env()).session_close_callback(), arraysize(argv), argv);
34703479

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 3b59d12

Browse files
pimterryaduh95
authored andcommitted
quic: add proper error codes & messages for QUIC failures
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #63198 Backport-PR-URL: #64675 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent b68f8ea commit 3b59d12

13 files changed

Lines changed: 186 additions & 58 deletions

‎lib/eslint.config_partial.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const noRestrictedSyntax = [
2323
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
2424
},
2525
{
26-
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
26+
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError)$/])',
2727
message: "Use an error exported by 'internal/errors' instead.",
2828
},
2929
{

‎lib/internal/errors.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,14 +1676,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
16761676
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS','%s',TypeError);
16771677
E('ERR_PROXY_INVALID_CONFIG','%s',Error);
16781678
E('ERR_PROXY_TUNNEL','%s',Error);
1679-
E('ERR_QUIC_APPLICATION_ERROR','A QUIC application error occurred. %d [%s]',Error);
16801679
E('ERR_QUIC_CONNECTION_FAILED','QUIC connection failed',Error);
16811680
E('ERR_QUIC_ENDPOINT_CLOSED','QUIC endpoint closed: %s (%d)',Error);
16821681
E('ERR_QUIC_OPEN_STREAM_FAILED','Failed to open QUIC stream',Error);
16831682
E('ERR_QUIC_STREAM_ABORTED','%s',Error);
16841683
E('ERR_QUIC_STREAM_RESET',
16851684
'The QUIC stream was reset by the peer with error code %d',Error);
1686-
E('ERR_QUIC_TRANSPORT_ERROR','A QUIC transport error occurred. %d [%s]',Error);
16871685
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR','The QUIC session requires version negotiation',Error);
16881686
E('ERR_REQUIRE_ASYNC_MODULE',function(filename,parentFilename){
16891687
letmessage='require() cannot be used on an ESM '+

‎lib/internal/quic/quic.js‎

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
ArrayPrototypePush,
1010
BigInt,
1111
DataViewPrototypeGetByteLength,
12+
ErrorCaptureStackTrace,
1213
FunctionPrototypeBind,
1314
Number,
1415
ObjectDefineProperties,
@@ -108,13 +109,11 @@ const {
108109
ERR_INVALID_THIS,
109110
ERR_MISSING_ARGS,
110111
ERR_OUT_OF_RANGE,
111-
ERR_QUIC_APPLICATION_ERROR,
112112
ERR_QUIC_CONNECTION_FAILED,
113113
ERR_QUIC_ENDPOINT_CLOSED,
114114
ERR_QUIC_OPEN_STREAM_FAILED,
115115
ERR_QUIC_STREAM_ABORTED,
116116
ERR_QUIC_STREAM_RESET,
117-
ERR_QUIC_TRANSPORT_ERROR,
118117
ERR_QUIC_VERSION_NEGOTIATION_ERROR,
119118
},
120119
}=require('internal/errors');
@@ -738,10 +737,12 @@ setCallbacks({
738737
* @param {number} errorType
739738
* @param {number} code
740739
* @param {string} [reason]
740+
* @param {string} [errorName] Decoded TLS alert name when `code` is a
741+
* CRYPTO_ERROR; otherwise undefined.
741742
*/
742-
onSessionClose(errorType,code,reason){
743-
debug('session close callback',errorType,code,reason);
744-
this[kOwner][kFinishClose](errorType,code,reason);
743+
onSessionClose(errorType,code,reason,errorName){
744+
debug('session close callback',errorType,code,reason,errorName);
745+
this[kOwner][kFinishClose](errorType,code,reason,errorName);
745746
},
746747

747748
/**
@@ -931,8 +932,12 @@ setCallbacks({
931932
// was an abnormal termination even if the session closed cleanly.
932933
constresetCode=getQuicStreamState(this[kOwner]).resetCode;
933934
if(resetCode!==undefined&&resetCode>0n){
934-
error=newERR_QUIC_APPLICATION_ERROR(
935-
resetCode,`stream reset with code ${resetCode}`);
935+
error=makeQuicError(
936+
'ERR_QUIC_APPLICATION_ERROR',
937+
'QUIC application error',
938+
'application',
939+
resetCode,
940+
`stream reset with code ${resetCode}`);
936941
}
937942
}
938943
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
@@ -1054,21 +1059,50 @@ class QuicError extends Error {
10541059
}
10551060
}
10561061

1057-
// Converts a raw QuicError array [type, code, reason] from C++ into a
1058-
// proper Node.js Error object.
1062+
// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or
1063+
// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for
1064+
// the wire code when known: either the OpenSSL-decoded TLS alert
1065+
// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes
1066+
// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined.
1067+
// `reason` is the peer-supplied UTF-8 reason string from the
1068+
// CONNECTION_CLOSE / RESET_STREAM frame, often empty.
1069+
functionquicErrorMessage(prefix,errorCode,reason,errorName){
1070+
letmsg=`${prefix} `;
1071+
msg+=errorName ? `${errorName} (${errorCode})` : `${errorCode}`;
1072+
if(reason)msg+=`: ${reason}`;
1073+
returnmsg;
1074+
}
1075+
1076+
functionmakeQuicError(code,prefix,type,errorCode,reason,errorName){
1077+
consterr=newQuicError(
1078+
quicErrorMessage(prefix,errorCode,reason,errorName),
1079+
{ errorCode, code, type });
1080+
ErrorCaptureStackTrace(err,makeQuicError);
1081+
if(reason)err.reason=reason;
1082+
if(errorName)err.errorName=errorName;
1083+
returnerr;
1084+
}
1085+
10591086
functionconvertQuicError(error){
10601087
consttype=error[0];
10611088
constcode=error[1];
10621089
constreason=error[2];
1090+
consterrorName=error[3];
10631091
switch(type){
10641092
case'transport':
1065-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1093+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1094+
'QUIC transport error',
1095+
'transport',code,reason,errorName);
10661096
case'application':
1067-
returnnewERR_QUIC_APPLICATION_ERROR(code,reason);
1097+
returnmakeQuicError('ERR_QUIC_APPLICATION_ERROR',
1098+
'QUIC application error',
1099+
'application',code,reason,errorName);
10681100
case'version_negotiation':
10691101
returnnewERR_QUIC_VERSION_NEGOTIATION_ERROR();
10701102
default:
1071-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1103+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1104+
'QUIC transport error',
1105+
'transport',code,reason,errorName);
10721106
}
10731107
}
10741108

@@ -3575,7 +3609,7 @@ class QuicSession {
35753609
* @param {number} code
35763610
* @param {string} [reason]
35773611
*/
3578-
[kFinishClose](errorType,code,reason){
3612+
[kFinishClose](errorType,code,reason,errorName){
35793613
// If code is zero, then we closed without an error. Yay! We can destroy
35803614
// safely without specifying an error.
35813615
if(code===0n){
@@ -3584,7 +3618,8 @@ class QuicSession {
35843618
return;
35853619
}
35863620

3587-
debug('finishing closing the session with an error',errorType,code,reason);
3621+
debug('finishing closing the session with an error',
3622+
errorType,code,reason,errorName);
35883623

35893624
// If the local side initiated this close with an error code (via
35903625
// close({ code })), this is an intentional shutdown; not an error.
@@ -3611,10 +3646,14 @@ class QuicSession {
36113646
// session would leak with `closed` hanging forever.
36123647
switch(errorType){
36133648
case0: /* Transport Error */
3614-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3649+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3650+
'QUIC transport error',
3651+
'transport',code,reason,errorName));
36153652
break;
36163653
case1: /* Application Error */
3617-
this.destroy(newERR_QUIC_APPLICATION_ERROR(code,reason));
3654+
this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR',
3655+
'QUIC application error',
3656+
'application',code,reason,errorName));
36183657
break;
36193658
case2: /* Version Negotiation Error */
36203659
this.destroy(newERR_QUIC_VERSION_NEGOTIATION_ERROR());
@@ -3623,7 +3662,9 @@ class QuicSession {
36233662
this.destroy();
36243663
break;
36253664
default:
3626-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3665+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3666+
'QUIC transport error',
3667+
'transport',code,reason,errorName));
36273668
break;
36283669
}
36293670
}
@@ -3874,9 +3915,13 @@ class QuicSession {
38743915
// decide. In 'strict' mode, the handshake already failed at the C++
38753916
// level (SSL_VERIFY_PEER) so we won't reach here.
38763917
if(inner.verifyPeer==='auto'&&validationErrorReason!==undefined){
3877-
consterr=newERR_QUIC_TRANSPORT_ERROR(
3878-
0,`Peer certificate validation failed: ${validationErrorReason}`+
3879-
` [${validationErrorCode}]`);
3918+
consterr=makeQuicError(
3919+
'ERR_QUIC_TRANSPORT_ERROR',
3920+
'QUIC transport error',
3921+
'transport',
3922+
0n,
3923+
`Peer certificate validation failed: ${validationErrorReason}`+
3924+
` [${validationErrorCode}]`);
38803925
inner.pendingOpen.reject?.(err);
38813926
inner.pendingOpen.resolve=undefined;
38823927
inner.pendingOpen.reject=undefined;

‎src/quic/bindingdata.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V)
435435

436436
#undef V
437437

438+
Local<String> BindingData::error_name_string(constchar* name) {
439+
auto& slot = error_name_strings_[name];
440+
if (slot.IsEmpty()) {
441+
slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
442+
}
443+
return slot.Get(env()->isolate());
444+
}
445+
438446
JS_METHOD_IMPL(BindingData::SetCallbacks) {
439447
auto env = Environment::GetCurrent(args);
440448
auto isolate = env->isolate();

‎src/quic/bindingdata.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ class BindingData final
305305

306306
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
307307

308+
v8::Local<v8::String> error_name_string(constchar* name);
309+
308310
size_t current_ngtcp2_memory_ = 0;
309311

310312
// The following set up various storage and accessors for common strings,
@@ -357,6 +359,9 @@ class BindingData final
357359
QUIC_JS_CALLBACKS(V)
358360
#undef V
359361

362+
// Lazy cache backing error_name_string()
363+
std::unordered_map<constchar*, v8::Eternal<v8::String>> error_name_strings_;
364+
360365
std::unique_ptr<SessionManager> session_manager_;
361366

362367
// Type-erased arena storage. The concrete AliasedStructArena<T> types

‎src/quic/data.cc‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#if HAVE_OPENSSL && HAVE_QUIC
22
#include"guard.h"
33
#ifndef OPENSSL_NO_QUIC
4-
#include"data.h"
54
#include<env-inl.h>
65
#include<memory_tracker-inl.h>
76
#include<ngtcp2/ngtcp2.h>
87
#include<node_sockaddr-inl.h>
8+
#include<openssl/ssl.h>
99
#include<string_bytes.h>
1010
#include<v8.h>
11+
#include"bindingdata.h"
12+
#include"data.h"
1113
#include"defs.h"
1214
#include"util.h"
1315

@@ -363,6 +365,62 @@ std::optional<int> QuicError::get_crypto_error() const {
363365
returncode() & ~NGTCP2_CRYPTO_ERROR;
364366
}
365367

368+
constchar* QuicError::name() const {
369+
// CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8).
370+
// OpenSSL's SSL_alert_desc_string_long owns a stable string for every
371+
// alert it knows about; we filter out the "unknown" placeholder so the
372+
// JS side can present `errorName` as undefined for unrecognised alerts.
373+
if (auto alert = get_crypto_error()) {
374+
constchar* n = SSL_alert_desc_string_long(*alert);
375+
if (n != nullptr && std::string_view(n) != "unknown") return n;
376+
returnnullptr;
377+
}
378+
// Named transport-layer error codes from RFC 9000 sec. 20.1 (and the
379+
// RFC 9368 version-negotiation extension). Application error codes are
380+
// opaque to QUIC, so we only decode for transport.
381+
if (type() != Type::TRANSPORT) returnnullptr;
382+
switch (code()) {
383+
caseNGTCP2_NO_ERROR:
384+
return"NO_ERROR";
385+
caseNGTCP2_INTERNAL_ERROR:
386+
return"INTERNAL_ERROR";
387+
caseNGTCP2_CONNECTION_REFUSED:
388+
return"CONNECTION_REFUSED";
389+
caseNGTCP2_FLOW_CONTROL_ERROR:
390+
return"FLOW_CONTROL_ERROR";
391+
caseNGTCP2_STREAM_LIMIT_ERROR:
392+
return"STREAM_LIMIT_ERROR";
393+
caseNGTCP2_STREAM_STATE_ERROR:
394+
return"STREAM_STATE_ERROR";
395+
caseNGTCP2_FINAL_SIZE_ERROR:
396+
return"FINAL_SIZE_ERROR";
397+
caseNGTCP2_FRAME_ENCODING_ERROR:
398+
return"FRAME_ENCODING_ERROR";
399+
caseNGTCP2_TRANSPORT_PARAMETER_ERROR:
400+
return"TRANSPORT_PARAMETER_ERROR";
401+
caseNGTCP2_CONNECTION_ID_LIMIT_ERROR:
402+
return"CONNECTION_ID_LIMIT_ERROR";
403+
caseNGTCP2_PROTOCOL_VIOLATION:
404+
return"PROTOCOL_VIOLATION";
405+
caseNGTCP2_INVALID_TOKEN:
406+
return"INVALID_TOKEN";
407+
caseNGTCP2_APPLICATION_ERROR:
408+
return"APPLICATION_ERROR";
409+
caseNGTCP2_CRYPTO_BUFFER_EXCEEDED:
410+
return"CRYPTO_BUFFER_EXCEEDED";
411+
caseNGTCP2_KEY_UPDATE_ERROR:
412+
return"KEY_UPDATE_ERROR";
413+
caseNGTCP2_AEAD_LIMIT_REACHED:
414+
return"AEAD_LIMIT_REACHED";
415+
caseNGTCP2_NO_VIABLE_PATH:
416+
return"NO_VIABLE_PATH";
417+
caseNGTCP2_VERSION_NEGOTIATION_ERROR:
418+
return"VERSION_NEGOTIATION_ERROR";
419+
default:
420+
returnnullptr;
421+
}
422+
}
423+
366424
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
367425
if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
368426
(type() == Type::APPLICATION &&
@@ -384,6 +442,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
384442
type_str,
385443
BigInt::NewFromUnsigned(env->isolate(), code()),
386444
Undefined(env->isolate()),
445+
Undefined(env->isolate()),
387446
};
388447

389448
// Note that per the QUIC specification, the reason, if present, is
@@ -397,6 +456,13 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
397456
return {};
398457
}
399458

459+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
460+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
461+
// codes leave the slot as undefined.
462+
if (constchar* n = name()) {
463+
argv[3] = BindingData::Get(env).error_name_string(n);
464+
}
465+
400466
returnArray::New(env->isolate(), argv, arraysize(argv)).As<Value>();
401467
}
402468

‎src/quic/data.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ class QuicError final : public MemoryRetainer {
265265
boolis_crypto_error() const;
266266
std::optional<int> get_crypto_error() const;
267267

268+
// Returns a human-readable name for this error if known, or nullptr
269+
constchar* name() const;
270+
268271
// Note that since application errors are application-specific and we
269272
// don't know which application is being used here, it is possible that
270273
// the comparing two different QuicError instances from different applications

‎src/quic/session.cc‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3073,7 +3073,7 @@ void Session::CheckStreamIdleTimeout(uint64_t now) {
30733073
// Without this, the peer's stream sits orphaned until the
30743074
// session closes.
30753075
auto error =
3076-
QuicError::ForTransport(NGTCP2_ERR_PROTO, "stream idle timeout");
3076+
QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout");
30773077
ShutdownStream(id, error);
30783078
stream->Destroy(error);
30793079
STAT_INCREMENT(Stats, streams_idle_timed_out);
@@ -3459,12 +3459,21 @@ void Session::EmitClose(const QuicError& error) {
34593459
Integer::New(env()->isolate(), static_cast<int>(error.type())),
34603460
BigInt::NewFromUnsigned(env()->isolate(), error.code()),
34613461
Undefined(env()->isolate()),
3462+
Undefined(env()->isolate()),
34623463
};
34633464
if (error.reason().length() > 0 &&
34643465
!ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) {
34653466
return;
34663467
}
34673468

3469+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
3470+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
3471+
// codes leave the slot as undefined. See QuicError::name() for the
3472+
// matching path on stream-level errors.
3473+
if (constchar* n = error.name()) {
3474+
argv[3] = BindingData::Get(env()).error_name_string(n);
3475+
}
3476+
34683477
MakeCallback(
34693478
BindingData::Get(env()).session_close_callback(), arraysize(argv), argv);
34703479

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 3b59d12

Browse files
pimterryaduh95
authored andcommitted
quic: add proper error codes & messages for QUIC failures
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #63198 Backport-PR-URL: #64675 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent b68f8ea commit 3b59d12

13 files changed

Lines changed: 186 additions & 58 deletions

‎lib/eslint.config_partial.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const noRestrictedSyntax = [
2323
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
2424
},
2525
{
26-
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
26+
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError)$/])',
2727
message: "Use an error exported by 'internal/errors' instead.",
2828
},
2929
{

‎lib/internal/errors.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,14 +1676,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
16761676
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS','%s',TypeError);
16771677
E('ERR_PROXY_INVALID_CONFIG','%s',Error);
16781678
E('ERR_PROXY_TUNNEL','%s',Error);
1679-
E('ERR_QUIC_APPLICATION_ERROR','A QUIC application error occurred. %d [%s]',Error);
16801679
E('ERR_QUIC_CONNECTION_FAILED','QUIC connection failed',Error);
16811680
E('ERR_QUIC_ENDPOINT_CLOSED','QUIC endpoint closed: %s (%d)',Error);
16821681
E('ERR_QUIC_OPEN_STREAM_FAILED','Failed to open QUIC stream',Error);
16831682
E('ERR_QUIC_STREAM_ABORTED','%s',Error);
16841683
E('ERR_QUIC_STREAM_RESET',
16851684
'The QUIC stream was reset by the peer with error code %d',Error);
1686-
E('ERR_QUIC_TRANSPORT_ERROR','A QUIC transport error occurred. %d [%s]',Error);
16871685
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR','The QUIC session requires version negotiation',Error);
16881686
E('ERR_REQUIRE_ASYNC_MODULE',function(filename,parentFilename){
16891687
letmessage='require() cannot be used on an ESM '+

‎lib/internal/quic/quic.js‎

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
ArrayPrototypePush,
1010
BigInt,
1111
DataViewPrototypeGetByteLength,
12+
ErrorCaptureStackTrace,
1213
FunctionPrototypeBind,
1314
Number,
1415
ObjectDefineProperties,
@@ -108,13 +109,11 @@ const {
108109
ERR_INVALID_THIS,
109110
ERR_MISSING_ARGS,
110111
ERR_OUT_OF_RANGE,
111-
ERR_QUIC_APPLICATION_ERROR,
112112
ERR_QUIC_CONNECTION_FAILED,
113113
ERR_QUIC_ENDPOINT_CLOSED,
114114
ERR_QUIC_OPEN_STREAM_FAILED,
115115
ERR_QUIC_STREAM_ABORTED,
116116
ERR_QUIC_STREAM_RESET,
117-
ERR_QUIC_TRANSPORT_ERROR,
118117
ERR_QUIC_VERSION_NEGOTIATION_ERROR,
119118
},
120119
}=require('internal/errors');
@@ -738,10 +737,12 @@ setCallbacks({
738737
* @param {number} errorType
739738
* @param {number} code
740739
* @param {string} [reason]
740+
* @param {string} [errorName] Decoded TLS alert name when `code` is a
741+
* CRYPTO_ERROR; otherwise undefined.
741742
*/
742-
onSessionClose(errorType,code,reason){
743-
debug('session close callback',errorType,code,reason);
744-
this[kOwner][kFinishClose](errorType,code,reason);
743+
onSessionClose(errorType,code,reason,errorName){
744+
debug('session close callback',errorType,code,reason,errorName);
745+
this[kOwner][kFinishClose](errorType,code,reason,errorName);
745746
},
746747

747748
/**
@@ -931,8 +932,12 @@ setCallbacks({
931932
// was an abnormal termination even if the session closed cleanly.
932933
constresetCode=getQuicStreamState(this[kOwner]).resetCode;
933934
if(resetCode!==undefined&&resetCode>0n){
934-
error=newERR_QUIC_APPLICATION_ERROR(
935-
resetCode,`stream reset with code ${resetCode}`);
935+
error=makeQuicError(
936+
'ERR_QUIC_APPLICATION_ERROR',
937+
'QUIC application error',
938+
'application',
939+
resetCode,
940+
`stream reset with code ${resetCode}`);
936941
}
937942
}
938943
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
@@ -1054,21 +1059,50 @@ class QuicError extends Error {
10541059
}
10551060
}
10561061

1057-
// Converts a raw QuicError array [type, code, reason] from C++ into a
1058-
// proper Node.js Error object.
1062+
// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or
1063+
// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for
1064+
// the wire code when known: either the OpenSSL-decoded TLS alert
1065+
// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes
1066+
// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined.
1067+
// `reason` is the peer-supplied UTF-8 reason string from the
1068+
// CONNECTION_CLOSE / RESET_STREAM frame, often empty.
1069+
functionquicErrorMessage(prefix,errorCode,reason,errorName){
1070+
letmsg=`${prefix} `;
1071+
msg+=errorName ? `${errorName} (${errorCode})` : `${errorCode}`;
1072+
if(reason)msg+=`: ${reason}`;
1073+
returnmsg;
1074+
}
1075+
1076+
functionmakeQuicError(code,prefix,type,errorCode,reason,errorName){
1077+
consterr=newQuicError(
1078+
quicErrorMessage(prefix,errorCode,reason,errorName),
1079+
{ errorCode, code, type });
1080+
ErrorCaptureStackTrace(err,makeQuicError);
1081+
if(reason)err.reason=reason;
1082+
if(errorName)err.errorName=errorName;
1083+
returnerr;
1084+
}
1085+
10591086
functionconvertQuicError(error){
10601087
consttype=error[0];
10611088
constcode=error[1];
10621089
constreason=error[2];
1090+
consterrorName=error[3];
10631091
switch(type){
10641092
case'transport':
1065-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1093+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1094+
'QUIC transport error',
1095+
'transport',code,reason,errorName);
10661096
case'application':
1067-
returnnewERR_QUIC_APPLICATION_ERROR(code,reason);
1097+
returnmakeQuicError('ERR_QUIC_APPLICATION_ERROR',
1098+
'QUIC application error',
1099+
'application',code,reason,errorName);
10681100
case'version_negotiation':
10691101
returnnewERR_QUIC_VERSION_NEGOTIATION_ERROR();
10701102
default:
1071-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1103+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1104+
'QUIC transport error',
1105+
'transport',code,reason,errorName);
10721106
}
10731107
}
10741108

@@ -3575,7 +3609,7 @@ class QuicSession {
35753609
* @param {number} code
35763610
* @param {string} [reason]
35773611
*/
3578-
[kFinishClose](errorType,code,reason){
3612+
[kFinishClose](errorType,code,reason,errorName){
35793613
// If code is zero, then we closed without an error. Yay! We can destroy
35803614
// safely without specifying an error.
35813615
if(code===0n){
@@ -3584,7 +3618,8 @@ class QuicSession {
35843618
return;
35853619
}
35863620

3587-
debug('finishing closing the session with an error',errorType,code,reason);
3621+
debug('finishing closing the session with an error',
3622+
errorType,code,reason,errorName);
35883623

35893624
// If the local side initiated this close with an error code (via
35903625
// close({ code })), this is an intentional shutdown; not an error.
@@ -3611,10 +3646,14 @@ class QuicSession {
36113646
// session would leak with `closed` hanging forever.
36123647
switch(errorType){
36133648
case0: /* Transport Error */
3614-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3649+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3650+
'QUIC transport error',
3651+
'transport',code,reason,errorName));
36153652
break;
36163653
case1: /* Application Error */
3617-
this.destroy(newERR_QUIC_APPLICATION_ERROR(code,reason));
3654+
this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR',
3655+
'QUIC application error',
3656+
'application',code,reason,errorName));
36183657
break;
36193658
case2: /* Version Negotiation Error */
36203659
this.destroy(newERR_QUIC_VERSION_NEGOTIATION_ERROR());
@@ -3623,7 +3662,9 @@ class QuicSession {
36233662
this.destroy();
36243663
break;
36253664
default:
3626-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3665+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3666+
'QUIC transport error',
3667+
'transport',code,reason,errorName));
36273668
break;
36283669
}
36293670
}
@@ -3874,9 +3915,13 @@ class QuicSession {
38743915
// decide. In 'strict' mode, the handshake already failed at the C++
38753916
// level (SSL_VERIFY_PEER) so we won't reach here.
38763917
if(inner.verifyPeer==='auto'&&validationErrorReason!==undefined){
3877-
consterr=newERR_QUIC_TRANSPORT_ERROR(
3878-
0,`Peer certificate validation failed: ${validationErrorReason}`+
3879-
` [${validationErrorCode}]`);
3918+
consterr=makeQuicError(
3919+
'ERR_QUIC_TRANSPORT_ERROR',
3920+
'QUIC transport error',
3921+
'transport',
3922+
0n,
3923+
`Peer certificate validation failed: ${validationErrorReason}`+
3924+
` [${validationErrorCode}]`);
38803925
inner.pendingOpen.reject?.(err);
38813926
inner.pendingOpen.resolve=undefined;
38823927
inner.pendingOpen.reject=undefined;

‎src/quic/bindingdata.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V)
435435

436436
#undef V
437437

438+
Local<String> BindingData::error_name_string(constchar* name) {
439+
auto& slot = error_name_strings_[name];
440+
if (slot.IsEmpty()) {
441+
slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
442+
}
443+
return slot.Get(env()->isolate());
444+
}
445+
438446
JS_METHOD_IMPL(BindingData::SetCallbacks) {
439447
auto env = Environment::GetCurrent(args);
440448
auto isolate = env->isolate();

‎src/quic/bindingdata.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ class BindingData final
305305

306306
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
307307

308+
v8::Local<v8::String> error_name_string(constchar* name);
309+
308310
size_t current_ngtcp2_memory_ = 0;
309311

310312
// The following set up various storage and accessors for common strings,
@@ -357,6 +359,9 @@ class BindingData final
357359
QUIC_JS_CALLBACKS(V)
358360
#undef V
359361

362+
// Lazy cache backing error_name_string()
363+
std::unordered_map<constchar*, v8::Eternal<v8::String>> error_name_strings_;
364+
360365
std::unique_ptr<SessionManager> session_manager_;
361366

362367
// Type-erased arena storage. The concrete AliasedStructArena<T> types

‎src/quic/data.cc‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#if HAVE_OPENSSL && HAVE_QUIC
22
#include"guard.h"
33
#ifndef OPENSSL_NO_QUIC
4-
#include"data.h"
54
#include<env-inl.h>
65
#include<memory_tracker-inl.h>
76
#include<ngtcp2/ngtcp2.h>
87
#include<node_sockaddr-inl.h>
8+
#include<openssl/ssl.h>
99
#include<string_bytes.h>
1010
#include<v8.h>
11+
#include"bindingdata.h"
12+
#include"data.h"
1113
#include"defs.h"
1214
#include"util.h"
1315

@@ -363,6 +365,62 @@ std::optional<int> QuicError::get_crypto_error() const {
363365
returncode() & ~NGTCP2_CRYPTO_ERROR;
364366
}
365367

368+
constchar* QuicError::name() const {
369+
// CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8).
370+
// OpenSSL's SSL_alert_desc_string_long owns a stable string for every
371+
// alert it knows about; we filter out the "unknown" placeholder so the
372+
// JS side can present `errorName` as undefined for unrecognised alerts.
373+
if (auto alert = get_crypto_error()) {
374+
constchar* n = SSL_alert_desc_string_long(*alert);
375+
if (n != nullptr && std::string_view(n) != "unknown") return n;
376+
returnnullptr;
377+
}
378+
// Named transport-layer error codes from RFC 9000 sec. 20.1 (and the
379+
// RFC 9368 version-negotiation extension). Application error codes are
380+
// opaque to QUIC, so we only decode for transport.
381+
if (type() != Type::TRANSPORT) returnnullptr;
382+
switch (code()) {
383+
caseNGTCP2_NO_ERROR:
384+
return"NO_ERROR";
385+
caseNGTCP2_INTERNAL_ERROR:
386+
return"INTERNAL_ERROR";
387+
caseNGTCP2_CONNECTION_REFUSED:
388+
return"CONNECTION_REFUSED";
389+
caseNGTCP2_FLOW_CONTROL_ERROR:
390+
return"FLOW_CONTROL_ERROR";
391+
caseNGTCP2_STREAM_LIMIT_ERROR:
392+
return"STREAM_LIMIT_ERROR";
393+
caseNGTCP2_STREAM_STATE_ERROR:
394+
return"STREAM_STATE_ERROR";
395+
caseNGTCP2_FINAL_SIZE_ERROR:
396+
return"FINAL_SIZE_ERROR";
397+
caseNGTCP2_FRAME_ENCODING_ERROR:
398+
return"FRAME_ENCODING_ERROR";
399+
caseNGTCP2_TRANSPORT_PARAMETER_ERROR:
400+
return"TRANSPORT_PARAMETER_ERROR";
401+
caseNGTCP2_CONNECTION_ID_LIMIT_ERROR:
402+
return"CONNECTION_ID_LIMIT_ERROR";
403+
caseNGTCP2_PROTOCOL_VIOLATION:
404+
return"PROTOCOL_VIOLATION";
405+
caseNGTCP2_INVALID_TOKEN:
406+
return"INVALID_TOKEN";
407+
caseNGTCP2_APPLICATION_ERROR:
408+
return"APPLICATION_ERROR";
409+
caseNGTCP2_CRYPTO_BUFFER_EXCEEDED:
410+
return"CRYPTO_BUFFER_EXCEEDED";
411+
caseNGTCP2_KEY_UPDATE_ERROR:
412+
return"KEY_UPDATE_ERROR";
413+
caseNGTCP2_AEAD_LIMIT_REACHED:
414+
return"AEAD_LIMIT_REACHED";
415+
caseNGTCP2_NO_VIABLE_PATH:
416+
return"NO_VIABLE_PATH";
417+
caseNGTCP2_VERSION_NEGOTIATION_ERROR:
418+
return"VERSION_NEGOTIATION_ERROR";
419+
default:
420+
returnnullptr;
421+
}
422+
}
423+
366424
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
367425
if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
368426
(type() == Type::APPLICATION &&
@@ -384,6 +442,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
384442
type_str,
385443
BigInt::NewFromUnsigned(env->isolate(), code()),
386444
Undefined(env->isolate()),
445+
Undefined(env->isolate()),
387446
};
388447

389448
// Note that per the QUIC specification, the reason, if present, is
@@ -397,6 +456,13 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
397456
return {};
398457
}
399458

459+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
460+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
461+
// codes leave the slot as undefined.
462+
if (constchar* n = name()) {
463+
argv[3] = BindingData::Get(env).error_name_string(n);
464+
}
465+
400466
returnArray::New(env->isolate(), argv, arraysize(argv)).As<Value>();
401467
}
402468

‎src/quic/data.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ class QuicError final : public MemoryRetainer {
265265
boolis_crypto_error() const;
266266
std::optional<int> get_crypto_error() const;
267267

268+
// Returns a human-readable name for this error if known, or nullptr
269+
constchar* name() const;
270+
268271
// Note that since application errors are application-specific and we
269272
// don't know which application is being used here, it is possible that
270273
// the comparing two different QuicError instances from different applications

‎src/quic/session.cc‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3073,7 +3073,7 @@ void Session::CheckStreamIdleTimeout(uint64_t now) {
30733073
// Without this, the peer's stream sits orphaned until the
30743074
// session closes.
30753075
auto error =
3076-
QuicError::ForTransport(NGTCP2_ERR_PROTO, "stream idle timeout");
3076+
QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout");
30773077
ShutdownStream(id, error);
30783078
stream->Destroy(error);
30793079
STAT_INCREMENT(Stats, streams_idle_timed_out);
@@ -3459,12 +3459,21 @@ void Session::EmitClose(const QuicError& error) {
34593459
Integer::New(env()->isolate(), static_cast<int>(error.type())),
34603460
BigInt::NewFromUnsigned(env()->isolate(), error.code()),
34613461
Undefined(env()->isolate()),
3462+
Undefined(env()->isolate()),
34623463
};
34633464
if (error.reason().length() > 0 &&
34643465
!ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) {
34653466
return;
34663467
}
34673468

3469+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
3470+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
3471+
// codes leave the slot as undefined. See QuicError::name() for the
3472+
// matching path on stream-level errors.
3473+
if (constchar* n = error.name()) {
3474+
argv[3] = BindingData::Get(env()).error_name_string(n);
3475+
}
3476+
34683477
MakeCallback(
34693478
BindingData::Get(env()).session_close_callback(), arraysize(argv), argv);
34703479

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 3b59d12

Browse files
pimterryaduh95
authored andcommitted
quic: add proper error codes & messages for QUIC failures
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #63198 Backport-PR-URL: #64675 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent b68f8ea commit 3b59d12

13 files changed

Lines changed: 186 additions & 58 deletions

‎lib/eslint.config_partial.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const noRestrictedSyntax = [
2323
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
2424
},
2525
{
26-
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
26+
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError)$/])',
2727
message: "Use an error exported by 'internal/errors' instead.",
2828
},
2929
{

‎lib/internal/errors.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,14 +1676,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
16761676
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS','%s',TypeError);
16771677
E('ERR_PROXY_INVALID_CONFIG','%s',Error);
16781678
E('ERR_PROXY_TUNNEL','%s',Error);
1679-
E('ERR_QUIC_APPLICATION_ERROR','A QUIC application error occurred. %d [%s]',Error);
16801679
E('ERR_QUIC_CONNECTION_FAILED','QUIC connection failed',Error);
16811680
E('ERR_QUIC_ENDPOINT_CLOSED','QUIC endpoint closed: %s (%d)',Error);
16821681
E('ERR_QUIC_OPEN_STREAM_FAILED','Failed to open QUIC stream',Error);
16831682
E('ERR_QUIC_STREAM_ABORTED','%s',Error);
16841683
E('ERR_QUIC_STREAM_RESET',
16851684
'The QUIC stream was reset by the peer with error code %d',Error);
1686-
E('ERR_QUIC_TRANSPORT_ERROR','A QUIC transport error occurred. %d [%s]',Error);
16871685
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR','The QUIC session requires version negotiation',Error);
16881686
E('ERR_REQUIRE_ASYNC_MODULE',function(filename,parentFilename){
16891687
letmessage='require() cannot be used on an ESM '+

‎lib/internal/quic/quic.js‎

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
ArrayPrototypePush,
1010
BigInt,
1111
DataViewPrototypeGetByteLength,
12+
ErrorCaptureStackTrace,
1213
FunctionPrototypeBind,
1314
Number,
1415
ObjectDefineProperties,
@@ -108,13 +109,11 @@ const {
108109
ERR_INVALID_THIS,
109110
ERR_MISSING_ARGS,
110111
ERR_OUT_OF_RANGE,
111-
ERR_QUIC_APPLICATION_ERROR,
112112
ERR_QUIC_CONNECTION_FAILED,
113113
ERR_QUIC_ENDPOINT_CLOSED,
114114
ERR_QUIC_OPEN_STREAM_FAILED,
115115
ERR_QUIC_STREAM_ABORTED,
116116
ERR_QUIC_STREAM_RESET,
117-
ERR_QUIC_TRANSPORT_ERROR,
118117
ERR_QUIC_VERSION_NEGOTIATION_ERROR,
119118
},
120119
}=require('internal/errors');
@@ -738,10 +737,12 @@ setCallbacks({
738737
* @param {number} errorType
739738
* @param {number} code
740739
* @param {string} [reason]
740+
* @param {string} [errorName] Decoded TLS alert name when `code` is a
741+
* CRYPTO_ERROR; otherwise undefined.
741742
*/
742-
onSessionClose(errorType,code,reason){
743-
debug('session close callback',errorType,code,reason);
744-
this[kOwner][kFinishClose](errorType,code,reason);
743+
onSessionClose(errorType,code,reason,errorName){
744+
debug('session close callback',errorType,code,reason,errorName);
745+
this[kOwner][kFinishClose](errorType,code,reason,errorName);
745746
},
746747

747748
/**
@@ -931,8 +932,12 @@ setCallbacks({
931932
// was an abnormal termination even if the session closed cleanly.
932933
constresetCode=getQuicStreamState(this[kOwner]).resetCode;
933934
if(resetCode!==undefined&&resetCode>0n){
934-
error=newERR_QUIC_APPLICATION_ERROR(
935-
resetCode,`stream reset with code ${resetCode}`);
935+
error=makeQuicError(
936+
'ERR_QUIC_APPLICATION_ERROR',
937+
'QUIC application error',
938+
'application',
939+
resetCode,
940+
`stream reset with code ${resetCode}`);
936941
}
937942
}
938943
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
@@ -1054,21 +1059,50 @@ class QuicError extends Error {
10541059
}
10551060
}
10561061

1057-
// Converts a raw QuicError array [type, code, reason] from C++ into a
1058-
// proper Node.js Error object.
1062+
// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or
1063+
// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for
1064+
// the wire code when known: either the OpenSSL-decoded TLS alert
1065+
// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes
1066+
// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined.
1067+
// `reason` is the peer-supplied UTF-8 reason string from the
1068+
// CONNECTION_CLOSE / RESET_STREAM frame, often empty.
1069+
functionquicErrorMessage(prefix,errorCode,reason,errorName){
1070+
letmsg=`${prefix} `;
1071+
msg+=errorName ? `${errorName} (${errorCode})` : `${errorCode}`;
1072+
if(reason)msg+=`: ${reason}`;
1073+
returnmsg;
1074+
}
1075+
1076+
functionmakeQuicError(code,prefix,type,errorCode,reason,errorName){
1077+
consterr=newQuicError(
1078+
quicErrorMessage(prefix,errorCode,reason,errorName),
1079+
{ errorCode, code, type });
1080+
ErrorCaptureStackTrace(err,makeQuicError);
1081+
if(reason)err.reason=reason;
1082+
if(errorName)err.errorName=errorName;
1083+
returnerr;
1084+
}
1085+
10591086
functionconvertQuicError(error){
10601087
consttype=error[0];
10611088
constcode=error[1];
10621089
constreason=error[2];
1090+
consterrorName=error[3];
10631091
switch(type){
10641092
case'transport':
1065-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1093+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1094+
'QUIC transport error',
1095+
'transport',code,reason,errorName);
10661096
case'application':
1067-
returnnewERR_QUIC_APPLICATION_ERROR(code,reason);
1097+
returnmakeQuicError('ERR_QUIC_APPLICATION_ERROR',
1098+
'QUIC application error',
1099+
'application',code,reason,errorName);
10681100
case'version_negotiation':
10691101
returnnewERR_QUIC_VERSION_NEGOTIATION_ERROR();
10701102
default:
1071-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1103+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1104+
'QUIC transport error',
1105+
'transport',code,reason,errorName);
10721106
}
10731107
}
10741108

@@ -3575,7 +3609,7 @@ class QuicSession {
35753609
* @param {number} code
35763610
* @param {string} [reason]
35773611
*/
3578-
[kFinishClose](errorType,code,reason){
3612+
[kFinishClose](errorType,code,reason,errorName){
35793613
// If code is zero, then we closed without an error. Yay! We can destroy
35803614
// safely without specifying an error.
35813615
if(code===0n){
@@ -3584,7 +3618,8 @@ class QuicSession {
35843618
return;
35853619
}
35863620

3587-
debug('finishing closing the session with an error',errorType,code,reason);
3621+
debug('finishing closing the session with an error',
3622+
errorType,code,reason,errorName);
35883623

35893624
// If the local side initiated this close with an error code (via
35903625
// close({ code })), this is an intentional shutdown; not an error.
@@ -3611,10 +3646,14 @@ class QuicSession {
36113646
// session would leak with `closed` hanging forever.
36123647
switch(errorType){
36133648
case0: /* Transport Error */
3614-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3649+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3650+
'QUIC transport error',
3651+
'transport',code,reason,errorName));
36153652
break;
36163653
case1: /* Application Error */
3617-
this.destroy(newERR_QUIC_APPLICATION_ERROR(code,reason));
3654+
this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR',
3655+
'QUIC application error',
3656+
'application',code,reason,errorName));
36183657
break;
36193658
case2: /* Version Negotiation Error */
36203659
this.destroy(newERR_QUIC_VERSION_NEGOTIATION_ERROR());
@@ -3623,7 +3662,9 @@ class QuicSession {
36233662
this.destroy();
36243663
break;
36253664
default:
3626-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3665+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3666+
'QUIC transport error',
3667+
'transport',code,reason,errorName));
36273668
break;
36283669
}
36293670
}
@@ -3874,9 +3915,13 @@ class QuicSession {
38743915
// decide. In 'strict' mode, the handshake already failed at the C++
38753916
// level (SSL_VERIFY_PEER) so we won't reach here.
38763917
if(inner.verifyPeer==='auto'&&validationErrorReason!==undefined){
3877-
consterr=newERR_QUIC_TRANSPORT_ERROR(
3878-
0,`Peer certificate validation failed: ${validationErrorReason}`+
3879-
` [${validationErrorCode}]`);
3918+
consterr=makeQuicError(
3919+
'ERR_QUIC_TRANSPORT_ERROR',
3920+
'QUIC transport error',
3921+
'transport',
3922+
0n,
3923+
`Peer certificate validation failed: ${validationErrorReason}`+
3924+
` [${validationErrorCode}]`);
38803925
inner.pendingOpen.reject?.(err);
38813926
inner.pendingOpen.resolve=undefined;
38823927
inner.pendingOpen.reject=undefined;

‎src/quic/bindingdata.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V)
435435

436436
#undef V
437437

438+
Local<String> BindingData::error_name_string(constchar* name) {
439+
auto& slot = error_name_strings_[name];
440+
if (slot.IsEmpty()) {
441+
slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
442+
}
443+
return slot.Get(env()->isolate());
444+
}
445+
438446
JS_METHOD_IMPL(BindingData::SetCallbacks) {
439447
auto env = Environment::GetCurrent(args);
440448
auto isolate = env->isolate();

‎src/quic/bindingdata.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ class BindingData final
305305

306306
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
307307

308+
v8::Local<v8::String> error_name_string(constchar* name);
309+
308310
size_t current_ngtcp2_memory_ = 0;
309311

310312
// The following set up various storage and accessors for common strings,
@@ -357,6 +359,9 @@ class BindingData final
357359
QUIC_JS_CALLBACKS(V)
358360
#undef V
359361

362+
// Lazy cache backing error_name_string()
363+
std::unordered_map<constchar*, v8::Eternal<v8::String>> error_name_strings_;
364+
360365
std::unique_ptr<SessionManager> session_manager_;
361366

362367
// Type-erased arena storage. The concrete AliasedStructArena<T> types

‎src/quic/data.cc‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#if HAVE_OPENSSL && HAVE_QUIC
22
#include"guard.h"
33
#ifndef OPENSSL_NO_QUIC
4-
#include"data.h"
54
#include<env-inl.h>
65
#include<memory_tracker-inl.h>
76
#include<ngtcp2/ngtcp2.h>
87
#include<node_sockaddr-inl.h>
8+
#include<openssl/ssl.h>
99
#include<string_bytes.h>
1010
#include<v8.h>
11+
#include"bindingdata.h"
12+
#include"data.h"
1113
#include"defs.h"
1214
#include"util.h"
1315

@@ -363,6 +365,62 @@ std::optional<int> QuicError::get_crypto_error() const {
363365
returncode() & ~NGTCP2_CRYPTO_ERROR;
364366
}
365367

368+
constchar* QuicError::name() const {
369+
// CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8).
370+
// OpenSSL's SSL_alert_desc_string_long owns a stable string for every
371+
// alert it knows about; we filter out the "unknown" placeholder so the
372+
// JS side can present `errorName` as undefined for unrecognised alerts.
373+
if (auto alert = get_crypto_error()) {
374+
constchar* n = SSL_alert_desc_string_long(*alert);
375+
if (n != nullptr && std::string_view(n) != "unknown") return n;
376+
returnnullptr;
377+
}
378+
// Named transport-layer error codes from RFC 9000 sec. 20.1 (and the
379+
// RFC 9368 version-negotiation extension). Application error codes are
380+
// opaque to QUIC, so we only decode for transport.
381+
if (type() != Type::TRANSPORT) returnnullptr;
382+
switch (code()) {
383+
caseNGTCP2_NO_ERROR:
384+
return"NO_ERROR";
385+
caseNGTCP2_INTERNAL_ERROR:
386+
return"INTERNAL_ERROR";
387+
caseNGTCP2_CONNECTION_REFUSED:
388+
return"CONNECTION_REFUSED";
389+
caseNGTCP2_FLOW_CONTROL_ERROR:
390+
return"FLOW_CONTROL_ERROR";
391+
caseNGTCP2_STREAM_LIMIT_ERROR:
392+
return"STREAM_LIMIT_ERROR";
393+
caseNGTCP2_STREAM_STATE_ERROR:
394+
return"STREAM_STATE_ERROR";
395+
caseNGTCP2_FINAL_SIZE_ERROR:
396+
return"FINAL_SIZE_ERROR";
397+
caseNGTCP2_FRAME_ENCODING_ERROR:
398+
return"FRAME_ENCODING_ERROR";
399+
caseNGTCP2_TRANSPORT_PARAMETER_ERROR:
400+
return"TRANSPORT_PARAMETER_ERROR";
401+
caseNGTCP2_CONNECTION_ID_LIMIT_ERROR:
402+
return"CONNECTION_ID_LIMIT_ERROR";
403+
caseNGTCP2_PROTOCOL_VIOLATION:
404+
return"PROTOCOL_VIOLATION";
405+
caseNGTCP2_INVALID_TOKEN:
406+
return"INVALID_TOKEN";
407+
caseNGTCP2_APPLICATION_ERROR:
408+
return"APPLICATION_ERROR";
409+
caseNGTCP2_CRYPTO_BUFFER_EXCEEDED:
410+
return"CRYPTO_BUFFER_EXCEEDED";
411+
caseNGTCP2_KEY_UPDATE_ERROR:
412+
return"KEY_UPDATE_ERROR";
413+
caseNGTCP2_AEAD_LIMIT_REACHED:
414+
return"AEAD_LIMIT_REACHED";
415+
caseNGTCP2_NO_VIABLE_PATH:
416+
return"NO_VIABLE_PATH";
417+
caseNGTCP2_VERSION_NEGOTIATION_ERROR:
418+
return"VERSION_NEGOTIATION_ERROR";
419+
default:
420+
returnnullptr;
421+
}
422+
}
423+
366424
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
367425
if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
368426
(type() == Type::APPLICATION &&
@@ -384,6 +442,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
384442
type_str,
385443
BigInt::NewFromUnsigned(env->isolate(), code()),
386444
Undefined(env->isolate()),
445+
Undefined(env->isolate()),
387446
};
388447

389448
// Note that per the QUIC specification, the reason, if present, is
@@ -397,6 +456,13 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
397456
return {};
398457
}
399458

459+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
460+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
461+
// codes leave the slot as undefined.
462+
if (constchar* n = name()) {
463+
argv[3] = BindingData::Get(env).error_name_string(n);
464+
}
465+
400466
returnArray::New(env->isolate(), argv, arraysize(argv)).As<Value>();
401467
}
402468

‎src/quic/data.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ class QuicError final : public MemoryRetainer {
265265
boolis_crypto_error() const;
266266
std::optional<int> get_crypto_error() const;
267267

268+
// Returns a human-readable name for this error if known, or nullptr
269+
constchar* name() const;
270+
268271
// Note that since application errors are application-specific and we
269272
// don't know which application is being used here, it is possible that
270273
// the comparing two different QuicError instances from different applications

‎src/quic/session.cc‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3073,7 +3073,7 @@ void Session::CheckStreamIdleTimeout(uint64_t now) {
30733073
// Without this, the peer's stream sits orphaned until the
30743074
// session closes.
30753075
auto error =
3076-
QuicError::ForTransport(NGTCP2_ERR_PROTO, "stream idle timeout");
3076+
QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout");
30773077
ShutdownStream(id, error);
30783078
stream->Destroy(error);
30793079
STAT_INCREMENT(Stats, streams_idle_timed_out);
@@ -3459,12 +3459,21 @@ void Session::EmitClose(const QuicError& error) {
34593459
Integer::New(env()->isolate(), static_cast<int>(error.type())),
34603460
BigInt::NewFromUnsigned(env()->isolate(), error.code()),
34613461
Undefined(env()->isolate()),
3462+
Undefined(env()->isolate()),
34623463
};
34633464
if (error.reason().length() > 0 &&
34643465
!ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) {
34653466
return;
34663467
}
34673468

3469+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
3470+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
3471+
// codes leave the slot as undefined. See QuicError::name() for the
3472+
// matching path on stream-level errors.
3473+
if (constchar* n = error.name()) {
3474+
argv[3] = BindingData::Get(env()).error_name_string(n);
3475+
}
3476+
34683477
MakeCallback(
34693478
BindingData::Get(env()).session_close_callback(), arraysize(argv), argv);
34703479

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 3b59d12

Browse files
pimterryaduh95
authored andcommitted
quic: add proper error codes & messages for QUIC failures
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #63198 Backport-PR-URL: #64675 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent b68f8ea commit 3b59d12

13 files changed

Lines changed: 186 additions & 58 deletions

‎lib/eslint.config_partial.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const noRestrictedSyntax = [
2323
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
2424
},
2525
{
26-
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
26+
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError)$/])',
2727
message: "Use an error exported by 'internal/errors' instead.",
2828
},
2929
{

‎lib/internal/errors.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,14 +1676,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
16761676
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS','%s',TypeError);
16771677
E('ERR_PROXY_INVALID_CONFIG','%s',Error);
16781678
E('ERR_PROXY_TUNNEL','%s',Error);
1679-
E('ERR_QUIC_APPLICATION_ERROR','A QUIC application error occurred. %d [%s]',Error);
16801679
E('ERR_QUIC_CONNECTION_FAILED','QUIC connection failed',Error);
16811680
E('ERR_QUIC_ENDPOINT_CLOSED','QUIC endpoint closed: %s (%d)',Error);
16821681
E('ERR_QUIC_OPEN_STREAM_FAILED','Failed to open QUIC stream',Error);
16831682
E('ERR_QUIC_STREAM_ABORTED','%s',Error);
16841683
E('ERR_QUIC_STREAM_RESET',
16851684
'The QUIC stream was reset by the peer with error code %d',Error);
1686-
E('ERR_QUIC_TRANSPORT_ERROR','A QUIC transport error occurred. %d [%s]',Error);
16871685
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR','The QUIC session requires version negotiation',Error);
16881686
E('ERR_REQUIRE_ASYNC_MODULE',function(filename,parentFilename){
16891687
letmessage='require() cannot be used on an ESM '+

‎lib/internal/quic/quic.js‎

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
ArrayPrototypePush,
1010
BigInt,
1111
DataViewPrototypeGetByteLength,
12+
ErrorCaptureStackTrace,
1213
FunctionPrototypeBind,
1314
Number,
1415
ObjectDefineProperties,
@@ -108,13 +109,11 @@ const {
108109
ERR_INVALID_THIS,
109110
ERR_MISSING_ARGS,
110111
ERR_OUT_OF_RANGE,
111-
ERR_QUIC_APPLICATION_ERROR,
112112
ERR_QUIC_CONNECTION_FAILED,
113113
ERR_QUIC_ENDPOINT_CLOSED,
114114
ERR_QUIC_OPEN_STREAM_FAILED,
115115
ERR_QUIC_STREAM_ABORTED,
116116
ERR_QUIC_STREAM_RESET,
117-
ERR_QUIC_TRANSPORT_ERROR,
118117
ERR_QUIC_VERSION_NEGOTIATION_ERROR,
119118
},
120119
}=require('internal/errors');
@@ -738,10 +737,12 @@ setCallbacks({
738737
* @param {number} errorType
739738
* @param {number} code
740739
* @param {string} [reason]
740+
* @param {string} [errorName] Decoded TLS alert name when `code` is a
741+
* CRYPTO_ERROR; otherwise undefined.
741742
*/
742-
onSessionClose(errorType,code,reason){
743-
debug('session close callback',errorType,code,reason);
744-
this[kOwner][kFinishClose](errorType,code,reason);
743+
onSessionClose(errorType,code,reason,errorName){
744+
debug('session close callback',errorType,code,reason,errorName);
745+
this[kOwner][kFinishClose](errorType,code,reason,errorName);
745746
},
746747

747748
/**
@@ -931,8 +932,12 @@ setCallbacks({
931932
// was an abnormal termination even if the session closed cleanly.
932933
constresetCode=getQuicStreamState(this[kOwner]).resetCode;
933934
if(resetCode!==undefined&&resetCode>0n){
934-
error=newERR_QUIC_APPLICATION_ERROR(
935-
resetCode,`stream reset with code ${resetCode}`);
935+
error=makeQuicError(
936+
'ERR_QUIC_APPLICATION_ERROR',
937+
'QUIC application error',
938+
'application',
939+
resetCode,
940+
`stream reset with code ${resetCode}`);
936941
}
937942
}
938943
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
@@ -1054,21 +1059,50 @@ class QuicError extends Error {
10541059
}
10551060
}
10561061

1057-
// Converts a raw QuicError array [type, code, reason] from C++ into a
1058-
// proper Node.js Error object.
1062+
// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or
1063+
// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for
1064+
// the wire code when known: either the OpenSSL-decoded TLS alert
1065+
// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes
1066+
// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined.
1067+
// `reason` is the peer-supplied UTF-8 reason string from the
1068+
// CONNECTION_CLOSE / RESET_STREAM frame, often empty.
1069+
functionquicErrorMessage(prefix,errorCode,reason,errorName){
1070+
letmsg=`${prefix} `;
1071+
msg+=errorName ? `${errorName} (${errorCode})` : `${errorCode}`;
1072+
if(reason)msg+=`: ${reason}`;
1073+
returnmsg;
1074+
}
1075+
1076+
functionmakeQuicError(code,prefix,type,errorCode,reason,errorName){
1077+
consterr=newQuicError(
1078+
quicErrorMessage(prefix,errorCode,reason,errorName),
1079+
{ errorCode, code, type });
1080+
ErrorCaptureStackTrace(err,makeQuicError);
1081+
if(reason)err.reason=reason;
1082+
if(errorName)err.errorName=errorName;
1083+
returnerr;
1084+
}
1085+
10591086
functionconvertQuicError(error){
10601087
consttype=error[0];
10611088
constcode=error[1];
10621089
constreason=error[2];
1090+
consterrorName=error[3];
10631091
switch(type){
10641092
case'transport':
1065-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1093+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1094+
'QUIC transport error',
1095+
'transport',code,reason,errorName);
10661096
case'application':
1067-
returnnewERR_QUIC_APPLICATION_ERROR(code,reason);
1097+
returnmakeQuicError('ERR_QUIC_APPLICATION_ERROR',
1098+
'QUIC application error',
1099+
'application',code,reason,errorName);
10681100
case'version_negotiation':
10691101
returnnewERR_QUIC_VERSION_NEGOTIATION_ERROR();
10701102
default:
1071-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1103+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1104+
'QUIC transport error',
1105+
'transport',code,reason,errorName);
10721106
}
10731107
}
10741108

@@ -3575,7 +3609,7 @@ class QuicSession {
35753609
* @param {number} code
35763610
* @param {string} [reason]
35773611
*/
3578-
[kFinishClose](errorType,code,reason){
3612+
[kFinishClose](errorType,code,reason,errorName){
35793613
// If code is zero, then we closed without an error. Yay! We can destroy
35803614
// safely without specifying an error.
35813615
if(code===0n){
@@ -3584,7 +3618,8 @@ class QuicSession {
35843618
return;
35853619
}
35863620

3587-
debug('finishing closing the session with an error',errorType,code,reason);
3621+
debug('finishing closing the session with an error',
3622+
errorType,code,reason,errorName);
35883623

35893624
// If the local side initiated this close with an error code (via
35903625
// close({ code })), this is an intentional shutdown; not an error.
@@ -3611,10 +3646,14 @@ class QuicSession {
36113646
// session would leak with `closed` hanging forever.
36123647
switch(errorType){
36133648
case0: /* Transport Error */
3614-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3649+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3650+
'QUIC transport error',
3651+
'transport',code,reason,errorName));
36153652
break;
36163653
case1: /* Application Error */
3617-
this.destroy(newERR_QUIC_APPLICATION_ERROR(code,reason));
3654+
this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR',
3655+
'QUIC application error',
3656+
'application',code,reason,errorName));
36183657
break;
36193658
case2: /* Version Negotiation Error */
36203659
this.destroy(newERR_QUIC_VERSION_NEGOTIATION_ERROR());
@@ -3623,7 +3662,9 @@ class QuicSession {
36233662
this.destroy();
36243663
break;
36253664
default:
3626-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3665+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3666+
'QUIC transport error',
3667+
'transport',code,reason,errorName));
36273668
break;
36283669
}
36293670
}
@@ -3874,9 +3915,13 @@ class QuicSession {
38743915
// decide. In 'strict' mode, the handshake already failed at the C++
38753916
// level (SSL_VERIFY_PEER) so we won't reach here.
38763917
if(inner.verifyPeer==='auto'&&validationErrorReason!==undefined){
3877-
consterr=newERR_QUIC_TRANSPORT_ERROR(
3878-
0,`Peer certificate validation failed: ${validationErrorReason}`+
3879-
` [${validationErrorCode}]`);
3918+
consterr=makeQuicError(
3919+
'ERR_QUIC_TRANSPORT_ERROR',
3920+
'QUIC transport error',
3921+
'transport',
3922+
0n,
3923+
`Peer certificate validation failed: ${validationErrorReason}`+
3924+
` [${validationErrorCode}]`);
38803925
inner.pendingOpen.reject?.(err);
38813926
inner.pendingOpen.resolve=undefined;
38823927
inner.pendingOpen.reject=undefined;

‎src/quic/bindingdata.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V)
435435

436436
#undef V
437437

438+
Local<String> BindingData::error_name_string(constchar* name) {
439+
auto& slot = error_name_strings_[name];
440+
if (slot.IsEmpty()) {
441+
slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
442+
}
443+
return slot.Get(env()->isolate());
444+
}
445+
438446
JS_METHOD_IMPL(BindingData::SetCallbacks) {
439447
auto env = Environment::GetCurrent(args);
440448
auto isolate = env->isolate();

‎src/quic/bindingdata.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ class BindingData final
305305

306306
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
307307

308+
v8::Local<v8::String> error_name_string(constchar* name);
309+
308310
size_t current_ngtcp2_memory_ = 0;
309311

310312
// The following set up various storage and accessors for common strings,
@@ -357,6 +359,9 @@ class BindingData final
357359
QUIC_JS_CALLBACKS(V)
358360
#undef V
359361

362+
// Lazy cache backing error_name_string()
363+
std::unordered_map<constchar*, v8::Eternal<v8::String>> error_name_strings_;
364+
360365
std::unique_ptr<SessionManager> session_manager_;
361366

362367
// Type-erased arena storage. The concrete AliasedStructArena<T> types

‎src/quic/data.cc‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#if HAVE_OPENSSL && HAVE_QUIC
22
#include"guard.h"
33
#ifndef OPENSSL_NO_QUIC
4-
#include"data.h"
54
#include<env-inl.h>
65
#include<memory_tracker-inl.h>
76
#include<ngtcp2/ngtcp2.h>
87
#include<node_sockaddr-inl.h>
8+
#include<openssl/ssl.h>
99
#include<string_bytes.h>
1010
#include<v8.h>
11+
#include"bindingdata.h"
12+
#include"data.h"
1113
#include"defs.h"
1214
#include"util.h"
1315

@@ -363,6 +365,62 @@ std::optional<int> QuicError::get_crypto_error() const {
363365
returncode() & ~NGTCP2_CRYPTO_ERROR;
364366
}
365367

368+
constchar* QuicError::name() const {
369+
// CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8).
370+
// OpenSSL's SSL_alert_desc_string_long owns a stable string for every
371+
// alert it knows about; we filter out the "unknown" placeholder so the
372+
// JS side can present `errorName` as undefined for unrecognised alerts.
373+
if (auto alert = get_crypto_error()) {
374+
constchar* n = SSL_alert_desc_string_long(*alert);
375+
if (n != nullptr && std::string_view(n) != "unknown") return n;
376+
returnnullptr;
377+
}
378+
// Named transport-layer error codes from RFC 9000 sec. 20.1 (and the
379+
// RFC 9368 version-negotiation extension). Application error codes are
380+
// opaque to QUIC, so we only decode for transport.
381+
if (type() != Type::TRANSPORT) returnnullptr;
382+
switch (code()) {
383+
caseNGTCP2_NO_ERROR:
384+
return"NO_ERROR";
385+
caseNGTCP2_INTERNAL_ERROR:
386+
return"INTERNAL_ERROR";
387+
caseNGTCP2_CONNECTION_REFUSED:
388+
return"CONNECTION_REFUSED";
389+
caseNGTCP2_FLOW_CONTROL_ERROR:
390+
return"FLOW_CONTROL_ERROR";
391+
caseNGTCP2_STREAM_LIMIT_ERROR:
392+
return"STREAM_LIMIT_ERROR";
393+
caseNGTCP2_STREAM_STATE_ERROR:
394+
return"STREAM_STATE_ERROR";
395+
caseNGTCP2_FINAL_SIZE_ERROR:
396+
return"FINAL_SIZE_ERROR";
397+
caseNGTCP2_FRAME_ENCODING_ERROR:
398+
return"FRAME_ENCODING_ERROR";
399+
caseNGTCP2_TRANSPORT_PARAMETER_ERROR:
400+
return"TRANSPORT_PARAMETER_ERROR";
401+
caseNGTCP2_CONNECTION_ID_LIMIT_ERROR:
402+
return"CONNECTION_ID_LIMIT_ERROR";
403+
caseNGTCP2_PROTOCOL_VIOLATION:
404+
return"PROTOCOL_VIOLATION";
405+
caseNGTCP2_INVALID_TOKEN:
406+
return"INVALID_TOKEN";
407+
caseNGTCP2_APPLICATION_ERROR:
408+
return"APPLICATION_ERROR";
409+
caseNGTCP2_CRYPTO_BUFFER_EXCEEDED:
410+
return"CRYPTO_BUFFER_EXCEEDED";
411+
caseNGTCP2_KEY_UPDATE_ERROR:
412+
return"KEY_UPDATE_ERROR";
413+
caseNGTCP2_AEAD_LIMIT_REACHED:
414+
return"AEAD_LIMIT_REACHED";
415+
caseNGTCP2_NO_VIABLE_PATH:
416+
return"NO_VIABLE_PATH";
417+
caseNGTCP2_VERSION_NEGOTIATION_ERROR:
418+
return"VERSION_NEGOTIATION_ERROR";
419+
default:
420+
returnnullptr;
421+
}
422+
}
423+
366424
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
367425
if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
368426
(type() == Type::APPLICATION &&
@@ -384,6 +442,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
384442
type_str,
385443
BigInt::NewFromUnsigned(env->isolate(), code()),
386444
Undefined(env->isolate()),
445+
Undefined(env->isolate()),
387446
};
388447

389448
// Note that per the QUIC specification, the reason, if present, is
@@ -397,6 +456,13 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
397456
return {};
398457
}
399458

459+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
460+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
461+
// codes leave the slot as undefined.
462+
if (constchar* n = name()) {
463+
argv[3] = BindingData::Get(env).error_name_string(n);
464+
}
465+
400466
returnArray::New(env->isolate(), argv, arraysize(argv)).As<Value>();
401467
}
402468

‎src/quic/data.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ class QuicError final : public MemoryRetainer {
265265
boolis_crypto_error() const;
266266
std::optional<int> get_crypto_error() const;
267267

268+
// Returns a human-readable name for this error if known, or nullptr
269+
constchar* name() const;
270+
268271
// Note that since application errors are application-specific and we
269272
// don't know which application is being used here, it is possible that
270273
// the comparing two different QuicError instances from different applications

‎src/quic/session.cc‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3073,7 +3073,7 @@ void Session::CheckStreamIdleTimeout(uint64_t now) {
30733073
// Without this, the peer's stream sits orphaned until the
30743074
// session closes.
30753075
auto error =
3076-
QuicError::ForTransport(NGTCP2_ERR_PROTO, "stream idle timeout");
3076+
QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout");
30773077
ShutdownStream(id, error);
30783078
stream->Destroy(error);
30793079
STAT_INCREMENT(Stats, streams_idle_timed_out);
@@ -3459,12 +3459,21 @@ void Session::EmitClose(const QuicError& error) {
34593459
Integer::New(env()->isolate(), static_cast<int>(error.type())),
34603460
BigInt::NewFromUnsigned(env()->isolate(), error.code()),
34613461
Undefined(env()->isolate()),
3462+
Undefined(env()->isolate()),
34623463
};
34633464
if (error.reason().length() > 0 &&
34643465
!ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) {
34653466
return;
34663467
}
34673468

3469+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
3470+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
3471+
// codes leave the slot as undefined. See QuicError::name() for the
3472+
// matching path on stream-level errors.
3473+
if (constchar* n = error.name()) {
3474+
argv[3] = BindingData::Get(env()).error_name_string(n);
3475+
}
3476+
34683477
MakeCallback(
34693478
BindingData::Get(env()).session_close_callback(), arraysize(argv), argv);
34703479

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 3b59d12

Browse files
pimterryaduh95
authored andcommitted
quic: add proper error codes & messages for QUIC failures
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #63198 Backport-PR-URL: #64675 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent b68f8ea commit 3b59d12

13 files changed

Lines changed: 186 additions & 58 deletions

‎lib/eslint.config_partial.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const noRestrictedSyntax = [
2323
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
2424
},
2525
{
26-
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
26+
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError)$/])',
2727
message: "Use an error exported by 'internal/errors' instead.",
2828
},
2929
{

‎lib/internal/errors.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,14 +1676,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
16761676
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS','%s',TypeError);
16771677
E('ERR_PROXY_INVALID_CONFIG','%s',Error);
16781678
E('ERR_PROXY_TUNNEL','%s',Error);
1679-
E('ERR_QUIC_APPLICATION_ERROR','A QUIC application error occurred. %d [%s]',Error);
16801679
E('ERR_QUIC_CONNECTION_FAILED','QUIC connection failed',Error);
16811680
E('ERR_QUIC_ENDPOINT_CLOSED','QUIC endpoint closed: %s (%d)',Error);
16821681
E('ERR_QUIC_OPEN_STREAM_FAILED','Failed to open QUIC stream',Error);
16831682
E('ERR_QUIC_STREAM_ABORTED','%s',Error);
16841683
E('ERR_QUIC_STREAM_RESET',
16851684
'The QUIC stream was reset by the peer with error code %d',Error);
1686-
E('ERR_QUIC_TRANSPORT_ERROR','A QUIC transport error occurred. %d [%s]',Error);
16871685
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR','The QUIC session requires version negotiation',Error);
16881686
E('ERR_REQUIRE_ASYNC_MODULE',function(filename,parentFilename){
16891687
letmessage='require() cannot be used on an ESM '+

‎lib/internal/quic/quic.js‎

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
ArrayPrototypePush,
1010
BigInt,
1111
DataViewPrototypeGetByteLength,
12+
ErrorCaptureStackTrace,
1213
FunctionPrototypeBind,
1314
Number,
1415
ObjectDefineProperties,
@@ -108,13 +109,11 @@ const {
108109
ERR_INVALID_THIS,
109110
ERR_MISSING_ARGS,
110111
ERR_OUT_OF_RANGE,
111-
ERR_QUIC_APPLICATION_ERROR,
112112
ERR_QUIC_CONNECTION_FAILED,
113113
ERR_QUIC_ENDPOINT_CLOSED,
114114
ERR_QUIC_OPEN_STREAM_FAILED,
115115
ERR_QUIC_STREAM_ABORTED,
116116
ERR_QUIC_STREAM_RESET,
117-
ERR_QUIC_TRANSPORT_ERROR,
118117
ERR_QUIC_VERSION_NEGOTIATION_ERROR,
119118
},
120119
}=require('internal/errors');
@@ -738,10 +737,12 @@ setCallbacks({
738737
* @param {number} errorType
739738
* @param {number} code
740739
* @param {string} [reason]
740+
* @param {string} [errorName] Decoded TLS alert name when `code` is a
741+
* CRYPTO_ERROR; otherwise undefined.
741742
*/
742-
onSessionClose(errorType,code,reason){
743-
debug('session close callback',errorType,code,reason);
744-
this[kOwner][kFinishClose](errorType,code,reason);
743+
onSessionClose(errorType,code,reason,errorName){
744+
debug('session close callback',errorType,code,reason,errorName);
745+
this[kOwner][kFinishClose](errorType,code,reason,errorName);
745746
},
746747

747748
/**
@@ -931,8 +932,12 @@ setCallbacks({
931932
// was an abnormal termination even if the session closed cleanly.
932933
constresetCode=getQuicStreamState(this[kOwner]).resetCode;
933934
if(resetCode!==undefined&&resetCode>0n){
934-
error=newERR_QUIC_APPLICATION_ERROR(
935-
resetCode,`stream reset with code ${resetCode}`);
935+
error=makeQuicError(
936+
'ERR_QUIC_APPLICATION_ERROR',
937+
'QUIC application error',
938+
'application',
939+
resetCode,
940+
`stream reset with code ${resetCode}`);
936941
}
937942
}
938943
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
@@ -1054,21 +1059,50 @@ class QuicError extends Error {
10541059
}
10551060
}
10561061

1057-
// Converts a raw QuicError array [type, code, reason] from C++ into a
1058-
// proper Node.js Error object.
1062+
// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or
1063+
// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for
1064+
// the wire code when known: either the OpenSSL-decoded TLS alert
1065+
// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes
1066+
// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined.
1067+
// `reason` is the peer-supplied UTF-8 reason string from the
1068+
// CONNECTION_CLOSE / RESET_STREAM frame, often empty.
1069+
functionquicErrorMessage(prefix,errorCode,reason,errorName){
1070+
letmsg=`${prefix} `;
1071+
msg+=errorName ? `${errorName} (${errorCode})` : `${errorCode}`;
1072+
if(reason)msg+=`: ${reason}`;
1073+
returnmsg;
1074+
}
1075+
1076+
functionmakeQuicError(code,prefix,type,errorCode,reason,errorName){
1077+
consterr=newQuicError(
1078+
quicErrorMessage(prefix,errorCode,reason,errorName),
1079+
{ errorCode, code, type });
1080+
ErrorCaptureStackTrace(err,makeQuicError);
1081+
if(reason)err.reason=reason;
1082+
if(errorName)err.errorName=errorName;
1083+
returnerr;
1084+
}
1085+
10591086
functionconvertQuicError(error){
10601087
consttype=error[0];
10611088
constcode=error[1];
10621089
constreason=error[2];
1090+
consterrorName=error[3];
10631091
switch(type){
10641092
case'transport':
1065-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1093+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1094+
'QUIC transport error',
1095+
'transport',code,reason,errorName);
10661096
case'application':
1067-
returnnewERR_QUIC_APPLICATION_ERROR(code,reason);
1097+
returnmakeQuicError('ERR_QUIC_APPLICATION_ERROR',
1098+
'QUIC application error',
1099+
'application',code,reason,errorName);
10681100
case'version_negotiation':
10691101
returnnewERR_QUIC_VERSION_NEGOTIATION_ERROR();
10701102
default:
1071-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1103+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1104+
'QUIC transport error',
1105+
'transport',code,reason,errorName);
10721106
}
10731107
}
10741108

@@ -3575,7 +3609,7 @@ class QuicSession {
35753609
* @param {number} code
35763610
* @param {string} [reason]
35773611
*/
3578-
[kFinishClose](errorType,code,reason){
3612+
[kFinishClose](errorType,code,reason,errorName){
35793613
// If code is zero, then we closed without an error. Yay! We can destroy
35803614
// safely without specifying an error.
35813615
if(code===0n){
@@ -3584,7 +3618,8 @@ class QuicSession {
35843618
return;
35853619
}
35863620

3587-
debug('finishing closing the session with an error',errorType,code,reason);
3621+
debug('finishing closing the session with an error',
3622+
errorType,code,reason,errorName);
35883623

35893624
// If the local side initiated this close with an error code (via
35903625
// close({ code })), this is an intentional shutdown; not an error.
@@ -3611,10 +3646,14 @@ class QuicSession {
36113646
// session would leak with `closed` hanging forever.
36123647
switch(errorType){
36133648
case0: /* Transport Error */
3614-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3649+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3650+
'QUIC transport error',
3651+
'transport',code,reason,errorName));
36153652
break;
36163653
case1: /* Application Error */
3617-
this.destroy(newERR_QUIC_APPLICATION_ERROR(code,reason));
3654+
this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR',
3655+
'QUIC application error',
3656+
'application',code,reason,errorName));
36183657
break;
36193658
case2: /* Version Negotiation Error */
36203659
this.destroy(newERR_QUIC_VERSION_NEGOTIATION_ERROR());
@@ -3623,7 +3662,9 @@ class QuicSession {
36233662
this.destroy();
36243663
break;
36253664
default:
3626-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3665+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3666+
'QUIC transport error',
3667+
'transport',code,reason,errorName));
36273668
break;
36283669
}
36293670
}
@@ -3874,9 +3915,13 @@ class QuicSession {
38743915
// decide. In 'strict' mode, the handshake already failed at the C++
38753916
// level (SSL_VERIFY_PEER) so we won't reach here.
38763917
if(inner.verifyPeer==='auto'&&validationErrorReason!==undefined){
3877-
consterr=newERR_QUIC_TRANSPORT_ERROR(
3878-
0,`Peer certificate validation failed: ${validationErrorReason}`+
3879-
` [${validationErrorCode}]`);
3918+
consterr=makeQuicError(
3919+
'ERR_QUIC_TRANSPORT_ERROR',
3920+
'QUIC transport error',
3921+
'transport',
3922+
0n,
3923+
`Peer certificate validation failed: ${validationErrorReason}`+
3924+
` [${validationErrorCode}]`);
38803925
inner.pendingOpen.reject?.(err);
38813926
inner.pendingOpen.resolve=undefined;
38823927
inner.pendingOpen.reject=undefined;

‎src/quic/bindingdata.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V)
435435

436436
#undef V
437437

438+
Local<String> BindingData::error_name_string(constchar* name) {
439+
auto& slot = error_name_strings_[name];
440+
if (slot.IsEmpty()) {
441+
slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
442+
}
443+
return slot.Get(env()->isolate());
444+
}
445+
438446
JS_METHOD_IMPL(BindingData::SetCallbacks) {
439447
auto env = Environment::GetCurrent(args);
440448
auto isolate = env->isolate();

‎src/quic/bindingdata.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ class BindingData final
305305

306306
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
307307

308+
v8::Local<v8::String> error_name_string(constchar* name);
309+
308310
size_t current_ngtcp2_memory_ = 0;
309311

310312
// The following set up various storage and accessors for common strings,
@@ -357,6 +359,9 @@ class BindingData final
357359
QUIC_JS_CALLBACKS(V)
358360
#undef V
359361

362+
// Lazy cache backing error_name_string()
363+
std::unordered_map<constchar*, v8::Eternal<v8::String>> error_name_strings_;
364+
360365
std::unique_ptr<SessionManager> session_manager_;
361366

362367
// Type-erased arena storage. The concrete AliasedStructArena<T> types

‎src/quic/data.cc‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#if HAVE_OPENSSL && HAVE_QUIC
22
#include"guard.h"
33
#ifndef OPENSSL_NO_QUIC
4-
#include"data.h"
54
#include<env-inl.h>
65
#include<memory_tracker-inl.h>
76
#include<ngtcp2/ngtcp2.h>
87
#include<node_sockaddr-inl.h>
8+
#include<openssl/ssl.h>
99
#include<string_bytes.h>
1010
#include<v8.h>
11+
#include"bindingdata.h"
12+
#include"data.h"
1113
#include"defs.h"
1214
#include"util.h"
1315

@@ -363,6 +365,62 @@ std::optional<int> QuicError::get_crypto_error() const {
363365
returncode() & ~NGTCP2_CRYPTO_ERROR;
364366
}
365367

368+
constchar* QuicError::name() const {
369+
// CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8).
370+
// OpenSSL's SSL_alert_desc_string_long owns a stable string for every
371+
// alert it knows about; we filter out the "unknown" placeholder so the
372+
// JS side can present `errorName` as undefined for unrecognised alerts.
373+
if (auto alert = get_crypto_error()) {
374+
constchar* n = SSL_alert_desc_string_long(*alert);
375+
if (n != nullptr && std::string_view(n) != "unknown") return n;
376+
returnnullptr;
377+
}
378+
// Named transport-layer error codes from RFC 9000 sec. 20.1 (and the
379+
// RFC 9368 version-negotiation extension). Application error codes are
380+
// opaque to QUIC, so we only decode for transport.
381+
if (type() != Type::TRANSPORT) returnnullptr;
382+
switch (code()) {
383+
caseNGTCP2_NO_ERROR:
384+
return"NO_ERROR";
385+
caseNGTCP2_INTERNAL_ERROR:
386+
return"INTERNAL_ERROR";
387+
caseNGTCP2_CONNECTION_REFUSED:
388+
return"CONNECTION_REFUSED";
389+
caseNGTCP2_FLOW_CONTROL_ERROR:
390+
return"FLOW_CONTROL_ERROR";
391+
caseNGTCP2_STREAM_LIMIT_ERROR:
392+
return"STREAM_LIMIT_ERROR";
393+
caseNGTCP2_STREAM_STATE_ERROR:
394+
return"STREAM_STATE_ERROR";
395+
caseNGTCP2_FINAL_SIZE_ERROR:
396+
return"FINAL_SIZE_ERROR";
397+
caseNGTCP2_FRAME_ENCODING_ERROR:
398+
return"FRAME_ENCODING_ERROR";
399+
caseNGTCP2_TRANSPORT_PARAMETER_ERROR:
400+
return"TRANSPORT_PARAMETER_ERROR";
401+
caseNGTCP2_CONNECTION_ID_LIMIT_ERROR:
402+
return"CONNECTION_ID_LIMIT_ERROR";
403+
caseNGTCP2_PROTOCOL_VIOLATION:
404+
return"PROTOCOL_VIOLATION";
405+
caseNGTCP2_INVALID_TOKEN:
406+
return"INVALID_TOKEN";
407+
caseNGTCP2_APPLICATION_ERROR:
408+
return"APPLICATION_ERROR";
409+
caseNGTCP2_CRYPTO_BUFFER_EXCEEDED:
410+
return"CRYPTO_BUFFER_EXCEEDED";
411+
caseNGTCP2_KEY_UPDATE_ERROR:
412+
return"KEY_UPDATE_ERROR";
413+
caseNGTCP2_AEAD_LIMIT_REACHED:
414+
return"AEAD_LIMIT_REACHED";
415+
caseNGTCP2_NO_VIABLE_PATH:
416+
return"NO_VIABLE_PATH";
417+
caseNGTCP2_VERSION_NEGOTIATION_ERROR:
418+
return"VERSION_NEGOTIATION_ERROR";
419+
default:
420+
returnnullptr;
421+
}
422+
}
423+
366424
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
367425
if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
368426
(type() == Type::APPLICATION &&
@@ -384,6 +442,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
384442
type_str,
385443
BigInt::NewFromUnsigned(env->isolate(), code()),
386444
Undefined(env->isolate()),
445+
Undefined(env->isolate()),
387446
};
388447

389448
// Note that per the QUIC specification, the reason, if present, is
@@ -397,6 +456,13 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
397456
return {};
398457
}
399458

459+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
460+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
461+
// codes leave the slot as undefined.
462+
if (constchar* n = name()) {
463+
argv[3] = BindingData::Get(env).error_name_string(n);
464+
}
465+
400466
returnArray::New(env->isolate(), argv, arraysize(argv)).As<Value>();
401467
}
402468

‎src/quic/data.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ class QuicError final : public MemoryRetainer {
265265
boolis_crypto_error() const;
266266
std::optional<int> get_crypto_error() const;
267267

268+
// Returns a human-readable name for this error if known, or nullptr
269+
constchar* name() const;
270+
268271
// Note that since application errors are application-specific and we
269272
// don't know which application is being used here, it is possible that
270273
// the comparing two different QuicError instances from different applications

‎src/quic/session.cc‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3073,7 +3073,7 @@ void Session::CheckStreamIdleTimeout(uint64_t now) {
30733073
// Without this, the peer's stream sits orphaned until the
30743074
// session closes.
30753075
auto error =
3076-
QuicError::ForTransport(NGTCP2_ERR_PROTO, "stream idle timeout");
3076+
QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout");
30773077
ShutdownStream(id, error);
30783078
stream->Destroy(error);
30793079
STAT_INCREMENT(Stats, streams_idle_timed_out);
@@ -3459,12 +3459,21 @@ void Session::EmitClose(const QuicError& error) {
34593459
Integer::New(env()->isolate(), static_cast<int>(error.type())),
34603460
BigInt::NewFromUnsigned(env()->isolate(), error.code()),
34613461
Undefined(env()->isolate()),
3462+
Undefined(env()->isolate()),
34623463
};
34633464
if (error.reason().length() > 0 &&
34643465
!ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) {
34653466
return;
34663467
}
34673468

3469+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
3470+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
3471+
// codes leave the slot as undefined. See QuicError::name() for the
3472+
// matching path on stream-level errors.
3473+
if (constchar* n = error.name()) {
3474+
argv[3] = BindingData::Get(env()).error_name_string(n);
3475+
}
3476+
34683477
MakeCallback(
34693478
BindingData::Get(env()).session_close_callback(), arraysize(argv), argv);
34703479

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 3b59d12

Browse files
pimterryaduh95
authored andcommitted
quic: add proper error codes & messages for QUIC failures
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #63198 Backport-PR-URL: #64675 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent b68f8ea commit 3b59d12

13 files changed

Lines changed: 186 additions & 58 deletions

‎lib/eslint.config_partial.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const noRestrictedSyntax = [
2323
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
2424
},
2525
{
26-
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
26+
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError)$/])',
2727
message: "Use an error exported by 'internal/errors' instead.",
2828
},
2929
{

‎lib/internal/errors.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,14 +1676,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
16761676
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS','%s',TypeError);
16771677
E('ERR_PROXY_INVALID_CONFIG','%s',Error);
16781678
E('ERR_PROXY_TUNNEL','%s',Error);
1679-
E('ERR_QUIC_APPLICATION_ERROR','A QUIC application error occurred. %d [%s]',Error);
16801679
E('ERR_QUIC_CONNECTION_FAILED','QUIC connection failed',Error);
16811680
E('ERR_QUIC_ENDPOINT_CLOSED','QUIC endpoint closed: %s (%d)',Error);
16821681
E('ERR_QUIC_OPEN_STREAM_FAILED','Failed to open QUIC stream',Error);
16831682
E('ERR_QUIC_STREAM_ABORTED','%s',Error);
16841683
E('ERR_QUIC_STREAM_RESET',
16851684
'The QUIC stream was reset by the peer with error code %d',Error);
1686-
E('ERR_QUIC_TRANSPORT_ERROR','A QUIC transport error occurred. %d [%s]',Error);
16871685
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR','The QUIC session requires version negotiation',Error);
16881686
E('ERR_REQUIRE_ASYNC_MODULE',function(filename,parentFilename){
16891687
letmessage='require() cannot be used on an ESM '+

‎lib/internal/quic/quic.js‎

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
ArrayPrototypePush,
1010
BigInt,
1111
DataViewPrototypeGetByteLength,
12+
ErrorCaptureStackTrace,
1213
FunctionPrototypeBind,
1314
Number,
1415
ObjectDefineProperties,
@@ -108,13 +109,11 @@ const {
108109
ERR_INVALID_THIS,
109110
ERR_MISSING_ARGS,
110111
ERR_OUT_OF_RANGE,
111-
ERR_QUIC_APPLICATION_ERROR,
112112
ERR_QUIC_CONNECTION_FAILED,
113113
ERR_QUIC_ENDPOINT_CLOSED,
114114
ERR_QUIC_OPEN_STREAM_FAILED,
115115
ERR_QUIC_STREAM_ABORTED,
116116
ERR_QUIC_STREAM_RESET,
117-
ERR_QUIC_TRANSPORT_ERROR,
118117
ERR_QUIC_VERSION_NEGOTIATION_ERROR,
119118
},
120119
}=require('internal/errors');
@@ -738,10 +737,12 @@ setCallbacks({
738737
* @param {number} errorType
739738
* @param {number} code
740739
* @param {string} [reason]
740+
* @param {string} [errorName] Decoded TLS alert name when `code` is a
741+
* CRYPTO_ERROR; otherwise undefined.
741742
*/
742-
onSessionClose(errorType,code,reason){
743-
debug('session close callback',errorType,code,reason);
744-
this[kOwner][kFinishClose](errorType,code,reason);
743+
onSessionClose(errorType,code,reason,errorName){
744+
debug('session close callback',errorType,code,reason,errorName);
745+
this[kOwner][kFinishClose](errorType,code,reason,errorName);
745746
},
746747

747748
/**
@@ -931,8 +932,12 @@ setCallbacks({
931932
// was an abnormal termination even if the session closed cleanly.
932933
constresetCode=getQuicStreamState(this[kOwner]).resetCode;
933934
if(resetCode!==undefined&&resetCode>0n){
934-
error=newERR_QUIC_APPLICATION_ERROR(
935-
resetCode,`stream reset with code ${resetCode}`);
935+
error=makeQuicError(
936+
'ERR_QUIC_APPLICATION_ERROR',
937+
'QUIC application error',
938+
'application',
939+
resetCode,
940+
`stream reset with code ${resetCode}`);
936941
}
937942
}
938943
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
@@ -1054,21 +1059,50 @@ class QuicError extends Error {
10541059
}
10551060
}
10561061

1057-
// Converts a raw QuicError array [type, code, reason] from C++ into a
1058-
// proper Node.js Error object.
1062+
// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or
1063+
// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for
1064+
// the wire code when known: either the OpenSSL-decoded TLS alert
1065+
// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes
1066+
// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined.
1067+
// `reason` is the peer-supplied UTF-8 reason string from the
1068+
// CONNECTION_CLOSE / RESET_STREAM frame, often empty.
1069+
functionquicErrorMessage(prefix,errorCode,reason,errorName){
1070+
letmsg=`${prefix} `;
1071+
msg+=errorName ? `${errorName} (${errorCode})` : `${errorCode}`;
1072+
if(reason)msg+=`: ${reason}`;
1073+
returnmsg;
1074+
}
1075+
1076+
functionmakeQuicError(code,prefix,type,errorCode,reason,errorName){
1077+
consterr=newQuicError(
1078+
quicErrorMessage(prefix,errorCode,reason,errorName),
1079+
{ errorCode, code, type });
1080+
ErrorCaptureStackTrace(err,makeQuicError);
1081+
if(reason)err.reason=reason;
1082+
if(errorName)err.errorName=errorName;
1083+
returnerr;
1084+
}
1085+
10591086
functionconvertQuicError(error){
10601087
consttype=error[0];
10611088
constcode=error[1];
10621089
constreason=error[2];
1090+
consterrorName=error[3];
10631091
switch(type){
10641092
case'transport':
1065-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1093+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1094+
'QUIC transport error',
1095+
'transport',code,reason,errorName);
10661096
case'application':
1067-
returnnewERR_QUIC_APPLICATION_ERROR(code,reason);
1097+
returnmakeQuicError('ERR_QUIC_APPLICATION_ERROR',
1098+
'QUIC application error',
1099+
'application',code,reason,errorName);
10681100
case'version_negotiation':
10691101
returnnewERR_QUIC_VERSION_NEGOTIATION_ERROR();
10701102
default:
1071-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1103+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1104+
'QUIC transport error',
1105+
'transport',code,reason,errorName);
10721106
}
10731107
}
10741108

@@ -3575,7 +3609,7 @@ class QuicSession {
35753609
* @param {number} code
35763610
* @param {string} [reason]
35773611
*/
3578-
[kFinishClose](errorType,code,reason){
3612+
[kFinishClose](errorType,code,reason,errorName){
35793613
// If code is zero, then we closed without an error. Yay! We can destroy
35803614
// safely without specifying an error.
35813615
if(code===0n){
@@ -3584,7 +3618,8 @@ class QuicSession {
35843618
return;
35853619
}
35863620

3587-
debug('finishing closing the session with an error',errorType,code,reason);
3621+
debug('finishing closing the session with an error',
3622+
errorType,code,reason,errorName);
35883623

35893624
// If the local side initiated this close with an error code (via
35903625
// close({ code })), this is an intentional shutdown; not an error.
@@ -3611,10 +3646,14 @@ class QuicSession {
36113646
// session would leak with `closed` hanging forever.
36123647
switch(errorType){
36133648
case0: /* Transport Error */
3614-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3649+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3650+
'QUIC transport error',
3651+
'transport',code,reason,errorName));
36153652
break;
36163653
case1: /* Application Error */
3617-
this.destroy(newERR_QUIC_APPLICATION_ERROR(code,reason));
3654+
this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR',
3655+
'QUIC application error',
3656+
'application',code,reason,errorName));
36183657
break;
36193658
case2: /* Version Negotiation Error */
36203659
this.destroy(newERR_QUIC_VERSION_NEGOTIATION_ERROR());
@@ -3623,7 +3662,9 @@ class QuicSession {
36233662
this.destroy();
36243663
break;
36253664
default:
3626-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3665+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3666+
'QUIC transport error',
3667+
'transport',code,reason,errorName));
36273668
break;
36283669
}
36293670
}
@@ -3874,9 +3915,13 @@ class QuicSession {
38743915
// decide. In 'strict' mode, the handshake already failed at the C++
38753916
// level (SSL_VERIFY_PEER) so we won't reach here.
38763917
if(inner.verifyPeer==='auto'&&validationErrorReason!==undefined){
3877-
consterr=newERR_QUIC_TRANSPORT_ERROR(
3878-
0,`Peer certificate validation failed: ${validationErrorReason}`+
3879-
` [${validationErrorCode}]`);
3918+
consterr=makeQuicError(
3919+
'ERR_QUIC_TRANSPORT_ERROR',
3920+
'QUIC transport error',
3921+
'transport',
3922+
0n,
3923+
`Peer certificate validation failed: ${validationErrorReason}`+
3924+
` [${validationErrorCode}]`);
38803925
inner.pendingOpen.reject?.(err);
38813926
inner.pendingOpen.resolve=undefined;
38823927
inner.pendingOpen.reject=undefined;

‎src/quic/bindingdata.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V)
435435

436436
#undef V
437437

438+
Local<String> BindingData::error_name_string(constchar* name) {
439+
auto& slot = error_name_strings_[name];
440+
if (slot.IsEmpty()) {
441+
slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
442+
}
443+
return slot.Get(env()->isolate());
444+
}
445+
438446
JS_METHOD_IMPL(BindingData::SetCallbacks) {
439447
auto env = Environment::GetCurrent(args);
440448
auto isolate = env->isolate();

‎src/quic/bindingdata.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ class BindingData final
305305

306306
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
307307

308+
v8::Local<v8::String> error_name_string(constchar* name);
309+
308310
size_t current_ngtcp2_memory_ = 0;
309311

310312
// The following set up various storage and accessors for common strings,
@@ -357,6 +359,9 @@ class BindingData final
357359
QUIC_JS_CALLBACKS(V)
358360
#undef V
359361

362+
// Lazy cache backing error_name_string()
363+
std::unordered_map<constchar*, v8::Eternal<v8::String>> error_name_strings_;
364+
360365
std::unique_ptr<SessionManager> session_manager_;
361366

362367
// Type-erased arena storage. The concrete AliasedStructArena<T> types

‎src/quic/data.cc‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#if HAVE_OPENSSL && HAVE_QUIC
22
#include"guard.h"
33
#ifndef OPENSSL_NO_QUIC
4-
#include"data.h"
54
#include<env-inl.h>
65
#include<memory_tracker-inl.h>
76
#include<ngtcp2/ngtcp2.h>
87
#include<node_sockaddr-inl.h>
8+
#include<openssl/ssl.h>
99
#include<string_bytes.h>
1010
#include<v8.h>
11+
#include"bindingdata.h"
12+
#include"data.h"
1113
#include"defs.h"
1214
#include"util.h"
1315

@@ -363,6 +365,62 @@ std::optional<int> QuicError::get_crypto_error() const {
363365
returncode() & ~NGTCP2_CRYPTO_ERROR;
364366
}
365367

368+
constchar* QuicError::name() const {
369+
// CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8).
370+
// OpenSSL's SSL_alert_desc_string_long owns a stable string for every
371+
// alert it knows about; we filter out the "unknown" placeholder so the
372+
// JS side can present `errorName` as undefined for unrecognised alerts.
373+
if (auto alert = get_crypto_error()) {
374+
constchar* n = SSL_alert_desc_string_long(*alert);
375+
if (n != nullptr && std::string_view(n) != "unknown") return n;
376+
returnnullptr;
377+
}
378+
// Named transport-layer error codes from RFC 9000 sec. 20.1 (and the
379+
// RFC 9368 version-negotiation extension). Application error codes are
380+
// opaque to QUIC, so we only decode for transport.
381+
if (type() != Type::TRANSPORT) returnnullptr;
382+
switch (code()) {
383+
caseNGTCP2_NO_ERROR:
384+
return"NO_ERROR";
385+
caseNGTCP2_INTERNAL_ERROR:
386+
return"INTERNAL_ERROR";
387+
caseNGTCP2_CONNECTION_REFUSED:
388+
return"CONNECTION_REFUSED";
389+
caseNGTCP2_FLOW_CONTROL_ERROR:
390+
return"FLOW_CONTROL_ERROR";
391+
caseNGTCP2_STREAM_LIMIT_ERROR:
392+
return"STREAM_LIMIT_ERROR";
393+
caseNGTCP2_STREAM_STATE_ERROR:
394+
return"STREAM_STATE_ERROR";
395+
caseNGTCP2_FINAL_SIZE_ERROR:
396+
return"FINAL_SIZE_ERROR";
397+
caseNGTCP2_FRAME_ENCODING_ERROR:
398+
return"FRAME_ENCODING_ERROR";
399+
caseNGTCP2_TRANSPORT_PARAMETER_ERROR:
400+
return"TRANSPORT_PARAMETER_ERROR";
401+
caseNGTCP2_CONNECTION_ID_LIMIT_ERROR:
402+
return"CONNECTION_ID_LIMIT_ERROR";
403+
caseNGTCP2_PROTOCOL_VIOLATION:
404+
return"PROTOCOL_VIOLATION";
405+
caseNGTCP2_INVALID_TOKEN:
406+
return"INVALID_TOKEN";
407+
caseNGTCP2_APPLICATION_ERROR:
408+
return"APPLICATION_ERROR";
409+
caseNGTCP2_CRYPTO_BUFFER_EXCEEDED:
410+
return"CRYPTO_BUFFER_EXCEEDED";
411+
caseNGTCP2_KEY_UPDATE_ERROR:
412+
return"KEY_UPDATE_ERROR";
413+
caseNGTCP2_AEAD_LIMIT_REACHED:
414+
return"AEAD_LIMIT_REACHED";
415+
caseNGTCP2_NO_VIABLE_PATH:
416+
return"NO_VIABLE_PATH";
417+
caseNGTCP2_VERSION_NEGOTIATION_ERROR:
418+
return"VERSION_NEGOTIATION_ERROR";
419+
default:
420+
returnnullptr;
421+
}
422+
}
423+
366424
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
367425
if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
368426
(type() == Type::APPLICATION &&
@@ -384,6 +442,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
384442
type_str,
385443
BigInt::NewFromUnsigned(env->isolate(), code()),
386444
Undefined(env->isolate()),
445+
Undefined(env->isolate()),
387446
};
388447

389448
// Note that per the QUIC specification, the reason, if present, is
@@ -397,6 +456,13 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
397456
return {};
398457
}
399458

459+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
460+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
461+
// codes leave the slot as undefined.
462+
if (constchar* n = name()) {
463+
argv[3] = BindingData::Get(env).error_name_string(n);
464+
}
465+
400466
returnArray::New(env->isolate(), argv, arraysize(argv)).As<Value>();
401467
}
402468

‎src/quic/data.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ class QuicError final : public MemoryRetainer {
265265
boolis_crypto_error() const;
266266
std::optional<int> get_crypto_error() const;
267267

268+
// Returns a human-readable name for this error if known, or nullptr
269+
constchar* name() const;
270+
268271
// Note that since application errors are application-specific and we
269272
// don't know which application is being used here, it is possible that
270273
// the comparing two different QuicError instances from different applications

‎src/quic/session.cc‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3073,7 +3073,7 @@ void Session::CheckStreamIdleTimeout(uint64_t now) {
30733073
// Without this, the peer's stream sits orphaned until the
30743074
// session closes.
30753075
auto error =
3076-
QuicError::ForTransport(NGTCP2_ERR_PROTO, "stream idle timeout");
3076+
QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout");
30773077
ShutdownStream(id, error);
30783078
stream->Destroy(error);
30793079
STAT_INCREMENT(Stats, streams_idle_timed_out);
@@ -3459,12 +3459,21 @@ void Session::EmitClose(const QuicError& error) {
34593459
Integer::New(env()->isolate(), static_cast<int>(error.type())),
34603460
BigInt::NewFromUnsigned(env()->isolate(), error.code()),
34613461
Undefined(env()->isolate()),
3462+
Undefined(env()->isolate()),
34623463
};
34633464
if (error.reason().length() > 0 &&
34643465
!ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) {
34653466
return;
34663467
}
34673468

3469+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
3470+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
3471+
// codes leave the slot as undefined. See QuicError::name() for the
3472+
// matching path on stream-level errors.
3473+
if (constchar* n = error.name()) {
3474+
argv[3] = BindingData::Get(env()).error_name_string(n);
3475+
}
3476+
34683477
MakeCallback(
34693478
BindingData::Get(env()).session_close_callback(), arraysize(argv), argv);
34703479

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 3b59d12

Browse files
pimterryaduh95
authored andcommitted
quic: add proper error codes & messages for QUIC failures
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #63198 Backport-PR-URL: #64675 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent b68f8ea commit 3b59d12

13 files changed

Lines changed: 186 additions & 58 deletions

‎lib/eslint.config_partial.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const noRestrictedSyntax = [
2323
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
2424
},
2525
{
26-
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
26+
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError)$/])',
2727
message: "Use an error exported by 'internal/errors' instead.",
2828
},
2929
{

‎lib/internal/errors.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,14 +1676,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
16761676
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS','%s',TypeError);
16771677
E('ERR_PROXY_INVALID_CONFIG','%s',Error);
16781678
E('ERR_PROXY_TUNNEL','%s',Error);
1679-
E('ERR_QUIC_APPLICATION_ERROR','A QUIC application error occurred. %d [%s]',Error);
16801679
E('ERR_QUIC_CONNECTION_FAILED','QUIC connection failed',Error);
16811680
E('ERR_QUIC_ENDPOINT_CLOSED','QUIC endpoint closed: %s (%d)',Error);
16821681
E('ERR_QUIC_OPEN_STREAM_FAILED','Failed to open QUIC stream',Error);
16831682
E('ERR_QUIC_STREAM_ABORTED','%s',Error);
16841683
E('ERR_QUIC_STREAM_RESET',
16851684
'The QUIC stream was reset by the peer with error code %d',Error);
1686-
E('ERR_QUIC_TRANSPORT_ERROR','A QUIC transport error occurred. %d [%s]',Error);
16871685
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR','The QUIC session requires version negotiation',Error);
16881686
E('ERR_REQUIRE_ASYNC_MODULE',function(filename,parentFilename){
16891687
letmessage='require() cannot be used on an ESM '+

‎lib/internal/quic/quic.js‎

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
ArrayPrototypePush,
1010
BigInt,
1111
DataViewPrototypeGetByteLength,
12+
ErrorCaptureStackTrace,
1213
FunctionPrototypeBind,
1314
Number,
1415
ObjectDefineProperties,
@@ -108,13 +109,11 @@ const {
108109
ERR_INVALID_THIS,
109110
ERR_MISSING_ARGS,
110111
ERR_OUT_OF_RANGE,
111-
ERR_QUIC_APPLICATION_ERROR,
112112
ERR_QUIC_CONNECTION_FAILED,
113113
ERR_QUIC_ENDPOINT_CLOSED,
114114
ERR_QUIC_OPEN_STREAM_FAILED,
115115
ERR_QUIC_STREAM_ABORTED,
116116
ERR_QUIC_STREAM_RESET,
117-
ERR_QUIC_TRANSPORT_ERROR,
118117
ERR_QUIC_VERSION_NEGOTIATION_ERROR,
119118
},
120119
}=require('internal/errors');
@@ -738,10 +737,12 @@ setCallbacks({
738737
* @param {number} errorType
739738
* @param {number} code
740739
* @param {string} [reason]
740+
* @param {string} [errorName] Decoded TLS alert name when `code` is a
741+
* CRYPTO_ERROR; otherwise undefined.
741742
*/
742-
onSessionClose(errorType,code,reason){
743-
debug('session close callback',errorType,code,reason);
744-
this[kOwner][kFinishClose](errorType,code,reason);
743+
onSessionClose(errorType,code,reason,errorName){
744+
debug('session close callback',errorType,code,reason,errorName);
745+
this[kOwner][kFinishClose](errorType,code,reason,errorName);
745746
},
746747

747748
/**
@@ -931,8 +932,12 @@ setCallbacks({
931932
// was an abnormal termination even if the session closed cleanly.
932933
constresetCode=getQuicStreamState(this[kOwner]).resetCode;
933934
if(resetCode!==undefined&&resetCode>0n){
934-
error=newERR_QUIC_APPLICATION_ERROR(
935-
resetCode,`stream reset with code ${resetCode}`);
935+
error=makeQuicError(
936+
'ERR_QUIC_APPLICATION_ERROR',
937+
'QUIC application error',
938+
'application',
939+
resetCode,
940+
`stream reset with code ${resetCode}`);
936941
}
937942
}
938943
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
@@ -1054,21 +1059,50 @@ class QuicError extends Error {
10541059
}
10551060
}
10561061

1057-
// Converts a raw QuicError array [type, code, reason] from C++ into a
1058-
// proper Node.js Error object.
1062+
// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or
1063+
// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for
1064+
// the wire code when known: either the OpenSSL-decoded TLS alert
1065+
// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes
1066+
// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined.
1067+
// `reason` is the peer-supplied UTF-8 reason string from the
1068+
// CONNECTION_CLOSE / RESET_STREAM frame, often empty.
1069+
functionquicErrorMessage(prefix,errorCode,reason,errorName){
1070+
letmsg=`${prefix} `;
1071+
msg+=errorName ? `${errorName} (${errorCode})` : `${errorCode}`;
1072+
if(reason)msg+=`: ${reason}`;
1073+
returnmsg;
1074+
}
1075+
1076+
functionmakeQuicError(code,prefix,type,errorCode,reason,errorName){
1077+
consterr=newQuicError(
1078+
quicErrorMessage(prefix,errorCode,reason,errorName),
1079+
{ errorCode, code, type });
1080+
ErrorCaptureStackTrace(err,makeQuicError);
1081+
if(reason)err.reason=reason;
1082+
if(errorName)err.errorName=errorName;
1083+
returnerr;
1084+
}
1085+
10591086
functionconvertQuicError(error){
10601087
consttype=error[0];
10611088
constcode=error[1];
10621089
constreason=error[2];
1090+
consterrorName=error[3];
10631091
switch(type){
10641092
case'transport':
1065-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1093+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1094+
'QUIC transport error',
1095+
'transport',code,reason,errorName);
10661096
case'application':
1067-
returnnewERR_QUIC_APPLICATION_ERROR(code,reason);
1097+
returnmakeQuicError('ERR_QUIC_APPLICATION_ERROR',
1098+
'QUIC application error',
1099+
'application',code,reason,errorName);
10681100
case'version_negotiation':
10691101
returnnewERR_QUIC_VERSION_NEGOTIATION_ERROR();
10701102
default:
1071-
returnnewERR_QUIC_TRANSPORT_ERROR(code,reason);
1103+
returnmakeQuicError('ERR_QUIC_TRANSPORT_ERROR',
1104+
'QUIC transport error',
1105+
'transport',code,reason,errorName);
10721106
}
10731107
}
10741108

@@ -3575,7 +3609,7 @@ class QuicSession {
35753609
* @param {number} code
35763610
* @param {string} [reason]
35773611
*/
3578-
[kFinishClose](errorType,code,reason){
3612+
[kFinishClose](errorType,code,reason,errorName){
35793613
// If code is zero, then we closed without an error. Yay! We can destroy
35803614
// safely without specifying an error.
35813615
if(code===0n){
@@ -3584,7 +3618,8 @@ class QuicSession {
35843618
return;
35853619
}
35863620

3587-
debug('finishing closing the session with an error',errorType,code,reason);
3621+
debug('finishing closing the session with an error',
3622+
errorType,code,reason,errorName);
35883623

35893624
// If the local side initiated this close with an error code (via
35903625
// close({ code })), this is an intentional shutdown; not an error.
@@ -3611,10 +3646,14 @@ class QuicSession {
36113646
// session would leak with `closed` hanging forever.
36123647
switch(errorType){
36133648
case0: /* Transport Error */
3614-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3649+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3650+
'QUIC transport error',
3651+
'transport',code,reason,errorName));
36153652
break;
36163653
case1: /* Application Error */
3617-
this.destroy(newERR_QUIC_APPLICATION_ERROR(code,reason));
3654+
this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR',
3655+
'QUIC application error',
3656+
'application',code,reason,errorName));
36183657
break;
36193658
case2: /* Version Negotiation Error */
36203659
this.destroy(newERR_QUIC_VERSION_NEGOTIATION_ERROR());
@@ -3623,7 +3662,9 @@ class QuicSession {
36233662
this.destroy();
36243663
break;
36253664
default:
3626-
this.destroy(newERR_QUIC_TRANSPORT_ERROR(code,reason));
3665+
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
3666+
'QUIC transport error',
3667+
'transport',code,reason,errorName));
36273668
break;
36283669
}
36293670
}
@@ -3874,9 +3915,13 @@ class QuicSession {
38743915
// decide. In 'strict' mode, the handshake already failed at the C++
38753916
// level (SSL_VERIFY_PEER) so we won't reach here.
38763917
if(inner.verifyPeer==='auto'&&validationErrorReason!==undefined){
3877-
consterr=newERR_QUIC_TRANSPORT_ERROR(
3878-
0,`Peer certificate validation failed: ${validationErrorReason}`+
3879-
` [${validationErrorCode}]`);
3918+
consterr=makeQuicError(
3919+
'ERR_QUIC_TRANSPORT_ERROR',
3920+
'QUIC transport error',
3921+
'transport',
3922+
0n,
3923+
`Peer certificate validation failed: ${validationErrorReason}`+
3924+
` [${validationErrorCode}]`);
38803925
inner.pendingOpen.reject?.(err);
38813926
inner.pendingOpen.resolve=undefined;
38823927
inner.pendingOpen.reject=undefined;

‎src/quic/bindingdata.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V)
435435

436436
#undef V
437437

438+
Local<String> BindingData::error_name_string(constchar* name) {
439+
auto& slot = error_name_strings_[name];
440+
if (slot.IsEmpty()) {
441+
slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
442+
}
443+
return slot.Get(env()->isolate());
444+
}
445+
438446
JS_METHOD_IMPL(BindingData::SetCallbacks) {
439447
auto env = Environment::GetCurrent(args);
440448
auto isolate = env->isolate();

‎src/quic/bindingdata.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ class BindingData final
305305

306306
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
307307

308+
v8::Local<v8::String> error_name_string(constchar* name);
309+
308310
size_t current_ngtcp2_memory_ = 0;
309311

310312
// The following set up various storage and accessors for common strings,
@@ -357,6 +359,9 @@ class BindingData final
357359
QUIC_JS_CALLBACKS(V)
358360
#undef V
359361

362+
// Lazy cache backing error_name_string()
363+
std::unordered_map<constchar*, v8::Eternal<v8::String>> error_name_strings_;
364+
360365
std::unique_ptr<SessionManager> session_manager_;
361366

362367
// Type-erased arena storage. The concrete AliasedStructArena<T> types

‎src/quic/data.cc‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#if HAVE_OPENSSL && HAVE_QUIC
22
#include"guard.h"
33
#ifndef OPENSSL_NO_QUIC
4-
#include"data.h"
54
#include<env-inl.h>
65
#include<memory_tracker-inl.h>
76
#include<ngtcp2/ngtcp2.h>
87
#include<node_sockaddr-inl.h>
8+
#include<openssl/ssl.h>
99
#include<string_bytes.h>
1010
#include<v8.h>
11+
#include"bindingdata.h"
12+
#include"data.h"
1113
#include"defs.h"
1214
#include"util.h"
1315

@@ -363,6 +365,62 @@ std::optional<int> QuicError::get_crypto_error() const {
363365
returncode() & ~NGTCP2_CRYPTO_ERROR;
364366
}
365367

368+
constchar* QuicError::name() const {
369+
// CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8).
370+
// OpenSSL's SSL_alert_desc_string_long owns a stable string for every
371+
// alert it knows about; we filter out the "unknown" placeholder so the
372+
// JS side can present `errorName` as undefined for unrecognised alerts.
373+
if (auto alert = get_crypto_error()) {
374+
constchar* n = SSL_alert_desc_string_long(*alert);
375+
if (n != nullptr && std::string_view(n) != "unknown") return n;
376+
returnnullptr;
377+
}
378+
// Named transport-layer error codes from RFC 9000 sec. 20.1 (and the
379+
// RFC 9368 version-negotiation extension). Application error codes are
380+
// opaque to QUIC, so we only decode for transport.
381+
if (type() != Type::TRANSPORT) returnnullptr;
382+
switch (code()) {
383+
caseNGTCP2_NO_ERROR:
384+
return"NO_ERROR";
385+
caseNGTCP2_INTERNAL_ERROR:
386+
return"INTERNAL_ERROR";
387+
caseNGTCP2_CONNECTION_REFUSED:
388+
return"CONNECTION_REFUSED";
389+
caseNGTCP2_FLOW_CONTROL_ERROR:
390+
return"FLOW_CONTROL_ERROR";
391+
caseNGTCP2_STREAM_LIMIT_ERROR:
392+
return"STREAM_LIMIT_ERROR";
393+
caseNGTCP2_STREAM_STATE_ERROR:
394+
return"STREAM_STATE_ERROR";
395+
caseNGTCP2_FINAL_SIZE_ERROR:
396+
return"FINAL_SIZE_ERROR";
397+
caseNGTCP2_FRAME_ENCODING_ERROR:
398+
return"FRAME_ENCODING_ERROR";
399+
caseNGTCP2_TRANSPORT_PARAMETER_ERROR:
400+
return"TRANSPORT_PARAMETER_ERROR";
401+
caseNGTCP2_CONNECTION_ID_LIMIT_ERROR:
402+
return"CONNECTION_ID_LIMIT_ERROR";
403+
caseNGTCP2_PROTOCOL_VIOLATION:
404+
return"PROTOCOL_VIOLATION";
405+
caseNGTCP2_INVALID_TOKEN:
406+
return"INVALID_TOKEN";
407+
caseNGTCP2_APPLICATION_ERROR:
408+
return"APPLICATION_ERROR";
409+
caseNGTCP2_CRYPTO_BUFFER_EXCEEDED:
410+
return"CRYPTO_BUFFER_EXCEEDED";
411+
caseNGTCP2_KEY_UPDATE_ERROR:
412+
return"KEY_UPDATE_ERROR";
413+
caseNGTCP2_AEAD_LIMIT_REACHED:
414+
return"AEAD_LIMIT_REACHED";
415+
caseNGTCP2_NO_VIABLE_PATH:
416+
return"NO_VIABLE_PATH";
417+
caseNGTCP2_VERSION_NEGOTIATION_ERROR:
418+
return"VERSION_NEGOTIATION_ERROR";
419+
default:
420+
returnnullptr;
421+
}
422+
}
423+
366424
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
367425
if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
368426
(type() == Type::APPLICATION &&
@@ -384,6 +442,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
384442
type_str,
385443
BigInt::NewFromUnsigned(env->isolate(), code()),
386444
Undefined(env->isolate()),
445+
Undefined(env->isolate()),
387446
};
388447

389448
// Note that per the QUIC specification, the reason, if present, is
@@ -397,6 +456,13 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
397456
return {};
398457
}
399458

459+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
460+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
461+
// codes leave the slot as undefined.
462+
if (constchar* n = name()) {
463+
argv[3] = BindingData::Get(env).error_name_string(n);
464+
}
465+
400466
returnArray::New(env->isolate(), argv, arraysize(argv)).As<Value>();
401467
}
402468

‎src/quic/data.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ class QuicError final : public MemoryRetainer {
265265
boolis_crypto_error() const;
266266
std::optional<int> get_crypto_error() const;
267267

268+
// Returns a human-readable name for this error if known, or nullptr
269+
constchar* name() const;
270+
268271
// Note that since application errors are application-specific and we
269272
// don't know which application is being used here, it is possible that
270273
// the comparing two different QuicError instances from different applications

‎src/quic/session.cc‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3073,7 +3073,7 @@ void Session::CheckStreamIdleTimeout(uint64_t now) {
30733073
// Without this, the peer's stream sits orphaned until the
30743074
// session closes.
30753075
auto error =
3076-
QuicError::ForTransport(NGTCP2_ERR_PROTO, "stream idle timeout");
3076+
QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout");
30773077
ShutdownStream(id, error);
30783078
stream->Destroy(error);
30793079
STAT_INCREMENT(Stats, streams_idle_timed_out);
@@ -3459,12 +3459,21 @@ void Session::EmitClose(const QuicError& error) {
34593459
Integer::New(env()->isolate(), static_cast<int>(error.type())),
34603460
BigInt::NewFromUnsigned(env()->isolate(), error.code()),
34613461
Undefined(env()->isolate()),
3462+
Undefined(env()->isolate()),
34623463
};
34633464
if (error.reason().length() > 0 &&
34643465
!ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) {
34653466
return;
34663467
}
34673468

3469+
// Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1
3470+
// names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown
3471+
// codes leave the slot as undefined. See QuicError::name() for the
3472+
// matching path on stream-level errors.
3473+
if (constchar* n = error.name()) {
3474+
argv[3] = BindingData::Get(env()).error_name_string(n);
3475+
}
3476+
34683477
MakeCallback(
34693478
BindingData::Get(env()).session_close_callback(), arraysize(argv), argv);
34703479

0 commit comments

Comments
 (0)