Skip to content

Commit 237aa7e

Browse files
committed
http2: refactor how trailers are done
Rather than an option, introduce a method and an event... ```js server.on('stream', (stream) => { stream.respond(undefined, { waitForTrailers: true }); stream.on('wantTrailers', () => { stream.sendTrailers({ abc: 'xyz'}); }); stream.end('hello world'); }); ``` This is a breaking change in the API such that the prior `options.getTrailers` is no longer supported at all. Ordinarily this would be semver-major and require a deprecation but the http2 stuff is still experimental. PR-URL: #19959 Reviewed-By: Yuta Hiroto <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 2e76b17 commit 237aa7e

17 files changed

+329
-285
lines changed

doc/api/errors.md

+13
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,19 @@ When setting the priority for an HTTP/2 stream, the stream may be marked as
10171017
a dependency for a parent stream. This error code is used when an attempt is
10181018
made to mark a stream and dependent of itself.
10191019

1020+
<a id="ERR_HTTP2_TRAILERS_ALREADY_SENT"></a>
1021+
### ERR_HTTP2_TRAILERS_ALREADY_SENT
1022+
1023+
Trailing headers have already been sent on the `Http2Stream`.
1024+
1025+
<a id="ERR_HTTP2_TRAILERS_NOT_READY"></a>
1026+
### ERR_HTTP2_TRAILERS_NOT_READY
1027+
1028+
The `http2stream.sendTrailers()` method cannot be called until after the
1029+
`'wantTrailers'` event is emitted on an `Http2Stream` object. The
1030+
`'wantTrailers'` event will only be emitted if the `waitForTrailers` option
1031+
is set for the `Http2Stream`.
1032+
10201033
<a id="ERR_HTTP2_UNSUPPORTED_PROTOCOL"></a>
10211034
### ERR_HTTP2_UNSUPPORTED_PROTOCOL
10221035

doc/api/http2.md

+116-75
Large diffs are not rendered by default.

lib/internal/errors.js

+5
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,11 @@ E('ERR_HTTP2_STREAM_CANCEL', 'The pending stream has been canceled', Error);
842842
E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s', Error);
843843
E('ERR_HTTP2_STREAM_SELF_DEPENDENCY',
844844
'A stream cannot depend on itself', Error);
845+
E('ERR_HTTP2_TRAILERS_ALREADY_SENT',
846+
'Trailing headers have already been sent', Error);
847+
E('ERR_HTTP2_TRAILERS_NOT_READY',
848+
'Trailing headers cannot be sent until after the wantTrailers event is ' +
849+
'emitted', Error);
845850
E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', 'protocol "%s" is unsupported.', Error);
846851
E('ERR_HTTP_HEADERS_SENT',
847852
'Cannot %s headers after they are sent to the client', Error);

lib/internal/http2/compat.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,10 @@ class Http2ServerRequest extends Readable {
358358
}
359359
}
360360

