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
3 changes: 3 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -4410,6 +4410,9 @@ added: v8.4.0
This method adds HTTP trailing headers (a header but at the end of the
message) to the response.

Trailers must be added before calling [`response.end()`][]; trailers added
afterwards are silently dropped.

Attempting to set a header field name or value that contains invalid characters
will result in a [`TypeError`][] being thrown.

Expand Down
53 changes: 44 additions & 9 deletions lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const {
validateObject,
} = require('internal/validators');
const {
kAutoEmptyTrailers,
kDisableAutoTrailers,
kSocket,
kRequest,
kProxySocket,
Expand Down Expand Up @@ -300,16 +302,23 @@ function onStreamCloseRequest() {
req.emit('close');
}

function onStreamTimeout(kind) {
return function onStreamTimeout() {
const obj = this[kind];
obj.emit('timeout');
};
// Shared between all Http2ServerRequest instances created without explicit
// options; the Readable constructor only reads from it.
const kDefaultRequestOptions = { autoDestroy: false };

function onStreamTimeoutRequest() {
this[kRequest].emit('timeout');
}

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

class Http2ServerRequest extends Readable {
constructor(stream, headers, options, rawHeaders) {
super({ autoDestroy: false, ...options });
super(options === undefined ?
kDefaultRequestOptions :
{ autoDestroy: false, ...options });
this[kState] = {
closed: false,
didRead: false,
Expand All @@ -332,7 +341,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 @@ -472,6 +481,8 @@ class Http2ServerResponse extends Stream {
this[kState] = {
closed: false,
ending: false,
finishing: false,
hasTrailers: false,
destroyed: false,
headRequest: false,
sendDate: true,
Expand All @@ -488,7 +499,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 Expand Up @@ -583,6 +594,15 @@ class Http2ServerResponse extends Stream {
name = name.trim().toLowerCase();
assertValidHeader(name, value);
this[kTrailers][name] = value;
const state = this[kState];
if (!state.hasTrailers) {
state.hasTrailers = true;
// If the response headers were already flushed with auto-empty
// trailers enabled, tell the stream to hand the trailers back to JS.
const stream = this[kStream];
if (stream.headersSent)
stream[kDisableAutoTrailers]();
}
}

addTrailers(headers) {
Expand Down Expand Up @@ -838,6 +858,12 @@ class Http2ServerResponse extends Stream {
return this;
}

// If the headers have not been flushed yet, they will be flushed below
// as part of ending the response. In that case there is no further
// opportunity to add trailers, so the trailers round trip can be
// skipped entirely when none have been registered.
state.finishing = true;

if (chunk !== null && chunk !== undefined)
this.write(chunk, encoding);

Expand Down Expand Up @@ -895,9 +921,18 @@ class Http2ServerResponse extends Stream {
const state = this[kState];
const headers = this[kHeaders];
headers[HTTP2_HEADER_STATUS] = state.statusCode;
// Only wait for trailers if some have been registered, or if the
// headers are flushed before the response is ended (in which case
// trailers may still be added during streaming). Trailers added after
// end() are dropped, matching HTTP/1 addTrailers() semantics.
const waitForTrailers = state.hasTrailers || !state.finishing;
const options = {
endStream: state.ending,
waitForTrailers: true,
waitForTrailers,
// When no trailers have been registered yet, let the native side
// finish the stream on its own if none show up by the time the last
// DATA frame is sent (see setTrailer()).
[kAutoEmptyTrailers]: waitForTrailers && !state.hasTrailers,
sendDate: state.sendDate,
};
this[kStream].respond(headers, options);
Expand Down
Loading
Loading