mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
quic: reset rejected HTTP/3 request streams with H3_REQUEST_REJECTED
An incoming HTTP/3 request stream rejected without any application processing (the session has no stream consumer) is now reset with H3_REQUEST_REJECTED so the peer learns the request was not processed, per RFC 9114 section 4.1.1. The code is surfaced per-application like the existing no-error and internal-error codes, so non-HTTP/3 applications are unchanged. Fixes: https://github.com/nodejs/node/issues/65441 Signed-off-by: Naman Trivedi <trivenay@amazon.com> PR-URL: https://github.com/nodejs/node/pull/65442 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
This commit is contained in:
1 parent
82babee3e6
commit
da7b94e0ae
7 files changed
+96
-1
No files matched your search
@@ -4166,7 +4166,16 @@ class QuicSession {
|
||||
// case rather than letting it hold flow control credit.
|
||||
if (!this.#hasStreamConsumer()) {
|
||||
process.emitWarning('A new stream was received but no stream consumer callback was provided');
|
||||
stream.destroy();
|
||||
// When the negotiated application defines a "request rejected" code
|
||||
// (HTTP/3: H3_REQUEST_REJECTED), reset the stream with it so the peer
|
||||
// learns the request was not processed (RFC 9114 section 4.1.1).
|
||||
// Other applications have no such semantic and are torn down as before.
|
||||
const rejectedCode = getQuicSessionState(this).requestRejectedCode;
|
||||
if (getQuicSessionState(this).streamCallbacksSupported === 1) {
|
||||
stream.destroy(undefined, { code: rejectedCode });
|
||||
} else {
|
||||
stream.destroy();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ const {
|
||||
IDX_STATE_SESSION_APPLICATION_TYPE,
|
||||
IDX_STATE_SESSION_NO_ERROR_CODE,
|
||||
IDX_STATE_SESSION_INTERNAL_ERROR_CODE,
|
||||
IDX_STATE_SESSION_REQUEST_REJECTED_CODE,
|
||||
IDX_STATE_SESSION_MAX_DATAGRAM_SIZE,
|
||||
IDX_STATE_SESSION_LAST_DATAGRAM_ID,
|
||||
IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS,
|
||||
@@ -125,6 +126,7 @@ assert(IDX_STATE_SESSION_WRAPPED !== undefined);
|
||||
assert(IDX_STATE_SESSION_APPLICATION_TYPE !== undefined);
|
||||
assert(IDX_STATE_SESSION_NO_ERROR_CODE !== undefined);
|
||||
assert(IDX_STATE_SESSION_INTERNAL_ERROR_CODE !== undefined);
|
||||
assert(IDX_STATE_SESSION_REQUEST_REJECTED_CODE !== undefined);
|
||||
assert(IDX_STATE_SESSION_MAX_DATAGRAM_SIZE !== undefined);
|
||||
assert(IDX_STATE_SESSION_LAST_DATAGRAM_ID !== undefined);
|
||||
assert(IDX_STATE_ENDPOINT_BOUND !== undefined);
|
||||
@@ -552,6 +554,13 @@ class QuicSessionState {
|
||||
handle, this.#offset + IDX_STATE_SESSION_INTERNAL_ERROR_CODE, kIsLittleEndian);
|
||||
}
|
||||
|
||||
get requestRejectedCode() {
|
||||
const handle = this.#handle;
|
||||
if (handle === undefined) return undefined;
|
||||
return DataViewPrototypeGetBigUint64(
|
||||
handle, this.#offset + IDX_STATE_SESSION_REQUEST_REJECTED_CODE, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get maxDatagramSize() {
|
||||
const handle = this.#handle;
|
||||
|
||||
@@ -280,6 +280,11 @@ class DefaultApplication final : public Session::Application {
|
||||
return NGTCP2_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
// Raw QUIC has no "request rejected" semantic; reuse the no-error code.
|
||||
error_code GetRequestRejectedCode() const override {
|
||||
return GetNoErrorCode();
|
||||
}
|
||||
|
||||
void EarlyDataRejected() override {
|
||||
// Destroy all open streams — ngtcp2 has already discarded their
|
||||
// internal state when it rejected the early data. Use the
|
||||
|
||||
@@ -77,6 +77,14 @@ class Session::Application : public MemoryRetainer {
|
||||
// NGTCP2_INTERNAL_ERROR (0x1).
|
||||
virtual error_code GetInternalErrorCode() const = 0;
|
||||
|
||||
// The "request rejected" code is sent on RESET_STREAM when an incoming
|
||||
// request stream is rejected without any application processing (e.g.
|
||||
// the session has no consumer for it), so the peer learns the request
|
||||
// was not processed. For HTTP/3 this is NGHTTP3_H3_REQUEST_REJECTED
|
||||
// (0x10b); other applications have no such semantic and reuse the
|
||||
// "no error" code.
|
||||
virtual error_code GetRequestRejectedCode() const = 0;
|
||||
|
||||
// Called after Session::Receive processes a packet, outside all callback
|
||||
// scopes. Applications can use this to handle deferred operations that
|
||||
// require calling into JS (e.g., HTTP/3 GOAWAY processing).
|
||||
|
||||
@@ -172,6 +172,10 @@ class Http3ApplicationImpl final : public Session::Application {
|
||||
return NGHTTP3_H3_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
error_code GetRequestRejectedCode() const override {
|
||||
return NGHTTP3_H3_REQUEST_REJECTED;
|
||||
}
|
||||
|
||||
void EarlyDataRejected() override {
|
||||
// When 0-RTT is rejected, destroy the nghttp3 connection and all
|
||||
// open streams — ngtcp2 has discarded their internal state.
|
||||
|
||||
@@ -141,6 +141,7 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) {
|
||||
V(APPLICATION_TYPE, application_type, uint8_t) \
|
||||
V(NO_ERROR_CODE, no_error_code, error_code) \
|
||||
V(INTERNAL_ERROR_CODE, internal_error_code, error_code) \
|
||||
V(REQUEST_REJECTED_CODE, request_rejected_code, error_code) \
|
||||
V(MAX_DATAGRAM_SIZE, max_datagram_size, uint16_t) \
|
||||
V(LAST_DATAGRAM_ID, last_datagram_id, datagram_id) \
|
||||
V(MAX_PENDING_DATAGRAMS, max_pending_datagrams, uint16_t)
|
||||
@@ -2660,6 +2661,7 @@ void Session::SetApplication(std::unique_ptr<Application> app) {
|
||||
// without duplicating the per-application table.
|
||||
impl_->state()->no_error_code = app->GetNoErrorCode();
|
||||
impl_->state()->internal_error_code = app->GetInternalErrorCode();
|
||||
impl_->state()->request_rejected_code = app->GetRequestRejectedCode();
|
||||
impl_->application_ = std::move(app);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// An incoming HTTP/3 request stream that is rejected without any
|
||||
// application processing (here, the session has no stream consumer) is
|
||||
// reset with H3_REQUEST_REJECTED (0x10b) so the peer learns the request
|
||||
// was not processed. See RFC 9114 section 4.1.1.
|
||||
// Refs: https://github.com/nodejs/node/issues/65441
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(fixtures.readKey('agent1-key.pem'));
|
||||
const cert = fixtures.readKey('agent1-cert.pem');
|
||||
|
||||
// RFC 9114 H3_REQUEST_REJECTED.
|
||||
const H3_REQUEST_REJECTED = 0x10bn;
|
||||
|
||||
// The server registers no stream consumer, so an incoming request stream
|
||||
// is rejected on arrival.
|
||||
const serverEndpoint = await listen(mustCall((serverSession) => {
|
||||
serverSession.onerror = () => {};
|
||||
}), {
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
servername: 'localhost',
|
||||
verifyPeer: 'manual',
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
const reset = Promise.withResolvers();
|
||||
const stream = await clientSession.createBidirectionalStream({
|
||||
headers: {
|
||||
':method': 'GET',
|
||||
':path': '/test',
|
||||
':scheme': 'https',
|
||||
':authority': 'localhost',
|
||||
},
|
||||
});
|
||||
stream.onreset = mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR');
|
||||
assert.strictEqual(err.errorCode, H3_REQUEST_REJECTED);
|
||||
reset.resolve();
|
||||
});
|
||||
await assert.rejects(stream.closed, { code: 'ERR_QUIC_APPLICATION_ERROR' });
|
||||
|
||||
await reset.promise;
|
||||
await clientSession.close();
|
||||
await serverEndpoint.close();
|
||||
Reference in new issue
Block a user