361+
function onStreamTrailersReady() {
362+
this[kStream].sendTrailers(this[kTrailers]);
363+
}
364+
361365
class Http2ServerResponse extends Stream {
362366
constructor(stream, options) {
363367
super(options);
@@ -377,6 +381,7 @@ class Http2ServerResponse extends Stream {
377381
stream.on('drain', onStreamDrain);
378382
stream.on('aborted', onStreamAbortedResponse);
379383
stream.on('close', this[kFinish].bind(this));
384+
stream.on('wantTrailers', onStreamTrailersReady.bind(this));
380385
}
381386

382387
// User land modules such as finalhandler just check truthiness of this
@@ -648,7 +653,7 @@ class Http2ServerResponse extends Stream {
648653
headers[HTTP2_HEADER_STATUS] = state.statusCode;
649654
const options = {
650655
endStream: state.ending,
651-
getTrailers: (trailers) => Object.assign(trailers, this[kTrailers])
656+
waitForTrailers: true,
652657
};
653658
this[kStream].respond(headers, options);
654659
}

lib/internal/http2/core.js

+43-42
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const {
4949
ERR_HTTP2_STREAM_CANCEL,
5050
ERR_HTTP2_STREAM_ERROR,
5151
ERR_HTTP2_STREAM_SELF_DEPENDENCY,
52+
ERR_HTTP2_TRAILERS_ALREADY_SENT,
53+
ERR_HTTP2_TRAILERS_NOT_READY,
5254
ERR_HTTP2_UNSUPPORTED_PROTOCOL,
5355
ERR_INVALID_ARG_TYPE,
5456
ERR_INVALID_CALLBACK,
@@ -295,25 +297,18 @@ function tryClose(fd) {
295297
fs.close(fd, (err) => assert.ifError(err));
296298
}
297299

298-
// Called to determine if there are trailers to be sent at the end of a
299-
// Stream. The 'getTrailers' callback is invoked and passed a holder object.
300-
// The trailers to return are set on that object by the handler. Once the
301-
// event handler returns, those are sent off for processing. Note that this
302-
// is a necessarily synchronous operation. We need to know immediately if
303-
// there are trailing headers to send.
300+
// Called when the Http2Stream has finished sending data and is ready for
301+
// trailers to be sent. This will only be called if the { hasOptions: true }
302+
// option is set.
304303
function onStreamTrailers() {
305304
const stream = this[kOwner];
305+
stream[kState].trailersReady = true;
306306
if (stream.destroyed)
307-
return [];
308-
const trailers = Object.create(null);
309-
stream[kState].getTrailers.call(stream, trailers);
310-
const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer);
311-
if (!Array.isArray(headersList)) {
312-
stream.destroy(headersList);
313-
return [];
307+
return;
308+
if (!stream.emit('wantTrailers')) {
309+
// There are no listeners, send empty trailing HEADERS frame and close.
310+
stream.sendTrailers({});
314311
}
315-
stream[kSentTrailers] = trailers;
316-
return headersList;
317312
}
318313

319314
// Submit an RST-STREAM frame to be sent to the remote peer.
@@ -527,10 +522,8 @@ function requestOnConnect(headers, options) {
527522
if (options.endStream)
528523
streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;
529524

530-
if (typeof options.getTrailers === 'function') {
525+
if (options.waitForTrailers)
531526
streamOptions |= STREAM_OPTION_GET_TRAILERS;
532-
this[kState].getTrailers = options.getTrailers;
533-
}
534527

535528
// ret will be either the reserved stream ID (if positive)
536529
// or an error code (if negative)
@@ -1408,11 +1401,6 @@ class ClientHttp2Session extends Http2Session {
14081401
throw new ERR_INVALID_OPT_VALUE('endStream', options.endStream);
14091402
}
14101403

1411-
if (options.getTrailers !== undefined &&
1412-
typeof options.getTrailers !== 'function') {
1413-
throw new ERR_INVALID_OPT_VALUE('getTrailers', options.getTrailers);
1414-
}
1415-
14161404
const headersList = mapToHeaders(headers);
14171405
if (!Array.isArray(headersList))
14181406
throw headersList;
@@ -1504,7 +1492,8 @@ class Http2Stream extends Duplex {
15041492
this[kState] = {
15051493
flags: STREAM_FLAGS_PENDING,
15061494
rstCode: NGHTTP2_NO_ERROR,
1507-
writeQueueSize: 0
1495+
writeQueueSize: 0,
1496+
trailersReady: false
15081497
};
15091498

15101499
this.on('resume', streamOnResume);
@@ -1745,6 +1734,33 @@ class Http2Stream extends Duplex {
17451734
priorityFn();
17461735
}
17471736

1737+
sendTrailers(headers) {
1738+
if (this.destroyed || this.closed)
1739+
throw new ERR_HTTP2_INVALID_STREAM();
1740+
if (this[kSentTrailers])
1741+
throw new ERR_HTTP2_TRAILERS_ALREADY_SENT();
1742+
if (!this[kState].trailersReady)
1743+
throw new ERR_HTTP2_TRAILERS_NOT_READY();
1744+
1745+
assertIsObject(headers, 'headers');
1746+
headers = Object.assign(Object.create(null), headers);
1747+
1748+
const session = this[kSession];
1749+
debug(`Http2Stream ${this[kID]} [Http2Session ` +
1750+
`${sessionName(session[kType])}]: sending trailers`);
1751+
1752+
this[kUpdateTimer]();
1753+
1754+
const headersList = mapToHeaders(headers, assertValidPseudoHeaderTrailer);
1755+
if (!Array.isArray(headersList))
1756+
throw headersList;
1757+
this[kSentTrailers] = headers;
1758+
1759+
const ret = this[kHandle].trailers(headersList);
1760+
if (ret < 0)
1761+
this.destroy(new NghttpError(ret));
1762+
}
1763+
17481764
get closed() {
17491765
return !!(this[kState].flags & STREAM_FLAGS_CLOSED);
17501766
}
@@ -2208,13 +2224,8 @@ class ServerHttp2Stream extends Http2Stream {
22082224
if (options.endStream)
22092225
streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;
22102226

2211-
if (options.getTrailers !== undefined) {
2212-
if (typeof options.getTrailers !== 'function') {
2213-
throw new ERR_INVALID_OPT_VALUE('getTrailers', options.getTrailers);
2214-
}
2227+
if (options.waitForTrailers)
22152228
streamOptions |= STREAM_OPTION_GET_TRAILERS;
2216-
state.getTrailers = options.getTrailers;
2217-
}
22182229

22192230
headers = processHeaders(headers);
22202231
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
@@ -2274,13 +2285,8 @@ class ServerHttp2Stream extends Http2Stream {
22742285
}
22752286

22762287
let streamOptions = 0;
2277-
if (options.getTrailers !== undefined) {
2278-
if (typeof options.getTrailers !== 'function') {
2279-
throw new ERR_INVALID_OPT_VALUE('getTrailers', options.getTrailers);
2280-
}
2288+
if (options.waitForTrailers)
22812289
streamOptions |= STREAM_OPTION_GET_TRAILERS;
2282-
this[kState].getTrailers = options.getTrailers;
2283-
}
22842290

22852291
if (typeof fd !== 'number')
22862292
throw new ERR_INVALID_ARG_TYPE('fd', 'number', fd);
@@ -2340,13 +2346,8 @@ class ServerHttp2Stream extends Http2Stream {
23402346
}
23412347

23422348
let streamOptions = 0;
2343-
if (options.getTrailers !== undefined) {
2344-
if (typeof options.getTrailers !== 'function') {
2345-
throw new ERR_INVALID_OPT_VALUE('getTrailers', options.getTrailers);
2346-
}
2349+
if (options.waitForTrailers)
23472350
streamOptions |= STREAM_OPTION_GET_TRAILERS;
2348-
this[kState].getTrailers = options.getTrailers;
2349-
}
23502351

23512352
const session = this[kSession];
23522353
debug(`Http2Stream ${this[kID]} [Http2Session ` +

src/node_http2.cc

+40-60
Original file line numberDiff line numberDiff line change
@@ -1066,16 +1066,6 @@ int Http2Session::OnNghttpError(nghttp2_session* handle,
10661066
return 0;
10671067
}
10681068

1069-
// Once all of the DATA frames for a Stream have been sent, the GetTrailers
1070-
// method calls out to JavaScript to fetch the trailing headers that need
1071-
// to be sent.
1072-
void Http2Session::GetTrailers(Http2Stream* stream, uint32_t* flags) {
1073-
if (!stream->IsDestroyed() && stream->HasTrailers()) {
1074-
Http2Stream::SubmitTrailers submit_trailers{this, stream, flags};
1075-
stream->OnTrailers(submit_trailers);
1076-
}
1077-
}
1078-
10791069
uv_buf_t Http2StreamListener::OnStreamAlloc(size_t size) {
10801070
// See the comments in Http2Session::OnDataChunkReceived
10811071
// (which is the only possible call site for this method).
@@ -1111,25 +1101,6 @@ void Http2StreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
11111101
stream->CallJSOnreadMethod(nread, buffer);
11121102
}
11131103

1114-
Http2Stream::SubmitTrailers::SubmitTrailers(
1115-
Http2Session* session,
1116-
Http2Stream* stream,
1117-
uint32_t* flags)
1118-
: session_(session), stream_(stream), flags_(flags) { }
1119-
1120-
1121-
void Http2Stream::SubmitTrailers::Submit(nghttp2_nv* trailers,
1122-
size_t length) const {
1123-
Http2Scope h2scope(session_);
1124-
if (length == 0)
1125-
return;
1126-
DEBUG_HTTP2SESSION2(session_, "sending trailers for stream %d, count: %d",
1127-
stream_->id(), length);
1128-
*flags_ |= NGHTTP2_DATA_FLAG_NO_END_STREAM;
1129-
CHECK_EQ(
1130-
nghttp2_submit_trailer(**session_, stream_->id(), trailers, length), 0);
1131-
}
1132-
11331104

11341105
// Called by OnFrameReceived to notify JavaScript land that a complete
11351106
// HEADERS frame has been received and processed. This method converts the
@@ -1725,30 +1696,6 @@ nghttp2_stream* Http2Stream::operator*() {
17251696
return nghttp2_session_find_stream(**session_, id_);
17261697
}
17271698

1728-
1729-
// Calls out to JavaScript land to fetch the actual trailer headers to send
1730-
// for this stream.
1731-
void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) {
1732-
DEBUG_HTTP2STREAM(this, "prompting for trailers");
1733-
CHECK(!this->IsDestroyed());
1734-
Isolate* isolate = env()->isolate();
1735-
HandleScope scope(isolate);
1736-
Local<Context> context = env()->context();
1737-
Context::Scope context_scope(context);
1738-
1739-
Local<Value> ret =
1740-
MakeCallback(env()->ontrailers_string(), 0, nullptr).ToLocalChecked();
1741-
if (!ret.IsEmpty() && !IsDestroyed()) {
1742-
if (ret->IsArray()) {
1743-
Local<Array> headers = ret.As<Array>();
1744-
if (headers->Length() > 0) {
1745-
Headers trailers(isolate, context, headers);
1746-
submit_trailers.Submit(*trailers, trailers.length());
1747-
}
1748-
}
1749-
}
1750-
}
1751-
17521699
void Http2Stream::Close(int32_t code) {
17531700
CHECK(!this->IsDestroyed());
17541701
flags_ |= NGHTTP2_STREAM_FLAG_CLOSED;
@@ -1843,6 +1790,26 @@ int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) {
18431790
return ret;
18441791
}
18451792

1793+
void Http2Stream::OnTrailers() {
1794+
DEBUG_HTTP2STREAM(this, "let javascript know we are ready for trailers");
1795+
CHECK(!this->IsDestroyed());
1796+
Isolate* isolate = env()->isolate();
1797+
HandleScope scope(isolate);
1798+
Local<Context> context = env()->context();
1799+
Context::Scope context_scope(context);
1800+
MakeCallback(env()->ontrailers_string(), 0, nullptr);
1801+
}
1802+
1803+
// Submit informational headers for a stream.
1804+
int Http2Stream::SubmitTrailers(nghttp2_nv* nva, size_t len) {
1805+
CHECK(!this->IsDestroyed());
1806+
Http2Scope h2scope(this);
1807+
DEBUG_HTTP2STREAM2(this, "sending %d trailers", len);
1808+
int ret = nghttp2_submit_trailer(**session_, id_, nva, len);
1809+
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
1810+
return ret;
1811+
}
1812+
18461813
// Submit a PRIORITY frame to the connected peer.
18471814
int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec,
18481815
bool silent) {
@@ -2068,13 +2035,10 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
20682035
if (stream->queue_.empty() && !stream->IsWritable()) {
20692036
DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id);
20702037
*flags |= NGHTTP2_DATA_FLAG_EOF;
2071-
session->GetTrailers(stream, flags);
2072-
// If the stream or session gets destroyed during the GetTrailers
2073-
// callback, check that here and close down the stream
2074-
if (stream->IsDestroyed())
2075-
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
2076-
if (session->IsDestroyed())
2077-
return NGHTTP2_ERR_CALLBACK_FAILURE;
2038+
if (stream->HasTrailers()) {
2039+
*flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM;
2040+
stream->OnTrailers();
2041+
}
20782042
}
20792043

20802044
stream->statistics_.sent_bytes += amount;
@@ -2361,6 +2325,21 @@ void Http2Stream::Info(const FunctionCallbackInfo<Value>& args) {
23612325
headers->Length());
23622326
}
23632327

2328+
// Submits trailing headers on the Http2Stream
2329+
void Http2Stream::Trailers(const FunctionCallbackInfo<Value>& args) {
2330+
Environment* env = Environment::GetCurrent(args);
2331+
Local<Context> context = env->context();
2332+
Isolate* isolate = env->isolate();
2333+
Http2Stream* stream;
2334+
ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
2335+
2336+
Local<Array> headers = args[0].As<Array>();
2337+
2338+
Headers list(isolate, context, headers);
2339+
args.GetReturnValue().Set(stream->SubmitTrailers(*list, list.length()));
2340+
DEBUG_HTTP2STREAM2(stream, "%d trailing headers sent", headers->Length());
2341+
}
2342+
23642343
// Grab the numeric id of the Http2Stream
23652344
void Http2Stream::GetID(const FunctionCallbackInfo<Value>& args) {
23662345
Http2Stream* stream;
@@ -2706,6 +2685,7 @@ void Initialize(Local<Object> target,
27062685
env->SetProtoMethod(stream, "priority", Http2Stream::Priority);
27072686
env->SetProtoMethod(stream, "pushPromise", Http2Stream::PushPromise);
27082687
env->SetProtoMethod(stream, "info", Http2Stream::Info);
2688+
env->SetProtoMethod(stream, "trailers", Http2Stream::Trailers);
27092689
env->SetProtoMethod(stream, "respond", Http2Stream::Respond);
27102690
env->SetProtoMethod(stream, "rstStream", Http2Stream::RstStream);
27112691
env->SetProtoMethod(stream, "refreshState", Http2Stream::RefreshState);

0 commit comments

Comments
 (0)