http2: near full http1 compatibility, add tests

Extensive re-work of http1 compatibility layer based on tests in
express, on-finished and finalhandler. Fix handling of HEAD
method to match http1. Adjust write, end, etc. to call writeHead
as in http1 and as expected by user-land modules. Add socket
proxy that instead uses the Http2Stream for the vast majority of
socket interactions. Add and change tests to closer represent
http1 behaviour.

Refs: https://github.com/nodejs/node/pull/15633
Refs: https://github.com/expressjs/express/tree/master/test
Refs: https://github.com/jshttp/on-finished/blob/master/test/test.js
Refs: https://github.com/pillarjs/finalhandler/blob/master/test/test.js
PR-URL: https://github.com/nodejs/node/pull/15702
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
Anatoli Papirovski 2017-10-02 21:56:53 -04:00 committed by Matteo Collina
parent 4f339b54e9
commit 2da7d9b820
21 changed files with 864 additions and 180 deletions

View File

@ -802,6 +802,12 @@ SETTINGS. By default, a maximum number of un-acknowledged `SETTINGS` frame may
be sent at any given time. This error code is used when that limit has been be sent at any given time. This error code is used when that limit has been
reached. reached.
<a id="ERR_HTTP2_NO_SOCKET_MANIPULATION"></a>
### ERR_HTTP2_NO_SOCKET_MANIPULATION
Used when attempting to read, write, pause, and/or resume a socket attached to
an `Http2Session`.
<a id="ERR_HTTP2_OUT_OF_STREAMS"></a> <a id="ERR_HTTP2_OUT_OF_STREAMS"></a>
### ERR_HTTP2_OUT_OF_STREAMS ### ERR_HTTP2_OUT_OF_STREAMS

View File

