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:
parent
4f339b54e9
commit
2da7d9b820
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
-->
|
-->
|
||||||
|
@ -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',
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
}));
|
}));
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
@ -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));
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}));
|
}));
|
||||||
|
@ -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();
|
||||||
|
}));
|
||||||
|
}));
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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() {
|
||||||
|
107
test/parallel/test-http2-compat-socket-set.js
Normal file
107
test/parallel/test-http2-compat-socket-set.js
Normal 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();
|
||||||
|
}));
|
||||||
|
}));
|
89
test/parallel/test-http2-compat-socket.js
Normal file
89
test/parallel/test-http2-compat-socket.js
Normal 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();
|
||||||
|
}));
|
||||||
|
}));
|
@ -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');
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
28
test/parallel/test-http2-stream-destroy-event-order.js
Normal file
28
test/parallel/test-http2-stream-destroy-event-order.js
Normal 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());
|
||||||
|
}));
|
Loading…
x
Reference in New Issue
Block a user