http2: add altsvc support
Add support for sending and receiving ALTSVC frames. PR-URL: https://github.com/nodejs/node/pull/17917 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
f51067a85d
commit
ce22d6f917
@ -821,6 +821,16 @@ that.
|
||||
|
||||
Occurs with multiple attempts to shutdown an HTTP/2 session.
|
||||
|
||||
<a id="ERR_HTTP2_ALTSVC_INVALID_ORIGIN"></a>
|
||||
### ERR_HTTP2_ALTSVC_INVALID_ORIGIN
|
||||
|
||||
HTTP/2 ALTSVC frames require a valid origin.
|
||||
|
||||
<a id="ERR_HTTP2_ALTSVC_LENGTH"></a>
|
||||
### ERR_HTTP2_ALTSVC_LENGTH
|
||||
|
||||
HTTP/2 ALTSVC frames are limited to a maximum of 16,382 payload bytes.
|
||||
|
||||
<a id="ERR_HTTP2_CONNECT_AUTHORITY"></a>
|
||||
### ERR_HTTP2_CONNECT_AUTHORITY
|
||||
|
||||
|
@ -558,11 +558,103 @@ added: REPLACEME
|
||||
Calls [`unref()`][`net.Socket.prototype.unref`] on this `Http2Session`
|
||||
instance's underlying [`net.Socket`].
|
||||
|
||||
### Class: ServerHttp2Session
|
||||
<!-- YAML
|
||||
added: v8.4.0
|
||||
-->
|
||||
|
||||
#### serverhttp2session.altsvc(alt, originOrStream)
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `alt` {string} A description of the alternative service configuration as
|
||||
defined by [RFC 7838][].
|
||||
* `originOrStream` {number|string|URL|Object} Either a URL string specifying
|
||||
the origin (or an Object with an `origin` property) or the numeric identifier
|
||||
of an active `Http2Stream` as given by the `http2stream.id` property.
|
||||
|
||||
Submits an `ALTSVC` frame (as defined by [RFC 7838][]) to the connected client.
|
||||
|
||||
```js
|
||||
const http2 = require('http2');
|
||||
|
||||
const server = http2.createServer();
|
||||
server.on('session', (session) => {
|
||||
// Set altsvc for origin https://example.org:80
|
||||
session.altsvc('h2=":8000"', 'https://example.org:80');
|
||||
});
|
||||
|
||||
server.on('stream', (stream) => {
|
||||
// Set altsvc for a specific stream
|
||||
stream.session.altsvc('h2=":8000"', stream.id);
|
||||
});
|
||||
```
|
||||
|
||||
Sending an `ALTSVC` frame with a specific stream ID indicates that the alternate
|
||||
service is associated with the origin of the given `Http2Stream`.
|
||||
|
||||
The `alt` and origin string *must* contain only ASCII bytes and are
|
||||
strictly interpreted as a sequence of ASCII bytes. The special value `'clear'`
|
||||
may be passed to clear any previously set alternative service for a given
|
||||
domain.
|
||||
|
||||
When a string is passed for the `originOrStream` argument, it will be parsed as
|
||||
a URL and the origin will be derived. For insetance, the origin for the
|
||||
HTTP URL `'https://example.org/foo/bar'` is the ASCII string
|
||||
`'https://example.org'`. An error will be thrown if either the given string
|
||||
cannot be parsed as a URL or if a valid origin cannot be derived.
|
||||
|
||||
A `URL` object, or any object with an `origin` property, may be passed as
|
||||
`originOrStream`, in which case the value of the `origin` property will be
|
||||
used. The value of the `origin` property *must* be a properly serialized
|
||||
ASCII origin.
|
||||
|
||||
#### Specifying alternative services
|
||||
|
||||
The format of the `alt` parameter is strictly defined by [RFC 7838][] as an
|
||||
ASCII string containing a comma-delimited list of "alternative" protocols
|
||||
associated with a specific host and port.
|
||||
|
||||
For example, the value `'h2="example.org:81"'` indicates that the HTTP/2
|
||||
protocol is available on the host `'example.org'` on TCP/IP port 81. The
|
||||
host and port *must* be contained within the quote (`"`) characters.
|
||||
|
||||
Multiple alternatives may be specified, for instance: `'h2="example.org:81",
|
||||
h2=":82"'`
|
||||
|
||||
The protocol identifier (`'h2'` in the examples) may be any valid
|
||||
[ALPN Protocol ID][].
|
||||
|
||||
The syntax of these values is not validated by the Node.js implementation and
|
||||
are passed through as provided by the user or received from the peer.
|
||||
|
||||
### Class: ClientHttp2Session
|
||||
<!-- YAML
|
||||
added: v8.4.0
|
||||
-->
|
||||
|
||||
#### Event: 'altsvc'
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
The `'altsvc'` event is emitted whenever an `ALTSVC` frame is received by
|
||||
the client. The event is emitted with the `ALTSVC` value, origin, and stream
|
||||
ID, if any. If no `origin` is provided in the `ALTSVC` frame, `origin` will
|
||||
be an empty string.
|
||||
|
||||
```js
|
||||
const http2 = require('http2');
|
||||
const client = http2.connect('https://example.org');
|
||||
|
||||
client.on('altsvc', (alt, origin, stream) => {
|
||||
console.log(alt);
|
||||
console.log(origin);
|
||||
console.log(stream);
|
||||
});
|
||||
```
|
||||
|
||||
#### clienthttp2session.request(headers[, options])
|
||||
<!-- YAML
|
||||
added: v8.4.0
|
||||
@ -2850,6 +2942,7 @@ following additional properties:
|
||||
|
||||
|
||||
[ALPN negotiation]: #http2_alpn_negotiation
|
||||
[ALPN Protocol ID]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
|
||||
[Compatibility API]: #http2_compatibility_api
|
||||
[HTTP/1]: http.html
|
||||
[HTTP/2]: https://tools.ietf.org/html/rfc7540
|
||||
@ -2858,6 +2951,7 @@ following additional properties:
|
||||
[Http2Session and Sockets]: #http2_http2session_and_sockets
|
||||
[Performance Observer]: perf_hooks.html
|
||||
[Readable Stream]: stream.html#stream_class_stream_readable
|
||||
[RFC 7838]: https://tools.ietf.org/html/rfc7838
|
||||
[Settings Object]: #http2_settings_object
|
||||
[Using options.selectPadding]: #http2_using_options_selectpadding
|
||||
[Writable Stream]: stream.html#stream_writable_streams
|
||||
|
@ -304,6 +304,10 @@ E('ERR_FS_INVALID_SYMLINK_TYPE',
|
||||
'Symlink type must be one of "dir", "file", or "junction". Received "%s"');
|
||||
E('ERR_HTTP2_ALREADY_SHUTDOWN',
|
||||
'Http2Session is already shutdown or destroyed');
|
||||
E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN',
|
||||
'HTTP/2 ALTSVC frames require a valid origin');
|
||||
E('ERR_HTTP2_ALTSVC_LENGTH',
|
||||
'HTTP/2 ALTSVC frames are limited to 16382 bytes');
|
||||
E('ERR_HTTP2_CONNECT_AUTHORITY',
|
||||
':authority header is required for CONNECT requests');
|
||||
E('ERR_HTTP2_CONNECT_PATH',
|
||||
|
@ -32,6 +32,9 @@ const kMaxFrameSize = (2 ** 24) - 1;
|
||||
const kMaxInt = (2 ** 32) - 1;
|
||||
const kMaxStreams = (2 ** 31) - 1;
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/;
|
||||
|
||||
const {
|
||||
assertIsObject,
|
||||
assertValidPseudoHeaderResponse,
|
||||
@ -364,6 +367,16 @@ function onFrameError(id, type, code) {
|
||||
process.nextTick(emit, emitter, 'frameError', type, code, id);
|
||||
}
|
||||
|
||||
function onAltSvc(stream, origin, alt) {
|
||||
const session = this[kOwner];
|
||||
if (session.destroyed)
|
||||
return;
|
||||
debug(`Http2Session ${sessionName(session[kType])}: altsvc received: ` +
|
||||
`stream: ${stream}, origin: ${origin}, alt: ${alt}`);
|
||||
session[kUpdateTimer]();
|
||||
process.nextTick(emit, session, 'altsvc', alt, origin, stream);
|
||||
}
|
||||
|
||||
// Receiving a GOAWAY frame from the connected peer is a signal that no
|
||||
// new streams should be created. If the code === NGHTTP2_NO_ERROR, we
|
||||
// are going to send our close, but allow existing frames to close
|
||||
@ -706,6 +719,7 @@ function setupHandle(socket, type, options) {
|
||||
handle.onheaders = onSessionHeaders;
|
||||
handle.onframeerror = onFrameError;
|
||||
handle.ongoawaydata = onGoawayData;
|
||||
handle.onaltsvc = onAltSvc;
|
||||
|
||||
if (typeof options.selectPadding === 'function')
|
||||
handle.ongetpadding = onSelectPadding(options.selectPadding);
|
||||
@ -1154,6 +1168,54 @@ class ServerHttp2Session extends Http2Session {
|
||||
get server() {
|
||||
return this[kServer];
|
||||
}
|
||||
|
||||
// Submits an altsvc frame to be sent to the client. `stream` is a
|
||||
// numeric Stream ID. origin is a URL string that will be used to get
|
||||
// the origin. alt is a string containing the altsvc details. No fancy
|
||||
// API is provided for that.
|
||||
altsvc(alt, originOrStream) {
|
||||
if (this.destroyed)
|
||||
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
|
||||
|
||||
let stream = 0;
|
||||
let origin;
|
||||
|
||||
if (typeof originOrStream === 'string') {
|
||||
origin = (new URL(originOrStream)).origin;
|
||||
if (origin === 'null')
|
||||
throw new errors.TypeError('ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
|
||||
} else if (typeof originOrStream === 'number') {
|
||||
if (originOrStream >>> 0 !== originOrStream || originOrStream === 0)
|
||||
throw new errors.RangeError('ERR_OUT_OF_RANGE', 'originOrStream');
|
||||
stream = originOrStream;
|
||||
} else if (originOrStream !== undefined) {
|
||||
// Allow origin to be passed a URL or object with origin property
|
||||
if (originOrStream !== null && typeof originOrStream === 'object')
|
||||
origin = originOrStream.origin;
|
||||
// Note: if originOrStream is an object with an origin property other
|
||||
// than a URL, then it is possible that origin will be malformed.
|
||||
// We do not verify that here. Users who go that route need to
|
||||
// ensure they are doing the right thing or the payload data will
|
||||
// be invalid.
|
||||
if (typeof origin !== 'string') {
|
||||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'originOrStream',
|
||||
['string', 'number', 'URL', 'object']);
|
||||
} else if (origin === 'null' || origin.length === 0) {
|
||||
throw new errors.TypeError('ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof alt !== 'string')
|
||||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'alt', 'string');
|
||||
if (!kQuotedString.test(alt))
|
||||
throw new errors.TypeError('ERR_INVALID_CHAR', 'alt');
|
||||
|
||||
// Max length permitted for ALTSVC
|
||||
if ((alt.length + (origin !== undefined ? origin.length : 0)) > 16382)
|
||||
throw new errors.TypeError('ERR_HTTP2_ALTSVC_LENGTH');
|
||||
|
||||
this[kHandle].altsvc(stream, origin || '', alt);
|
||||
}
|
||||
}
|
||||
|
||||
// ClientHttp2Session instances have to wait for the socket to connect after
|
||||
|
@ -179,6 +179,7 @@ class ModuleWrap;
|
||||
V(netmask_string, "netmask") \
|
||||
V(nsname_string, "nsname") \
|
||||
V(ocsp_request_string, "OCSPRequest") \
|
||||
V(onaltsvc_string, "onaltsvc") \
|
||||
V(onchange_string, "onchange") \
|
||||
V(onclienthello_string, "onclienthello") \
|
||||
V(oncomplete_string, "oncomplete") \
|
||||
|
@ -103,6 +103,11 @@ Http2Options::Http2Options(Environment* env) {
|
||||
// are required to buffer.
|
||||
nghttp2_option_set_no_auto_window_update(options_, 1);
|
||||
|
||||
// Enable built in support for ALTSVC frames. Once we add support for
|
||||
// other non-built in extension frames, this will need to be handled
|
||||
// a bit differently. For now, let's let nghttp2 take care of it.
|
||||
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC);
|
||||
|
||||
AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
|
||||
env->http2_state()->options_buffer;
|
||||
uint32_t flags = buffer[IDX_OPTIONS_FLAGS];
|
||||
@ -847,6 +852,10 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
|
||||
break;
|
||||
case NGHTTP2_PING:
|
||||
session->HandlePingFrame(frame);
|
||||
break;
|
||||
case NGHTTP2_ALTSVC:
|
||||
session->HandleAltSvcFrame(frame);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -1185,6 +1194,34 @@ inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
|
||||
MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv);
|
||||
}
|
||||
|
||||
// Called by OnFrameReceived when a complete ALTSVC frame has been received.
|
||||
inline void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) {
|
||||
Isolate* isolate = env()->isolate();
|
||||
HandleScope scope(isolate);
|
||||
Local<Context> context = env()->context();
|
||||
Context::Scope context_scope(context);
|
||||
|
||||
int32_t id = GetFrameID(frame);
|
||||
|
||||
nghttp2_extension ext = frame->ext;
|
||||
nghttp2_ext_altsvc* altsvc = static_cast<nghttp2_ext_altsvc*>(ext.payload);
|
||||
DEBUG_HTTP2SESSION(this, "handling altsvc frame");
|
||||
|
||||
Local<Value> argv[3] = {
|
||||
Integer::New(isolate, id),
|
||||
String::NewFromOneByte(isolate,
|
||||
altsvc->origin,
|
||||
v8::NewStringType::kNormal,
|
||||
altsvc->origin_len).ToLocalChecked(),
|
||||
String::NewFromOneByte(isolate,
|
||||
altsvc->field_value,
|
||||
v8::NewStringType::kNormal,
|
||||
altsvc->field_value_len).ToLocalChecked(),
|
||||
};
|
||||
|
||||
MakeCallback(env()->onaltsvc_string(), arraysize(argv), argv);
|
||||
}
|
||||
|
||||
// Called by OnFrameReceived when a complete PING frame has been received.
|
||||
inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
|
||||
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
|
||||
@ -2494,6 +2531,44 @@ void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) {
|
||||
}
|
||||
}
|
||||
|
||||
void Http2Session::AltSvc(int32_t id,
|
||||
uint8_t* origin,
|
||||
size_t origin_len,
|
||||
uint8_t* value,
|
||||
size_t value_len) {
|
||||
Http2Scope h2scope(this);
|
||||
CHECK_EQ(nghttp2_submit_altsvc(session_, NGHTTP2_FLAG_NONE, id,
|
||||
origin, origin_len, value, value_len), 0);
|
||||
}
|
||||
|
||||
// Submits an AltSvc frame to the sent to the connected peer.
|
||||
void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
Http2Session* session;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
|
||||
|
||||
int32_t id = args[0]->Int32Value(env->context()).ToChecked();
|
||||
|
||||
// origin and value are both required to be ASCII, handle them as such.
|
||||
Local<String> origin_str = args[1]->ToString(env->context()).ToLocalChecked();
|
||||
Local<String> value_str = args[2]->ToString(env->context()).ToLocalChecked();
|
||||
|
||||
size_t origin_len = origin_str->Length();
|
||||
size_t value_len = value_str->Length();
|
||||
|
||||
CHECK_LE(origin_len + value_len, 16382); // Max permitted for ALTSVC
|
||||
// Verify that origin len != 0 if stream id == 0, or
|
||||
// that origin len == 0 if stream id != 0
|
||||
CHECK((origin_len != 0 && id == 0) || (origin_len == 0 && id != 0));
|
||||
|
||||
MaybeStackBuffer<uint8_t> origin(origin_len);
|
||||
MaybeStackBuffer<uint8_t> value(value_len);
|
||||
origin_str->WriteOneByte(*origin);
|
||||
value_str->WriteOneByte(*value);
|
||||
|
||||
session->AltSvc(id, *origin, origin_len, *value, value_len);
|
||||
}
|
||||
|
||||
// Submits a PING frame to be sent to the connected peer.
|
||||
void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
@ -2711,6 +2786,7 @@ void Initialize(Local<Object> target,
|
||||
session->SetClassName(http2SessionClassName);
|
||||
session->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
AsyncWrap::AddWrapMethods(env, session);
|
||||
env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc);
|
||||
env->SetProtoMethod(session, "ping", Http2Session::Ping);
|
||||
env->SetProtoMethod(session, "consume", Http2Session::Consume);
|
||||
env->SetProtoMethod(session, "destroy", Http2Session::Destroy);
|
||||
|
@ -800,6 +800,11 @@ class Http2Session : public AsyncWrap {
|
||||
void Consume(Local<External> external);
|
||||
void Unconsume();
|
||||
void Goaway(uint32_t code, int32_t lastStreamID, uint8_t* data, size_t len);
|
||||
void AltSvc(int32_t id,
|
||||
uint8_t* origin,
|
||||
size_t origin_len,
|
||||
uint8_t* value,
|
||||
size_t value_len);
|
||||
|
||||
bool Ping(v8::Local<v8::Function> function);
|
||||
|
||||
@ -879,6 +884,7 @@ class Http2Session : public AsyncWrap {
|
||||
static void UpdateChunksSent(const FunctionCallbackInfo<Value>& args);
|
||||
static void RefreshState(const FunctionCallbackInfo<Value>& args);
|
||||
static void Ping(const FunctionCallbackInfo<Value>& args);
|
||||
static void AltSvc(const FunctionCallbackInfo<Value>& args);
|
||||
|
||||
template <get_setting fn>
|
||||
static void RefreshSettings(const FunctionCallbackInfo<Value>& args);
|
||||
@ -923,6 +929,7 @@ class Http2Session : public AsyncWrap {
|
||||
inline void HandlePriorityFrame(const nghttp2_frame* frame);
|
||||
inline void HandleSettingsFrame(const nghttp2_frame* frame);
|
||||
inline void HandlePingFrame(const nghttp2_frame* frame);
|
||||
inline void HandleAltSvcFrame(const nghttp2_frame* frame);
|
||||
|
||||
// nghttp2 callbacks
|
||||
static inline int OnBeginHeadersCallback(
|
||||
|
126
test/parallel/test-http2-altsvc.js
Normal file
126
test/parallel/test-http2-altsvc.js
Normal file
@ -0,0 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
const assert = require('assert');
|
||||
const http2 = require('http2');
|
||||
const { URL } = require('url');
|
||||
const Countdown = require('../common/countdown');
|
||||
|
||||
const server = http2.createServer();
|
||||
server.on('stream', common.mustCall((stream) => {
|
||||
stream.session.altsvc('h2=":8000"', stream.id);
|
||||
stream.respond();
|
||||
stream.end('ok');
|
||||
}));
|
||||
server.on('session', common.mustCall((session) => {
|
||||
// Origin may be specified by string, URL object, or object with an
|
||||
// origin property. For string and URL object, origin is guaranteed
|
||||
// to be an ASCII serialized origin. For object with an origin
|
||||
// property, it is up to the user to ensure proper serialization.
|
||||
session.altsvc('h2=":8000"', 'https://example.org:8111/this');
|
||||
session.altsvc('h2=":8000"', new URL('https://example.org:8111/this'));
|
||||
session.altsvc('h2=":8000"', { origin: 'https://example.org:8111' });
|
||||
|
||||
// Won't error, but won't send anything because the stream does not exist
|
||||
session.altsvc('h2=":8000"', 3);
|
||||
|
||||
// Will error because the numeric stream id is out of valid range
|
||||
[0, -1, 1.1, 0xFFFFFFFF + 1, Infinity, -Infinity].forEach((i) => {
|
||||
common.expectsError(
|
||||
() => session.altsvc('h2=":8000"', i),
|
||||
{
|
||||
code: 'ERR_OUT_OF_RANGE',
|
||||
type: RangeError
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// First argument must be a string
|
||||
[0, {}, [], null, Infinity].forEach((i) => {
|
||||
common.expectsError(
|
||||
() => session.altsvc(i),
|
||||
{
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
type: TypeError
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
['\u0001', 'h2="\uff20"', '👀'].forEach((i) => {
|
||||
common.expectsError(
|
||||
() => session.altsvc(i),
|
||||
{
|
||||
code: 'ERR_INVALID_CHAR',
|
||||
type: TypeError
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
[{}, [], true].forEach((i) => {
|
||||
common.expectsError(
|
||||
() => session.altsvc('clear', i),
|
||||
{
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
type: TypeError
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
[
|
||||
'abc:',
|
||||
new URL('abc:'),
|
||||
{ origin: 'null' },
|
||||
{ origin: '' }
|
||||
].forEach((i) => {
|
||||
common.expectsError(
|
||||
() => session.altsvc('h2=":8000', i),
|
||||
{
|
||||
code: 'ERR_HTTP2_ALTSVC_INVALID_ORIGIN',
|
||||
type: TypeError
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// arguments + origin are too long for an ALTSVC frame
|
||||
common.expectsError(
|
||||
() => {
|
||||
session.altsvc('h2=":8000"',
|
||||
`http://example.${'a'.repeat(17000)}.org:8000`);
|
||||
},
|
||||
{
|
||||
code: 'ERR_HTTP2_ALTSVC_LENGTH',
|
||||
type: TypeError
|
||||
}
|
||||
);
|
||||
}));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
|
||||
const countdown = new Countdown(4, () => {
|
||||
client.close();
|
||||
server.close();
|
||||
});
|
||||
|
||||
client.on('altsvc', common.mustCall((alt, origin, stream) => {
|
||||
assert.strictEqual(alt, 'h2=":8000"');
|
||||
switch (stream) {
|
||||
case 0:
|
||||
assert.strictEqual(origin, 'https://example.org:8111');
|
||||
break;
|
||||
case 1:
|
||||
assert.strictEqual(origin, '');
|
||||
break;
|
||||
default:
|
||||
assert.fail('should not happen');
|
||||
}
|
||||
countdown.dec();
|
||||
}, 4));
|
||||
|
||||
const req = client.request();
|
||||
req.resume();
|
||||
req.on('close', common.mustCall());
|
||||
}));
|
Loading…
x
Reference in New Issue
Block a user