@ -2046,7 +2046,7 @@ console.log(request.headers);
See [Headers Object][]. See [Headers Object][].
### request.httpVersion #### request.httpVersion
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2117,7 +2117,14 @@ added: v8.4.0
* `msecs` {number} * `msecs` {number}
* `callback` {Function} * `callback` {Function}
Calls `request.connection.setTimeout(msecs, callback)`. Sets the [`Http2Stream`]()'s timeout value to `msecs`. If a callback is
provided, then it is added as a listener on the `'timeout'` event on
the response object.
If no `'timeout'` listener is added to the request, the response, or
the server, then [`Http2Stream`]()s are destroyed when they time out. If a
handler is assigned to the request, the response, or the server's `'timeout'`
events, timed out sockets must be handled explicitly.
Returns `request`. Returns `request`.
@ -2128,13 +2135,24 @@ added: v8.4.0
* {net.Socket} * {net.Socket}
The [`net.Socket`][] object associated with the connection. Returns a Proxy object that acts as a `net.Socket` but applies getters,
setters and methods based on HTTP/2 logic.
With TLS support, use [`request.socket.getPeerCertificate()`][] to obtain the `destroyed`, `readable`, and `writable` properties will be retrieved from and
client's authentication details. set on `request.stream`.
*Note*: do not use this socket object to send or receive any data. All `destroy`, `emit`, `end`, `on` and `once` methods will be called on
data transfers are managed by HTTP/2 and data might be lost. `request.stream`.
`setTimeout` method will be called on `request.stream.session`.
`pause`, `read`, `resume`, and `write` will throw an error with code
`ERR_HTTP2_NO_SOCKET_MANIPULATION`. See [`Http2Session and Sockets`][] for
more information.
All other interactions will be routed directly to the socket. With TLS support,
use [`request.socket.getPeerCertificate()`][] to obtain the client's
authentication details.
#### request.stream #### request.stream
<!-- YAML <!-- YAML
@ -2232,7 +2250,7 @@ passed as the second parameter to the [`'request'`][] event.
The response implements, but does not inherit from, the [Writable Stream][] The response implements, but does not inherit from, the [Writable Stream][]
interface. This is an [`EventEmitter`][] with the following events: interface. This is an [`EventEmitter`][] with the following events:
### Event: 'close' #### Event: 'close'
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2240,7 +2258,7 @@ added: v8.4.0
Indicates that the underlying [`Http2Stream`]() was terminated before Indicates that the underlying [`Http2Stream`]() was terminated before
[`response.end()`][] was called or able to flush. [`response.end()`][] was called or able to flush.
### Event: 'finish' #### Event: 'finish'
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2252,7 +2270,7 @@ does not imply that the client has received anything yet.
After this event, no more events will be emitted on the response object. After this event, no more events will be emitted on the response object.
### response.addTrailers(headers) #### response.addTrailers(headers)
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2265,7 +2283,7 @@ message) to the response.
Attempting to set a header field name or value that contains invalid characters Attempting to set a header field name or value that contains invalid characters
will result in a [`TypeError`][] being thrown. will result in a [`TypeError`][] being thrown.
### response.connection #### response.connection
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2274,7 +2292,7 @@ added: v8.4.0
See [`response.socket`][]. See [`response.socket`][].
### response.end([data][, encoding][, callback]) #### response.end([data][, encoding][, callback])
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2293,7 +2311,7 @@ If `data` is specified, it is equivalent to calling
If `callback` is specified, it will be called when the response stream If `callback` is specified, it will be called when the response stream
is finished. is finished.
### response.finished #### response.finished
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2303,7 +2321,7 @@ added: v8.4.0
Boolean value that indicates whether the response has completed. Starts Boolean value that indicates whether the response has completed. Starts
as `false`. After [`response.end()`][] executes, the value will be `true`. as `false`. After [`response.end()`][] executes, the value will be `true`.
### response.getHeader(name) #### response.getHeader(name)
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2320,7 +2338,7 @@ Example:
const contentType = response.getHeader('content-type'); const contentType = response.getHeader('content-type');
``` ```
### response.getHeaderNames() #### response.getHeaderNames()
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2340,7 +2358,7 @@ const headerNames = response.getHeaderNames();
// headerNames === ['foo', 'set-cookie'] // headerNames === ['foo', 'set-cookie']
``` ```
### response.getHeaders() #### response.getHeaders()
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2368,7 +2386,7 @@ const headers = response.getHeaders();
// headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] } // headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] }
``` ```
### response.hasHeader(name) #### response.hasHeader(name)
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2385,7 +2403,7 @@ Example:
const hasContentType = response.hasHeader('content-type'); const hasContentType = response.hasHeader('content-type');
``` ```
### response.headersSent #### response.headersSent
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2394,7 +2412,7 @@ added: v8.4.0
Boolean (read-only). True if headers were sent, false otherwise. Boolean (read-only). True if headers were sent, false otherwise.
### response.removeHeader(name) #### response.removeHeader(name)
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2409,7 +2427,7 @@ Example:
response.removeHeader('Content-Encoding'); response.removeHeader('Content-Encoding');
``` ```
### response.sendDate #### response.sendDate
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2422,7 +2440,7 @@ the response if it is not already present in the headers. Defaults to true.
This should only be disabled for testing; HTTP requires the Date header This should only be disabled for testing; HTTP requires the Date header
in responses. in responses.
### response.setHeader(name, value) #### response.setHeader(name, value)
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2463,7 +2481,7 @@ const server = http2.createServer((req, res) => {
}); });
``` ```
### response.setTimeout(msecs[, callback]) #### response.setTimeout(msecs[, callback])
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2482,18 +2500,29 @@ events, timed out sockets must be handled explicitly.
Returns `response`. Returns `response`.
### response.socket #### response.socket
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
* {net.Socket} * {net.Socket}
Reference to the underlying socket. Usually users will not want to access Returns a Proxy object that acts as a `net.Socket` but applies getters,
this property. In particular, the socket will not emit `'readable'` events setters and methods based on HTTP/2 logic.
because of how the protocol parser attaches to the socket. After
`response.end()`, the property is nulled. The `socket` may also be accessed `destroyed`, `readable`, and `writable` properties will be retrieved from and
via `response.connection`. set on `response.stream`.
`destroy`, `emit`, `end`, `on` and `once` methods will be called on
`response.stream`.
`setTimeout` method will be called on `response.stream.session`.
`pause`, `read`, `resume`, and `write` will throw an error with code
`ERR_HTTP2_NO_SOCKET_MANIPULATION`. See [`Http2Session and Sockets`][] for
more information.
All other interactions will be routed directly to the socket.
Example: Example:
@ -2506,7 +2535,7 @@ const server = http2.createServer((req, res) => {
}).listen(3000); }).listen(3000);
``` ```
### response.statusCode #### response.statusCode
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2526,7 +2555,7 @@ response.statusCode = 404;
After response header was sent to the client, this property indicates the After response header was sent to the client, this property indicates the
status code which was sent out. status code which was sent out.
### response.statusMessage #### response.statusMessage
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2545,7 +2574,7 @@ added: v8.4.0
The [`Http2Stream`][] object backing the response. The [`Http2Stream`][] object backing the response.
### response.write(chunk[, encoding][, callback]) #### response.write(chunk[, encoding][, callback])
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2583,7 +2612,7 @@ Returns `true` if the entire data was flushed successfully to the kernel
buffer. Returns `false` if all or part of the data was queued in user memory. buffer. Returns `false` if all or part of the data was queued in user memory.
`'drain'` will be emitted when the buffer is free again. `'drain'` will be emitted when the buffer is free again.
### response.writeContinue() #### response.writeContinue()
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2592,7 +2621,7 @@ Sends a status `100 Continue` to the client, indicating that the request body
should be sent. See the [`'checkContinue'`][] event on `Http2Server` and should be sent. See the [`'checkContinue'`][] event on `Http2Server` and
`Http2SecureServer`. `Http2SecureServer`.
### response.writeHead(statusCode[, statusMessage][, headers]) #### response.writeHead(statusCode[, statusMessage][, headers])
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->
@ -2648,7 +2677,7 @@ const server = http2.createServer((req, res) => {
Attempting to set a header field name or value that contains invalid characters Attempting to set a header field name or value that contains invalid characters
will result in a [`TypeError`][] being thrown. will result in a [`TypeError`][] being thrown.
### response.createPushResponse(headers, callback) #### response.createPushResponse(headers, callback)
<!-- YAML <!-- YAML
added: v8.4.0 added: v8.4.0
--> -->

View File

@ -206,6 +206,9 @@ E('ERR_HTTP2_INVALID_SETTING_VALUE',
E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed'); E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed');
E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
(max) => `Maximum number of pending settings acknowledgements (${max})`); (max) => `Maximum number of pending settings acknowledgements (${max})`);
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
'HTTP/2 sockets should not be directly read from, written to, ' +
'paused and/or resumed.');
E('ERR_HTTP2_OUT_OF_STREAMS', E('ERR_HTTP2_OUT_OF_STREAMS',
'No stream ID is available because maximum stream ID has been reached'); 'No stream ID is available because maximum stream ID has been reached');
E('ERR_HTTP2_PAYLOAD_FORBIDDEN', E('ERR_HTTP2_PAYLOAD_FORBIDDEN',

View File

@ -16,10 +16,10 @@ const kHeaders = Symbol('headers');
const kRawHeaders = Symbol('rawHeaders'); const kRawHeaders = Symbol('rawHeaders');
const kTrailers = Symbol('trailers'); const kTrailers = Symbol('trailers');
const kRawTrailers = Symbol('rawTrailers'); const kRawTrailers = Symbol('rawTrailers');
const kProxySocket = Symbol('proxySocket');
const kSetHeader = Symbol('setHeader');
const { const {
NGHTTP2_NO_ERROR,
HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_METHOD, HTTP2_HEADER_METHOD,
HTTP2_HEADER_PATH, HTTP2_HEADER_PATH,
@ -39,7 +39,7 @@ let statusMessageWarned = false;
// close as possible to the current require('http') API // close as possible to the current require('http') API
function assertValidHeader(name, value) { function assertValidHeader(name, value) {
if (name === '') if (name === '' || typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Header name', name); throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Header name', name);
if (isPseudoHeader(name)) if (isPseudoHeader(name))
throw new errors.Error('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED'); throw new errors.Error('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED');
@ -71,18 +71,23 @@ function statusMessageWarn() {
} }
function onStreamData(chunk) { function onStreamData(chunk) {
if (!this[kRequest].push(chunk)) const request = this[kRequest];
if (request !== undefined && !request.push(chunk))
this.pause(); this.pause();
} }
function onStreamTrailers(trailers, flags, rawTrailers) { function onStreamTrailers(trailers, flags, rawTrailers) {
const request = this[kRequest]; const request = this[kRequest];
if (request !== undefined) {
Object.assign(request[kTrailers], trailers); Object.assign(request[kTrailers], trailers);
request[kRawTrailers].push(...rawTrailers); request[kRawTrailers].push(...rawTrailers);
}
} }
function onStreamEnd() { function onStreamEnd() {
// Cause the request stream to end as well. // Cause the request stream to end as well.
const request = this[kRequest];
if (request !== undefined)
this[kRequest].push(null); this[kRequest].push(null);
} }
@ -97,62 +102,136 @@ function onStreamError(error) {
} }
function onRequestPause() { function onRequestPause() {
const stream = this[kStream]; this[kStream].pause();
if (stream)
stream.pause();
} }
function onRequestResume() { function onRequestResume() {
const stream = this[kStream]; this[kStream].resume();
if (stream)
stream.resume();
} }
function onStreamDrain() { function onStreamDrain() {
this[kResponse].emit('drain'); const response = this[kResponse];
if (response !== undefined)
response.emit('drain');
} }
// TODO Http2Stream does not emit 'close' // TODO Http2Stream does not emit 'close'
function onStreamClosedRequest() { function onStreamClosedRequest() {
this[kRequest].push(null); const request = this[kRequest];
if (request !== undefined)
request.push(null);
} }
// TODO Http2Stream does not emit 'close' // TODO Http2Stream does not emit 'close'
function onStreamClosedResponse() { function onStreamClosedResponse() {
this[kResponse].emit('finish'); const response = this[kResponse];
if (response !== undefined)
response.emit('finish');
} }
function onStreamAbortedRequest(hadError, code) { function onStreamAbortedRequest() {
const request = this[kRequest]; const request = this[kRequest];
if (request[kState].closed === false) { if (request !== undefined && request[kState].closed === false) {
request.emit('aborted', hadError, code); request.emit('aborted');
request.emit('close'); request.emit('close');
} }
} }
function onStreamAbortedResponse() { function onStreamAbortedResponse() {
const response = this[kResponse]; const response = this[kResponse];
if (response[kState].closed === false) { if (response !== undefined && response[kState].closed === false)
response.emit('close'); response.emit('close');
}
} }
function resumeStream(stream) { function resumeStream(stream) {
stream.resume(); stream.resume();
} }
const proxySocketHandler = {
get(stream, prop) {
switch (prop) {
case 'on':
case 'once':
case 'end':
case 'emit':
case 'destroy':
return stream[prop].bind(stream);
case 'writable':
case 'destroyed':
return stream[prop];
case 'readable':
if (stream.destroyed)
return false;
const request = stream[kRequest];
return request ? request.readable : stream.readable;
case 'setTimeout':
const session = stream.session;
if (session !== undefined)
return session.setTimeout.bind(session);
return stream.setTimeout.bind(stream);
case 'write':
case 'read':
case 'pause':
case 'resume':
throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION');
default:
const ref = stream.session !== undefined ?
stream.session.socket : stream;
const value = ref[prop];
return typeof value === 'function' ? value.bind(ref) : value;
}
},
getPrototypeOf(stream) {
if (stream.session !== undefined)
return stream.session.socket.constructor.prototype;
return stream.prototype;
},
set(stream, prop, value) {
switch (prop) {
case 'writable':
case 'readable':
case 'destroyed':
case 'on':
case 'once':
case 'end':
case 'emit':
case 'destroy':
stream[prop] = value;
return true;
case 'setTimeout':
const session = stream.session;
if (session !== undefined)
session[prop] = value;
else
stream[prop] = value;
return true;
case 'write':
case 'read':
case 'pause':
case 'resume':
throw new errors.Error('ERR_HTTP2_NO_SOCKET_MANIPULATION');
default:
const ref = stream.session !== undefined ?
stream.session.socket : stream;
ref[prop] = value;
return true;
}
}
};
class Http2ServerRequest extends Readable { class Http2ServerRequest extends Readable {
constructor(stream, headers, options, rawHeaders) { constructor(stream, headers, options, rawHeaders) {
super(options); super(options);
this[kState] = { this[kState] = {
closed: false, closed: false,
closedCode: NGHTTP2_NO_ERROR didRead: false,
}; };
this[kHeaders] = headers; this[kHeaders] = headers;
this[kRawHeaders] = rawHeaders; this[kRawHeaders] = rawHeaders;
this[kTrailers] = {}; this[kTrailers] = {};
this[kRawTrailers] = []; this[kRawTrailers] = [];
this[kStream] = stream; this[kStream] = stream;
stream[kProxySocket] = null;
stream[kRequest] = this; stream[kRequest] = this;
// Pause the stream.. // Pause the stream..
@ -170,12 +249,10 @@ class Http2ServerRequest extends Readable {
this.on('resume', onRequestResume); this.on('resume', onRequestResume);
} }
get closed() { get complete() {
return this[kState].closed; return this._readableState.ended ||
} this[kState].closed ||
this[kStream].destroyed;
get code() {
return this[kState].closedCode;
} }
get stream() { get stream() {
@ -212,9 +289,10 @@ class Http2ServerRequest extends Readable {
get socket() { get socket() {
const stream = this[kStream]; const stream = this[kStream];
if (stream === undefined) const proxySocket = stream[kProxySocket];
return; if (proxySocket === null)
return stream.session.socket; return stream[kProxySocket] = new Proxy(stream, proxySocketHandler);
return proxySocket;
} }
get connection() { get connection() {
@ -222,9 +300,10 @@ class Http2ServerRequest extends Readable {
} }
_read(nread) { _read(nread) {
const stream = this[kStream]; const state = this[kState];
if (stream !== undefined) { if (!state.closed) {
process.nextTick(resumeStream, stream); state.didRead = true;
process.nextTick(resumeStream, this[kStream]);
} else { } else {
this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED')); this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
} }
@ -234,6 +313,13 @@ class Http2ServerRequest extends Readable {
return this[kHeaders][HTTP2_HEADER_METHOD]; return this[kHeaders][HTTP2_HEADER_METHOD];
} }
set method(method) {
if (typeof method !== 'string' || method.trim() === '')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'method', 'string');
this[kHeaders][HTTP2_HEADER_METHOD] = method;
}
get authority() { get authority() {
return this[kHeaders][HTTP2_HEADER_AUTHORITY]; return this[kHeaders][HTTP2_HEADER_AUTHORITY];
} }
@ -256,15 +342,17 @@ class Http2ServerRequest extends Readable {
this[kStream].setTimeout(msecs, callback); this[kStream].setTimeout(msecs, callback);
} }
[kFinish](code) { [kFinish]() {
const state = this[kState]; const state = this[kState];
if (state.closed) if (state.closed)
return; return;
if (code !== undefined)
state.closedCode = Number(code);
state.closed = true; state.closed = true;
this.push(null); this.push(null);
process.nextTick(() => (this[kStream] = undefined)); this[kStream][kRequest] = undefined;
// if the user didn't interact with incoming data and didn't pipe it,
// dump it for compatibility with http1
if (!state.didRead && !this._readableState.resumeScheduled)
this.resume();
} }
} }
@ -272,14 +360,16 @@ class Http2ServerResponse extends Stream {
constructor(stream, options) { constructor(stream, options) {
super(options); super(options);
this[kState] = { this[kState] = {
closed: false,
ending: false,
headRequest: false,
sendDate: true, sendDate: true,
statusCode: HTTP_STATUS_OK, statusCode: HTTP_STATUS_OK,
closed: false,
closedCode: NGHTTP2_NO_ERROR
}; };
this[kHeaders] = Object.create(null); this[kHeaders] = Object.create(null);
this[kTrailers] = Object.create(null); this[kTrailers] = Object.create(null);
this[kStream] = stream; this[kStream] = stream;
stream[kProxySocket] = null;
stream[kResponse] = this; stream[kResponse] = this;
this.writable = true; this.writable = true;
stream.on('drain', onStreamDrain); stream.on('drain', onStreamDrain);
@ -290,17 +380,35 @@ class Http2ServerResponse extends Stream {
stream.on('finish', onfinish); stream.on('finish', onfinish);
} }
// User land modules such as finalhandler just check truthiness of this
// but if someone is actually trying to use this for more than that
// then we simply can't support such use cases
get _header() {
return this.headersSent;
}
get finished() { get finished() {
const stream = this[kStream]; const stream = this[kStream];
return stream === undefined || stream._writableState.ended; return stream.destroyed ||
stream._writableState.ended ||
this[kState].closed;
} }
get closed() { get socket() {
return this[kState].closed; // this is compatible with http1 which removes socket reference
// only from ServerResponse but not IncomingMessage
if (this[kState].closed)
return;
const stream = this[kStream];
const proxySocket = stream[kProxySocket];
if (proxySocket === null)
return stream[kProxySocket] = new Proxy(stream, proxySocketHandler);
return proxySocket;
} }
get code() { get connection() {
return this[kState].closedCode; return this.socket;
} }
get stream() { get stream() {
@ -308,8 +416,7 @@ class Http2ServerResponse extends Stream {
} }
get headersSent() { get headersSent() {
const stream = this[kStream]; return this[kStream].headersSent;
return stream !== undefined ? stream.headersSent : this[kState].headersSent;
} }
get sendDate() { get sendDate() {
@ -339,7 +446,7 @@ class Http2ServerResponse extends Stream {
name = name.trim().toLowerCase(); name = name.trim().toLowerCase();
assertValidHeader(name, value); assertValidHeader(name, value);
this[kTrailers][name] = String(value); this[kTrailers][name] = value;
} }
addTrailers(headers) { addTrailers(headers) {
@ -379,6 +486,9 @@ class Http2ServerResponse extends Stream {
if (typeof name !== 'string') if (typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string'); throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string');
if (this[kStream].headersSent)
throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
name = name.trim().toLowerCase(); name = name.trim().toLowerCase();
delete this[kHeaders][name]; delete this[kHeaders][name];
} }
@ -387,9 +497,16 @@ class Http2ServerResponse extends Stream {
if (typeof name !== 'string') if (typeof name !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string'); throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string');
if (this[kStream].headersSent)
throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
this[kSetHeader](name, value);
}
[kSetHeader](name, value) {
name = name.trim().toLowerCase(); name = name.trim().toLowerCase();
assertValidHeader(name, value); assertValidHeader(name, value);
this[kHeaders][name] = String(value); this[kHeaders][name] = value;
} }
get statusMessage() { get statusMessage() {
@ -403,50 +520,45 @@ class Http2ServerResponse extends Stream {
} }
flushHeaders() { flushHeaders() {
const stream = this[kStream]; const state = this[kState];
if (stream !== undefined && stream.headersSent === false) if (!state.closed && !this[kStream].headersSent)
this[kBeginSend](); this.writeHead(state.statusCode);
} }
writeHead(statusCode, statusMessage, headers) { writeHead(statusCode, statusMessage, headers) {
if (typeof statusMessage === 'string') { const state = this[kState];
statusMessageWarn();
}
if (headers === undefined && typeof statusMessage === 'object') { if (state.closed)
headers = statusMessage;
}
const stream = this[kStream];
if (stream === undefined) {
throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); throw new errors.Error('ERR_HTTP2_STREAM_CLOSED');
} if (this[kStream].headersSent)
if (stream.headersSent === true) { throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
throw new errors.Error('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND');
} if (typeof statusMessage === 'string')
statusMessageWarn();
if (headers === undefined && typeof statusMessage === 'object')
headers = statusMessage;
if (typeof headers === 'object') { if (typeof headers === 'object') {
const keys = Object.keys(headers); const keys = Object.keys(headers);
let key = ''; let key = '';
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
key = keys[i]; key = keys[i];
this.setHeader(key, headers[key]); this[kSetHeader](key, headers[key]);
} }
} }
this.statusCode = statusCode; state.statusCode = statusCode;
this[kBeginSend](); this[kBeginSend]();
} }
write(chunk, encoding, cb) { write(chunk, encoding, cb) {
const stream = this[kStream];
if (typeof encoding === 'function') { if (typeof encoding === 'function') {
cb = encoding; cb = encoding;
encoding = 'utf8'; encoding = 'utf8';
} }
if (stream === undefined) { if (this[kState].closed) {
const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED'); const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
if (typeof cb === 'function') if (typeof cb === 'function')
process.nextTick(cb, err); process.nextTick(cb, err);
@ -454,12 +566,21 @@ class Http2ServerResponse extends Stream {
throw err; throw err;
return; return;
} }
this[kBeginSend]();
const stream = this[kStream];
if (!stream.headersSent)
this.writeHead(this[kState].statusCode);
return stream.write(chunk, encoding, cb); return stream.write(chunk, encoding, cb);
} }
end(chunk, encoding, cb) { end(chunk, encoding, cb) {
const stream = this[kStream]; const stream = this[kStream];
const state = this[kState];
if ((state.closed || state.ending) &&
state.headRequest === stream.headRequest) {
return false;
}
if (typeof chunk === 'function') { if (typeof chunk === 'function') {
cb = chunk; cb = chunk;
@ -468,18 +589,27 @@ class Http2ServerResponse extends Stream {
cb = encoding; cb = encoding;
encoding = 'utf8'; encoding = 'utf8';
} }
if (this.finished === true) {
return false; if (chunk !== null && chunk !== undefined)
}
if (chunk !== null && chunk !== undefined) {
this.write(chunk, encoding); this.write(chunk, encoding);
}
const isFinished = this.finished;
state.headRequest = stream.headRequest;
state.ending = true;
if (typeof cb === 'function') { if (typeof cb === 'function') {
if (isFinished)
this.once('finish', cb);
else
stream.once('finish', cb); stream.once('finish', cb);
} }
this[kBeginSend]({ endStream: true }); if (!stream.headersSent)
this.writeHead(this[kState].statusCode);
if (isFinished)
this[kFinish]();
else
stream.end(); stream.end();
} }
@ -490,63 +620,52 @@ class Http2ServerResponse extends Stream {
} }
setTimeout(msecs, callback) { setTimeout(msecs, callback) {
const stream = this[kStream];
if (this[kState].closed) if (this[kState].closed)
return; return;
stream.setTimeout(msecs, callback); this[kStream].setTimeout(msecs, callback);
} }
createPushResponse(headers, callback) { createPushResponse(headers, callback) {
if (typeof callback !== 'function') if (typeof callback !== 'function')
throw new errors.TypeError('ERR_INVALID_CALLBACK'); throw new errors.TypeError('ERR_INVALID_CALLBACK');
const stream = this[kStream]; if (this[kState].closed) {
if (stream === undefined) {
process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED')); process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
return; return;
} }
stream.pushStream(headers, {}, function(stream, headers, options) { this[kStream].pushStream(headers, {}, function(stream, headers, options) {
const response = new Http2ServerResponse(stream); const response = new Http2ServerResponse(stream);
callback(null, response); callback(null, response);
}); });
} }
[kBeginSend](options) { [kBeginSend]() {
const stream = this[kStream]; const state = this[kState];
if (stream !== undefined &&
stream.destroyed === false &&
stream.headersSent === false) {
const headers = this[kHeaders]; const headers = this[kHeaders];
headers[HTTP2_HEADER_STATUS] = this[kState].statusCode; headers[HTTP2_HEADER_STATUS] = state.statusCode;
options = options || Object.create(null); const options = {
options.getTrailers = (trailers) => { endStream: state.ending,
Object.assign(trailers, this[kTrailers]); getTrailers: (trailers) => Object.assign(trailers, this[kTrailers])
}; };
stream.respond(headers, options); this[kStream].respond(headers, options);
}
} }
[kFinish](code) { [kFinish]() {
const stream = this[kStream];
const state = this[kState]; const state = this[kState];
if (state.closed) if (state.closed || stream.headRequest !== state.headRequest)
return; return;
if (code !== undefined)
state.closedCode = Number(code);
state.closed = true; state.closed = true;
state.headersSent = this[kStream].headersSent; this[kProxySocket] = null;
this.end(); stream[kResponse] = undefined;
process.nextTick(() => (this[kStream] = undefined));
this.emit('finish'); this.emit('finish');
} }
// TODO doesn't support callbacks // TODO doesn't support callbacks
writeContinue() { writeContinue() {
const stream = this[kStream]; const stream = this[kStream];
if (stream === undefined || if (stream.headersSent || this[kState].closed)
stream.headersSent === true ||
stream.destroyed === true) {
return false; return false;
} stream.additionalHeaders({
this[kStream].additionalHeaders({
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE [HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE
}); });
return true; return true;

View File

@ -166,8 +166,7 @@ function onSessionHeaders(id, cat, flags, headers) {
// For head requests, there must not be a body... // For head requests, there must not be a body...
// end the writable side immediately. // end the writable side immediately.
stream.end(); stream.end();
const state = stream[kState]; stream[kState].headRequest = true;
state.headRequest = true;
} }
} else { } else {
stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream }); stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream });
@ -1277,6 +1276,7 @@ class Http2Stream extends Duplex {
rst: false, rst: false,
rstCode: NGHTTP2_NO_ERROR, rstCode: NGHTTP2_NO_ERROR,
headersSent: false, headersSent: false,
headRequest: false,
aborted: false, aborted: false,
closeHandler: onSessionClose.bind(this) closeHandler: onSessionClose.bind(this)
}; };
@ -1333,6 +1333,11 @@ class Http2Stream extends Duplex {
return this[kState].aborted; return this[kState].aborted;
} }
// true if dealing with a HEAD request
get headRequest() {
return this[kState].headRequest;
}
// The error code reported when this Http2Stream was closed. // The error code reported when this Http2Stream was closed.
get rstCode() { get rstCode() {
return this[kState].rst ? this[kState].rstCode : undefined; return this[kState].rst ? this[kState].rstCode : undefined;
@ -1533,12 +1538,15 @@ function continueStreamDestroy(self, err, callback) {
// All done // All done
const rst = state.rst; const rst = state.rst;
const code = rst ? state.rstCode : NGHTTP2_NO_ERROR; const code = rst ? state.rstCode : NGHTTP2_NO_ERROR;
if (!err && code !== NGHTTP2_NO_ERROR) { // RST code 8 not emitted as an error as its used by clients to signify
// abort and is already covered by aborted event, also allows more
// seamless compatibility with http1
if (!err && code !== NGHTTP2_NO_ERROR && code !== NGHTTP2_CANCEL) {
err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code); err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
} }
callback(err);
process.nextTick(emit, self, 'streamClosed', code); process.nextTick(emit, self, 'streamClosed', code);
debug(`[${sessionName(session[kType])}] stream ${self[kID]} destroyed`); debug(`[${sessionName(session[kType])}] stream ${self[kID]} destroyed`);
callback(err);
} }
function finishStreamDestroy(self, handle) { function finishStreamDestroy(self, handle) {

View File

@ -3,6 +3,7 @@
const common = require('../common'); const common = require('../common');
if (!common.hasCrypto) if (!common.hasCrypto)
common.skip('missing crypto'); common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2'); const h2 = require('http2');
// Http2ServerRequest should always end readable stream // Http2ServerRequest should always end readable stream
@ -12,9 +13,17 @@ const server = h2.createServer();
server.listen(0, common.mustCall(function() { server.listen(0, common.mustCall(function() {
const port = server.address().port; const port = server.address().port;
server.once('request', common.mustCall(function(request, response) { server.once('request', common.mustCall(function(request, response) {
assert.strictEqual(request.complete, false);
request.on('data', () => {}); request.on('data', () => {});
request.on('end', common.mustCall(() => { request.on('end', common.mustCall(() => {
assert.strictEqual(request.complete, true);
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {
// the following tests edge cases on request socket
// right after finished fires but before backing
// Http2Stream is destroyed
assert.strictEqual(request.socket.readable, request.stream.readable);
assert.strictEqual(request.socket.readable, false);
server.close(); server.close();
})); }));
response.end(); response.end();

View File

@ -41,6 +41,27 @@ server.listen(0, common.mustCall(function() {
request.url = '/one'; request.url = '/one';
assert.strictEqual(request.url, '/one'); assert.strictEqual(request.url, '/one');
// third-party plugins for packages like express use query params to
// change the request method
request.method = 'POST';
assert.strictEqual(request.method, 'POST');
common.expectsError(
() => request.method = ' ',
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "method" argument must be of type string'
}
);
common.expectsError(
() => request.method = true,
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "method" argument must be of type string'
}
);
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {
server.close(); server.close();
})); }));

View File

@ -19,6 +19,7 @@ const server = http2.createServer();
server.on('request', common.mustCall((req, res) => { server.on('request', common.mustCall((req, res) => {
const dest = req.pipe(fs.createWriteStream(fn)); const dest = req.pipe(fs.createWriteStream(fn));
dest.on('finish', common.mustCall(() => { dest.on('finish', common.mustCall(() => {
assert.strictEqual(req.complete, true);
assert.deepStrictEqual(fs.readFileSync(loc), fs.readFileSync(fn)); assert.deepStrictEqual(fs.readFileSync(loc), fs.readFileSync(fn));
fs.unlinkSync(fn); fs.unlinkSync(fn);
res.end(); res.end();

View File

@ -19,9 +19,6 @@ server.listen(0, common.mustCall(function() {
httpVersionMinor: 0 httpVersionMinor: 0
}; };
assert.strictEqual(request.closed, false);
assert.strictEqual(request.code, h2.constants.NGHTTP2_NO_ERROR);
assert.strictEqual(request.httpVersion, expected.version); assert.strictEqual(request.httpVersion, expected.version);
assert.strictEqual(request.httpVersionMajor, expected.httpVersionMajor); assert.strictEqual(request.httpVersionMajor, expected.httpVersionMajor);
assert.strictEqual(request.httpVersionMinor, expected.httpVersionMinor); assert.strictEqual(request.httpVersionMinor, expected.httpVersionMinor);
@ -31,10 +28,8 @@ server.listen(0, common.mustCall(function() {
assert.strictEqual(request.socket, request.connection); assert.strictEqual(request.socket, request.connection);
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {
assert.strictEqual(request.closed, true);
assert.strictEqual(request.code, h2.constants.NGHTTP2_NO_ERROR);
process.nextTick(() => { process.nextTick(() => {
assert.strictEqual(request.socket, undefined); assert.ok(request.socket);
server.close(); server.close();
}); });
})); }));

View File

@ -22,7 +22,6 @@ const server = http2.createServer(common.mustCall((req, res) => {
res.on('finish', common.mustCall(() => { res.on('finish', common.mustCall(() => {
assert.doesNotThrow(() => res.destroy(nextError)); assert.doesNotThrow(() => res.destroy(nextError));
assert.strictEqual(res.closed, true);
process.nextTick(() => { process.nextTick(() => {
assert.doesNotThrow(() => res.destroy(nextError)); assert.doesNotThrow(() => res.destroy(nextError));
}); });

View File

@ -1,6 +1,12 @@
'use strict'; 'use strict';
const { mustCall, mustNotCall, hasCrypto, skip } = require('../common'); const {
mustCall,
mustNotCall,
hasCrypto,
platformTimeout,
skip
} = require('../common');
if (!hasCrypto) if (!hasCrypto)
skip('missing crypto'); skip('missing crypto');
const { strictEqual } = require('assert'); const { strictEqual } = require('assert');
@ -18,15 +24,16 @@ const {
// It may be invoked repeatedly without throwing errors // It may be invoked repeatedly without throwing errors
// but callback will only be called once // but callback will only be called once
const server = createServer(mustCall((request, response) => { const server = createServer(mustCall((request, response) => {
strictEqual(response.closed, false);
response.end('end', 'utf8', mustCall(() => { response.end('end', 'utf8', mustCall(() => {
strictEqual(response.closed, true);
response.end(mustNotCall()); response.end(mustNotCall());
process.nextTick(() => { process.nextTick(() => {
response.end(mustNotCall()); response.end(mustNotCall());
server.close(); server.close();
}); });
})); }));
response.on('finish', mustCall(() => {
response.end(mustNotCall());
}));
response.end(mustNotCall()); response.end(mustNotCall());
})); }));
server.listen(0, mustCall(() => { server.listen(0, mustCall(() => {
@ -111,12 +118,12 @@ const {
} }
{ {
// Http2ServerResponse.end is not necessary on HEAD requests since the stream // Http2ServerResponse.end is necessary on HEAD requests in compat
// is already closed. Headers, however, can still be sent to the client. // for http1 compatibility
const server = createServer(mustCall((request, response) => { const server = createServer(mustCall((request, response) => {
strictEqual(response.finished, true); strictEqual(response.finished, true);
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' }); response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.end(mustNotCall()); response.end('data', mustCall());
})); }));
server.listen(0, mustCall(() => { server.listen(0, mustCall(() => {
const { port } = server.address(); const { port } = server.address();
@ -144,3 +151,175 @@ const {
})); }));
})); }));
} }
{
// .end should trigger 'end' event on request if user did not attempt
// to read from the request
const server = createServer(mustCall((request, response) => {
request.on('end', mustCall());
response.end();
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Should be able to call .end with cb from stream 'streamClosed'
const server = createServer(mustCall((request, response) => {
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.stream.on('streamClosed', mustCall(() => {
response.end(mustCall());
}));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // the end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Should be able to respond to HEAD request after timeout
const server = createServer(mustCall((request, response) => {
setTimeout(mustCall(() => {
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.end('data', mustCall());
}), platformTimeout(10));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // the end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// finish should only trigger after 'end' is called
const server = createServer(mustCall((request, response) => {
let finished = false;
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.on('finish', mustCall(() => {
finished = false;
}));
response.end('data', mustCall(() => {
strictEqual(finished, false);
response.end('data', mustNotCall());
}));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // the end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Should be able to respond to HEAD with just .end
const server = createServer(mustCall((request, response) => {
response.end('data', mustCall());
response.end(mustNotCall());
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // the end of stream flag is set
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));
}

View File

@ -5,19 +5,23 @@ if (!common.hasCrypto)
common.skip('missing crypto'); common.skip('missing crypto');
const assert = require('assert'); const assert = require('assert');
const h2 = require('http2'); const h2 = require('http2');
const net = require('net');
// Http2ServerResponse.finished // Http2ServerResponse.finished
const server = h2.createServer(); const server = h2.createServer();
server.listen(0, common.mustCall(function() { server.listen(0, common.mustCall(function() {
const port = server.address().port; const port = server.address().port;
server.once('request', common.mustCall(function(request, response) { server.once('request', common.mustCall(function(request, response) {
assert.ok(response.socket instanceof net.Socket);
assert.ok(response.connection instanceof net.Socket);
assert.strictEqual(response.socket, response.connection);
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {
assert.ok(request.stream !== undefined); assert.strictEqual(response.socket, undefined);
assert.ok(response.stream !== undefined); assert.strictEqual(response.connection, undefined);
server.close();
process.nextTick(common.mustCall(() => { process.nextTick(common.mustCall(() => {
assert.strictEqual(request.stream, undefined); assert.ok(response.stream);
assert.strictEqual(response.stream, undefined); server.close();
})); }));
})); }));
assert.strictEqual(response.finished, false); assert.strictEqual(response.finished, false);

View File

@ -15,18 +15,23 @@ server.listen(0, common.mustCall(function() {
const port = server.address().port; const port = server.address().port;
server.once('request', common.mustCall(function(request, response) { server.once('request', common.mustCall(function(request, response) {
assert.strictEqual(response.headersSent, false); assert.strictEqual(response.headersSent, false);
assert.strictEqual(response._header, false); // alias for headersSent
response.flushHeaders(); response.flushHeaders();
assert.strictEqual(response.headersSent, true); assert.strictEqual(response.headersSent, true);
assert.strictEqual(response._header, true);
response.flushHeaders(); // Idempotent response.flushHeaders(); // Idempotent
common.expectsError(() => { common.expectsError(() => {
response.writeHead(400, { 'foo-bar': 'abc123' }); response.writeHead(400, { 'foo-bar': 'abc123' });
}, { }, {
code: 'ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND' code: 'ERR_HTTP2_HEADERS_SENT'
}); });
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {
server.close(); server.close();
process.nextTick(() => {
response.flushHeaders(); // Idempotent
});
})); }));
serverResponse = response; serverResponse = response;
})); }));

View File

@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// makes sure that Http2ServerResponse setHeader & removeHeader, do not throw
// any errors if the stream was destroyed before headers were sent
const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
response.destroy();
response.on('finish', common.mustCall(() => {
assert.strictEqual(response.headersSent, false);
assert.doesNotThrow(() => response.setHeader('test', 'value'));
assert.doesNotThrow(() => response.removeHeader('test', 'value'));
process.nextTick(() => {
assert.doesNotThrow(() => response.setHeader('test', 'value'));
assert.doesNotThrow(() => response.removeHeader('test', 'value'));
server.close();
});
}));
}));
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('end', common.mustCall(function() {
client.destroy();
}));
request.end();
request.resume();
}));
}));

View File

@ -124,14 +124,44 @@ server.listen(0, common.mustCall(function() {
response.sendDate = false; response.sendDate = false;
assert.strictEqual(response.sendDate, false); assert.strictEqual(response.sendDate, false);
assert.strictEqual(response.code, h2.constants.NGHTTP2_NO_ERROR);
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {
assert.strictEqual(response.code, h2.constants.NGHTTP2_NO_ERROR);
assert.strictEqual(response.headersSent, true); assert.strictEqual(response.headersSent, true);
common.expectsError(
() => response.setHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
type: Error,
message: 'Response has already been initiated.'
}
);
common.expectsError(
() => response.removeHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
type: Error,
message: 'Response has already been initiated.'
}
);
process.nextTick(() => { process.nextTick(() => {
// can access headersSent after stream is undefined common.expectsError(
assert.strictEqual(response.stream, undefined); () => response.setHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
type: Error,
message: 'Response has already been initiated.'
}
);
common.expectsError(
() => response.removeHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
type: Error,
message: 'Response has already been initiated.'
}
);
assert.strictEqual(response.headersSent, true); assert.strictEqual(response.headersSent, true);
server.close(); server.close();
}); });

View File

@ -16,7 +16,7 @@ server.listen(0, common.mustCall(function() {
response.writeHead(418, { 'foo-bar': 'abc123' }); // Override response.writeHead(418, { 'foo-bar': 'abc123' }); // Override
common.expectsError(() => { response.writeHead(300); }, { common.expectsError(() => { response.writeHead(300); }, {
code: 'ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND' code: 'ERR_HTTP2_HEADERS_SENT'
}); });
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {

View File

@ -0,0 +1,107 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// Tests behaviour of the proxied socket in Http2ServerRequest
// & Http2ServerResponse - specifically property setters
const errMsg = {
code: 'ERR_HTTP2_NO_SOCKET_MANIPULATION',
type: Error,
message: 'HTTP/2 sockets should not be directly read from, written to, ' +
'paused and/or resumed.'
};
const server = h2.createServer();
server.on('request', common.mustCall(function(request, response) {
const noop = () => {};
assert.strictEqual(request.stream.destroyed, false);
request.socket.destroyed = true;
assert.strictEqual(request.stream.destroyed, true);
request.socket.destroyed = false;
assert.strictEqual(request.stream.readable, false);
request.socket.readable = true;
assert.strictEqual(request.stream.readable, true);
assert.strictEqual(request.stream.writable, true);
request.socket.writable = false;
assert.strictEqual(request.stream.writable, false);
const realOn = request.stream.on;
request.socket.on = noop;
assert.strictEqual(request.stream.on, noop);
request.stream.on = realOn;
const realOnce = request.stream.once;
request.socket.once = noop;
assert.strictEqual(request.stream.once, noop);
request.stream.once = realOnce;
const realEnd = request.stream.end;
request.socket.end = noop;
assert.strictEqual(request.stream.end, noop);
request.socket.end = common.mustCall();
request.socket.end();
request.stream.end = realEnd;
const realEmit = request.stream.emit;
request.socket.emit = noop;
assert.strictEqual(request.stream.emit, noop);
request.stream.emit = realEmit;
const realDestroy = request.stream.destroy;
request.socket.destroy = noop;
assert.strictEqual(request.stream.destroy, noop);
request.stream.destroy = realDestroy;
request.socket.setTimeout = noop;
assert.strictEqual(request.stream.session.setTimeout, noop);
assert.strictEqual(request.stream.session.socket._isProcessing, undefined);
request.socket._isProcessing = true;
assert.strictEqual(request.stream.session.socket._isProcessing, true);
common.expectsError(() => request.socket.read = noop, errMsg);
common.expectsError(() => request.socket.write = noop, errMsg);
common.expectsError(() => request.socket.pause = noop, errMsg);
common.expectsError(() => request.socket.resume = noop, errMsg);
request.stream.on('finish', common.mustCall(() => {
setImmediate(() => {
request.socket.setTimeout = noop;
assert.strictEqual(request.stream.setTimeout, noop);
assert.strictEqual(request.stream._isProcessing, undefined);
request.socket._isProcessing = true;
assert.strictEqual(request.stream._isProcessing, true);
});
}));
response.stream.destroy();
}));
server.listen(0, common.mustCall(function() {
const port = server.address().port;
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('end', common.mustCall(() => {
client.destroy();
server.close();
}));
request.end();
request.resume();
}));
}));

View File

@ -0,0 +1,89 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
const net = require('net');
// Tests behaviour of the proxied socket in Http2ServerRequest
// & Http2ServerResponse - this proxy socket should mimic the
// behaviour of http1 but against the http2 api & model
const errMsg = {
code: 'ERR_HTTP2_NO_SOCKET_MANIPULATION',
type: Error,
message: 'HTTP/2 sockets should not be directly read from, written to, ' +
'paused and/or resumed.'
};
const server = h2.createServer();
server.on('request', common.mustCall(function(request, response) {
assert.ok(request.socket instanceof net.Socket);
assert.ok(response.socket instanceof net.Socket);
assert.strictEqual(request.socket, response.socket);
assert.ok(request.socket.readable);
request.resume();
assert.ok(request.socket.writable);
assert.strictEqual(request.socket.destroyed, false);
request.socket.setTimeout(987);
assert.strictEqual(request.stream.session._idleTimeout, 987);
request.socket.setTimeout(0);
common.expectsError(() => request.socket.read(), errMsg);
common.expectsError(() => request.socket.write(), errMsg);
common.expectsError(() => request.socket.pause(), errMsg);
common.expectsError(() => request.socket.resume(), errMsg);
// should have correct this context for socket methods & getters
assert.ok(request.socket.address() != null);
assert.ok(request.socket.remotePort);
request.on('end', common.mustCall(() => {
assert.strictEqual(request.socket.readable, false);
assert.doesNotThrow(() => response.socket.destroy());
}));
response.on('finish', common.mustCall(() => {
assert.ok(request.socket);
assert.strictEqual(response.socket, undefined);
assert.ok(request.socket.destroyed);
assert.strictEqual(request.socket.readable, false);
process.nextTick(() => {
assert.strictEqual(request.socket.writable, false);
server.close();
});
}));
// properties that do not exist on the proxy are retrieved from the socket
assert.ok(request.socket._server);
assert.strictEqual(request.socket.connecting, false);
// socket events are bound and emitted on Http2Stream
request.socket.on('streamClosed', common.mustCall());
request.socket.once('streamClosed', common.mustCall());
request.socket.on('testEvent', common.mustCall());
request.socket.emit('testEvent');
}));
server.listen(0, common.mustCall(function() {
const port = server.address().port;
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(() => {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('end', common.mustCall(() => {
client.destroy();
}));
request.end();
request.resume();
}));
}));

View File

@ -3,6 +3,7 @@
const common = require('../common'); const common = require('../common');
if (!common.hasCrypto) if (!common.hasCrypto)
common.skip('missing crypto'); common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2'); const h2 = require('http2');
const server = h2.createServer(); const server = h2.createServer();
@ -32,11 +33,9 @@ server.on('stream', common.mustCall((stream) => {
}, common.mustCall((pushedStream) => { }, common.mustCall((pushedStream) => {
pushedStream.respond({ ':status': 200 }); pushedStream.respond({ ':status': 200 });
pushedStream.on('aborted', common.mustCall()); pushedStream.on('aborted', common.mustCall());
pushedStream.on('error', common.mustCall(common.expectsError({ pushedStream.on('error', common.mustNotCall());
code: 'ERR_HTTP2_STREAM_ERROR', pushedStream.on('streamClosed',
type: Error, common.mustCall((code) => assert.strictEqual(code, 8)));
message: 'Stream closed with error code 8'
})));
})); }));
stream.end('hello world'); stream.end('hello world');

View File

@ -17,7 +17,7 @@ const {
NGHTTP2_INTERNAL_ERROR NGHTTP2_INTERNAL_ERROR
} = http2.constants; } = http2.constants;
const errCheck = common.expectsError({ code: 'ERR_HTTP2_STREAM_ERROR' }, 8); const errCheck = common.expectsError({ code: 'ERR_HTTP2_STREAM_ERROR' }, 6);
function checkRstCode(rstMethod, expectRstCode) { function checkRstCode(rstMethod, expectRstCode) {
const server = http2.createServer(); const server = http2.createServer();
@ -32,8 +32,11 @@ function checkRstCode(rstMethod, expectRstCode) {
else else
stream[rstMethod](); stream[rstMethod]();
if (expectRstCode > NGHTTP2_NO_ERROR) { if (expectRstCode !== NGHTTP2_NO_ERROR &&
expectRstCode !== NGHTTP2_CANCEL) {
stream.on('error', common.mustCall(errCheck)); stream.on('error', common.mustCall(errCheck));
} else {
stream.on('error', common.mustNotCall());
} }
}); });
@ -58,8 +61,11 @@ function checkRstCode(rstMethod, expectRstCode) {
req.on('aborted', common.mustCall()); req.on('aborted', common.mustCall());
req.on('end', common.mustCall()); req.on('end', common.mustCall());
if (expectRstCode > NGHTTP2_NO_ERROR) { if (expectRstCode !== NGHTTP2_NO_ERROR &&
expectRstCode !== NGHTTP2_CANCEL) {
req.on('error', common.mustCall(errCheck)); req.on('error', common.mustCall(errCheck));
} else {
req.on('error', common.mustNotCall());
} }
})); }));

View File

@ -0,0 +1,28 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
let client;
let req;
const server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
stream.on('error', common.mustCall(() => {
stream.on('streamClosed', common.mustCall((code) => {
assert.strictEqual(code, 2);
client.destroy();
server.close();
}));
}));
req.rstStream(2);
}));
server.listen(0, common.mustCall(() => {
client = http2.connect(`http://localhost:${server.address().port}`);
req = client.request();
req.resume();
req.on('error', common.mustCall());
}));