Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,12 @@ function onStreamCloseRequest() {
req.emit('close');
}

function onStreamTimeout(kind) {
return function onStreamTimeout() {
const obj = this[kind];
obj.emit('timeout');
};
function onStreamTimeoutRequest() {
this[kRequest].emit('timeout');
}

function onStreamTimeoutResponse() {
this[kResponse].emit('timeout');
}

class Http2ServerRequest extends Readable {
Expand Down Expand Up @@ -332,7 +333,7 @@ class Http2ServerRequest extends Readable {
stream.on('error', onStreamError);
stream.on('aborted', onStreamAbortedRequest);
stream.on('close', onStreamCloseRequest);
stream.on('timeout', onStreamTimeout(kRequest));
stream.on('timeout', onStreamTimeoutRequest);
this.on('pause', onRequestPause);
this.on('resume', onRequestResume);
}
Expand Down Expand Up @@ -486,7 +487,7 @@ class Http2ServerResponse extends Stream {
stream.on('aborted', onStreamAbortedResponse);
stream.on('close', onStreamCloseResponse);
stream.on('wantTrailers', onStreamTrailersReady);
stream.on('timeout', onStreamTimeout(kResponse));
stream.on('timeout', onStreamTimeoutResponse);
}

// User land modules such as finalhandler just check truthiness of this
Expand Down
141 changes: 91 additions & 50 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -1991,6 +1991,47 @@ function shutdownWritable(callback) {
return afterShutdown.call(req, 0);
}

// Completes one of the two halves of a dispatched write (the write callback
// itself and the end-of-stream check); the stream machinery callback runs
// once both have finished. The state lives on stream[kState] because only a
// single write may be in flight at any given time.
function finishWrite(stream) {
const state = stream[kState];
if (--state.writePending !== 0)
return;
const cb = state.writeCb;
state.writeCb = null;
const err = aggregateTwoErrors(state.endErr, state.writeErr);
state.writeErr = null;
state.endErr = null;
// writeGeneric does not destroy on error and
// we cannot enable autoDestroy,
// so make sure to destroy on error.
if (err) {
stream.destroy(err);
}
cb(err);
}

// Runs on the tick after a write was dispatched: if the write turned out to
// be the last chunk of an ending writable, shut the writable side down right
// away so the final DATA frame can include the END_STREAM flag.
function endCheckNT(stream) {
const state = stream[kState];
if (state.writeErr ||
!stream._writableState.ending ||
stream._writableState.buffered.length ||
(state.flags & STREAM_FLAGS_HAS_TRAILERS)) {
finishWrite(stream);
return;
}
debugStreamObj(stream, 'shutting down writable on last write');
shutdownWritable.call(stream, (err) => {
state.endErr = err;
finishWrite(stream);
});
}

function finishSendTrailers(stream, headersList) {
// The stream might be destroyed and in that case
// there is nothing to do.
Expand Down Expand Up @@ -2098,6 +2139,12 @@ class Http2Stream extends Duplex {
writeQueueSize: 0,
trailersReady: false,
endAfterHeaders: false,
writeCb: null,
writeErr: null,
endErr: null,
writePending: 0,
shutdownWritableCalled: false,
fd: -1,
};

// Fields used by the compat API to avoid megamorphisms.
Expand Down Expand Up @@ -2285,45 +2332,34 @@ class Http2Stream extends Duplex {
if (!this.headersSent)
this[kProceed]();

let req;
// The stream machinery dispatches at most one _write()/_writev() at a
// time, so the coordination state between the write callback and the
// end-of-stream check below can live on the stream state instead of
// being captured by per-write closures.
const state = this[kState];
state.writeCb = cb;
state.writeErr = null;
state.endErr = null;

if (state.flags & STREAM_FLAGS_HAS_TRAILERS) {
// Trailers are pending, so the writable side cannot be shut down
// early anyway; there is no point in scheduling the end check.
state.writePending = 1;
} else {
state.writePending = 2;
// Shutdown write stream right after last chunk is sent
// so final DATA frame can include END_STREAM flag
process.nextTick(endCheckNT, this);
}

let waitingForWriteCallback = true;
let waitingForEndCheck = true;
let writeCallbackErr;
let endCheckCallbackErr;
const done = () => {
if (waitingForEndCheck || waitingForWriteCallback) return;
const err = aggregateTwoErrors(endCheckCallbackErr, writeCallbackErr);
// writeGeneric does not destroy on error and
// we cannot enable autoDestroy,
// so make sure to destroy on error.
if (err) {
this.destroy(err);
}
cb(err);
};
// This is invoked both as a method on the write req and as a plain
// call, so the stream has to be captured here.
const writeCallback = (err) => {
waitingForWriteCallback = false;
writeCallbackErr = err;
done();
state.writeErr = err;
finishWrite(this);
};
const endCheckCallback = (err) => {
waitingForEndCheck = false;
endCheckCallbackErr = err;
done();
};
// Shutdown write stream right after last chunk is sent
// so final DATA frame can include END_STREAM flag
process.nextTick(() => {
if (writeCallbackErr ||
!this._writableState.ending ||
this._writableState.buffered.length ||
(this[kState].flags & STREAM_FLAGS_HAS_TRAILERS))
return endCheckCallback();
debugStreamObj(this, 'shutting down writable on last write');
shutdownWritable.call(this, endCheckCallback);
});

let req;
if (writev)
req = writevGeneric(this, data, writeCallback);
else
Expand Down Expand Up @@ -2618,31 +2654,35 @@ function prepareResponseHeaders(stream, headersParam, options) {
function prepareResponseHeadersObject(oldHeaders, options) {
assertIsObject(oldHeaders, 'headers', ['Object', 'Array']);
const headers = { __proto__: null };
let statusCode;
let hasDate = false;

if (oldHeaders !== null && oldHeaders !== undefined) {
// This loop is here for performance reason. Do not change.
// The :status and date fields are picked up while copying so they do
// not have to be looked up again on the null-prototype copy.
for (const key in oldHeaders) {
if (ObjectHasOwn(oldHeaders, key)) {
headers[key] = oldHeaders[key];
const value = oldHeaders[key];
headers[key] = value;
if (key === HTTP2_HEADER_STATUS)
statusCode = value;
else if (key === HTTP2_HEADER_DATE)
hasDate = value != null;
}
}
headers[kSensitiveHeaders] = oldHeaders[kSensitiveHeaders];
}

const statusCode =
headers[HTTP2_HEADER_STATUS] =
headers[HTTP2_HEADER_STATUS] | 0 || HTTP_STATUS_OK;
statusCode = headers[HTTP2_HEADER_STATUS] = statusCode | 0 || HTTP_STATUS_OK;

if (options.sendDate == null || options.sendDate) {
headers[HTTP2_HEADER_DATE] ??= utcDate();
if (!hasDate && (options.sendDate == null || options.sendDate)) {
headers[HTTP2_HEADER_DATE] = utcDate();
}

validatePreparedResponseHeaders(headers, statusCode);

return {
headers,
statusCode: headers[HTTP2_HEADER_STATUS],
};
return { headers, statusCode };
}

function prepareResponseHeadersArray(headers, options) {
Expand Down Expand Up @@ -3011,15 +3051,17 @@ class ServerHttp2Stream extends Http2Stream {
const state = this[kState];

assertIsObject(options, 'options');
options = { ...options };
// The options are only read, never mutated, so the user-provided object
// can be used directly instead of copying it.
options ??= kEmptyObject;

debugStreamObj(this, 'initiating response');
this[kUpdateTimer]();

options.endStream = !!options.endStream;
const endStream = !!options.endStream;

let streamOptions = 0;
if (options.endStream)
if (endStream)
streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;

if (options.waitForTrailers) {
Expand All @@ -3037,12 +3079,11 @@ class ServerHttp2Stream extends Http2Stream {

// Close the writable side if the endStream option is set or status
// is one of known codes with no payload, or it's a head request
if (!!options.endStream ||
if (endStream ||
statusCode === HTTP_STATUS_NO_CONTENT ||
statusCode === HTTP_STATUS_RESET_CONTENT ||
statusCode === HTTP_STATUS_NOT_MODIFIED ||
this.headRequest === true) {
options.endStream = true;
this.end();
}

Expand Down
32 changes: 21 additions & 11 deletions lib/internal/http2/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -770,14 +770,16 @@ function buildNgHeaderString(arrayOrMap,
let pseudoHeaders = '';
let count = 0;

const singles = new SafeSet();
let singles;
const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray;
const neverIndex = sensitiveHeaders.map((v) => v.toLowerCase());
const neverIndex = sensitiveHeaders.length === 0 ?
emptyArray : sensitiveHeaders.map((v) => v.toLowerCase());

function processHeader(key, value) {
key = key.toLowerCase();
const isSingleValueField = kSingleValueFields.has(key);
const isStrictSingleValueField = strictSingleValueFields &&
kSingleValueFields.has(key);
isSingleValueField;
let isArray = ArrayIsArray(value);
if (isArray) {
switch (value.length) {
Expand All @@ -795,11 +797,15 @@ function buildNgHeaderString(arrayOrMap,
value = String(value);
}
if (isStrictSingleValueField) {
if (singles.has(key))
if (singles === undefined) {
singles = [key];
} else if (singles.includes(key)) {
throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key);
singles.add(key);
} else {
singles.push(key);
}
}
const flags = neverIndex.includes(key) ?
const flags = neverIndex.length !== 0 && neverIndex.includes(key) ?
kNeverIndexFlag :
kNoHeaderFlags;
if (key[0] === ':') {
Expand All @@ -810,11 +816,15 @@ function buildNgHeaderString(arrayOrMap,
count++;
return;
}
if (!checkIsHttpToken(key)) {
throw new ERR_INVALID_HTTP_TOKEN('Header name', key);
}
if (isIllegalConnectionSpecificHeader(key, value)) {
throw new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key);
// Well-known single-value fields are all valid HTTP tokens and none of
// them is a connection-specific header, so both checks can be skipped.
if (!isSingleValueField) {
if (!checkIsHttpToken(key)) {
throw new ERR_INVALID_HTTP_TOKEN('Header name', key);
}
if (isIllegalConnectionSpecificHeader(key, value)) {
throw new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key);
}
}
if (isArray) {
for (let j = 0; j < value.length; ++j) {
Expand Down