diff --git a/doc/api/http2.md b/doc/api/http2.md index cc7f3ea3b501e2..6f106c69c1b18b 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -16,9 +16,10 @@ in order to use the `'http2'` module. The Core API provides a low-level interface designed specifically around support for HTTP/2 protocol features. It is specifically *not* designed for -compatibility with the existing [HTTP/1][] module API. +compatibility with the existing [HTTP/1][] module API. However, the [Compatibility API][] is. -The following illustrates a simple, plain-text HTTP/2 server: +The following illustrates a simple, plain-text HTTP/2 server using the +Core API: ```js const http2 = require('http2'); @@ -27,6 +28,7 @@ const http2 = require('http2'); const server = http2.createServer(); server.on('stream', (stream, headers) => { + // stream is a Duplex stream.respond({ 'content-type': 'text/html', ':status': 200 @@ -44,6 +46,7 @@ const http2 = require('http2'); const client = http2.connect('http://localhost:80'); +// req is a Duplex const req = client.request({ ':path': '/' }); req.on('response', (headers) => { @@ -1171,6 +1174,17 @@ server.on('stream', (stream, headers, flags) => { }); ``` +#### Event: 'request' + + +* `request` {http2.Http2ServerRequest} +* `response` {http2.Http2ServerResponse} + +Emitted each time there is a request. Note that there may be multiple requests +per session. See the [Compatibility API](compatiblity-api). + #### Event: 'timeout' + +* `request` {http2.Http2ServerRequest} +* `response` {http2.Http2ServerResponse} + +Emitted each time there is a request. Note that there may be multiple requests +per session. See the [Compatibility API](compatiblity-api). + #### Event: 'timeout' + +A `Http2ServerRequest` object is created by [`http2.Server`][] or +[`http2.SecureServer`][] and passed as the first argument to the [`'request'`][] event. It may be used to access a request status, +headers and data. + +It implements the [Readable Stream][] interface, as well as the +following additional events, methods, and properties. + +#### Event: 'aborted' + + +The `'aborted'` event is emitted whenever a `Http2ServerRequest` instance is +abnormally aborted in mid-communication. + +*Note*: The `'aborted'` event will only be emitted if the +`Http2ServerRequest` writable side has not been ended. + +#### Event: 'close' + + +Indicates that the underlying [`Http2Stream`]() was closed. +Just like `'end'`, this event occurs only once per response. + +#### request.destroy([error]) + + +* `error` {Error} + +Calls `destroy()` on the [Http2Stream]() that received the `ServerRequest`. If `error` +is provided, an `'error'` event is emitted and `error` is passed as an argument +to any listeners on the event. + +It does nothing if the stream was already destroyed. + +#### request.headers + + +* {Object} + +The request/response headers object. + +Key-value pairs of header names and values. Header names are lower-cased. +Example: + +```js +// Prints something like: +// +// { 'user-agent': 'curl/7.22.0', +// host: '127.0.0.1:8000', +// accept: '*/*' } +console.log(request.headers); +``` + +See [Headers Object][]. + +### request.httpVersion + + +* {string} + +In case of server request, the HTTP version sent by the client. In the case of +client response, the HTTP version of the connected-to server. Returns +`'2.0'`. + +Also `message.httpVersionMajor` is the first integer and +`message.httpVersionMinor` is the second. + +#### request.method + + +* {string} + +The request method as a string. Read only. Example: +`'GET'`, `'DELETE'`. + +#### request.rawHeaders + + +* {Array} + +The raw request/response headers list exactly as they were received. + +Note that the keys and values are in the same list. It is *not* a +list of tuples. So, the even-numbered offsets are key values, and the +odd-numbered offsets are the associated values. + +Header names are not lowercased, and duplicates are not merged. + +```js +// Prints something like: +// +// [ 'user-agent', +// 'this is invalid because there can be only one', +// 'User-Agent', +// 'curl/7.22.0', +// 'Host', +// '127.0.0.1:8000', +// 'ACCEPT', +// '*/*' ] +console.log(request.rawHeaders); +``` + +#### request.rawTrailers + + +* {Array} + +The raw request/response trailer keys and values exactly as they were +received. Only populated at the `'end'` event. + +#### request.setTimeout(msecs, callback) + + +* `msecs` {number} +* `callback` {Function} + +Calls `request.connection.setTimeout(msecs, callback)`. + +Returns `request`. + +#### request.socket + + +* {net.Socket} + +The [`net.Socket`][] object associated with the connection. + +With TLS support, use [`request.socket.getPeerCertificate()`][] to obtain the +client's authentication details. + +*Note*: do not use this socket object to send or receive any data. All +data transfers are managed by HTTP/2 and data might be lost. + +#### request.stream + + +* {http2.Http2Stream} + +The [`Http2Stream`][] object backing the request. + +#### request.trailers + + +* {Object} + +The request/response trailers object. Only populated at the `'end'` event. + +#### request.url + + +* {string} + +Request URL string. This contains only the URL that is +present in the actual HTTP request. If the request is: + +```txt +GET /status?name=ryan HTTP/1.1\r\n +Accept: text/plain\r\n +\r\n +``` + +Then `request.url` will be: + + +```js +'/status?name=ryan' +``` + +To parse the url into its parts `require('url').parse(request.url)` +can be used. Example: + +```txt +$ node +> require('url').parse('/status?name=ryan') +Url { + protocol: null, + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: '?name=ryan', + query: 'name=ryan', + pathname: '/status', + path: '/status?name=ryan', + href: '/status?name=ryan' } +``` + +To extract the parameters from the query string, the +`require('querystring').parse` function can be used, or +`true` can be passed as the second argument to `require('url').parse`. +Example: + +```txt +$ node +> require('url').parse('/status?name=ryan', true) +Url { + protocol: null, + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: '?name=ryan', + query: { name: 'ryan' }, + pathname: '/status', + path: '/status?name=ryan', + href: '/status?name=ryan' } +``` + +### Class: http2.Http2ServerResponse + + +This object is created internally by an HTTP server--not by the user. It is +passed as the second parameter to the [`'request'`][] event. + +The response implements, but does not inherit from, the [Writable Stream][] +interface. This is an [`EventEmitter`][] with the following events: + +### Event: 'close' + + +Indicates that the underlying [`Http2Stream`]() was terminated before +[`response.end()`][] was called or able to flush. + +### Event: 'finish' + + +Emitted when the response has been sent. More specifically, this event is +emitted when the last segment of the response headers and body have been +handed off to the HTTP/2 multiplexing for transmission over the network. It +does not imply that the client has received anything yet. + +After this event, no more events will be emitted on the response object. + +### response.addTrailers(headers) + + +* `headers` {Object} + +This method adds HTTP trailing headers (a header but at the end of the +message) to the response. + +Attempting to set a header field name or value that contains invalid characters +will result in a [`TypeError`][] being thrown. + +### response.connection + + +* {net.Socket} + +See [`response.socket`][]. + +### response.end([data][, encoding][, callback]) + + +* `data` {string|Buffer} +* `encoding` {string} +* `callback` {Function} + +This method signals to the server that all of the response headers and body +have been sent; that server should consider this message complete. +The method, `response.end()`, MUST be called on each response. + +If `data` is specified, it is equivalent to calling +[`response.write(data, encoding)`][] followed by `response.end(callback)`. + +If `callback` is specified, it will be called when the response stream +is finished. + +### response.finished + + +* {boolean} + +Boolean value that indicates whether the response has completed. Starts +as `false`. After [`response.end()`][] executes, the value will be `true`. + +### response.getHeader(name) + + +* `name` {string} +* Returns: {string} + +Reads out a header that's already been queued but not sent to the client. +Note that the name is case insensitive. + +Example: + +```js +const contentType = response.getHeader('content-type'); +``` + +### response.getHeaderNames() + + +* Returns: {Array} + +Returns an array containing the unique names of the current outgoing headers. +All header names are lowercase. + +Example: + +```js +response.setHeader('Foo', 'bar'); +response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + +const headerNames = response.getHeaderNames(); +// headerNames === ['foo', 'set-cookie'] +``` + +### response.getHeaders() + + +* Returns: {Object} + +Returns a shallow copy of the current outgoing headers. Since a shallow copy +is used, array values may be mutated without additional calls to various +header-related http module methods. The keys of the returned object are the +header names and the values are the respective header values. All header names +are lowercase. + +*Note*: The object returned by the `response.getHeaders()` method _does not_ +prototypically inherit from the JavaScript `Object`. This means that typical +`Object` methods such as `obj.toString()`, `obj.hasOwnProperty()`, and others +are not defined and *will not work*. + +Example: + +```js +response.setHeader('Foo', 'bar'); +response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + +const headers = response.getHeaders(); +// headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] } +``` + +### response.hasHeader(name) + + +* `name` {string} +* Returns: {boolean} + +Returns `true` if the header identified by `name` is currently set in the +outgoing headers. Note that the header name matching is case-insensitive. + +Example: + +```js +const hasContentType = response.hasHeader('content-type'); +``` + +### response.headersSent + + +* {boolean} + +Boolean (read-only). True if headers were sent, false otherwise. + +### response.removeHeader(name) + + +* `name` {string} + +Removes a header that's queued for implicit sending. + +Example: + +```js +response.removeHeader('Content-Encoding'); +``` + +### response.sendDate + + +* {boolean} + +When true, the Date header will be automatically generated and sent in +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 +in responses. + +### response.setHeader(name, value) + + +* `name` {string} +* `value` {string | string[]} + +Sets a single header value for implicit headers. If this header already exists +in the to-be-sent headers, its value will be replaced. Use an array of strings +here to send multiple headers with the same name. + +Example: + +```js +response.setHeader('Content-Type', 'text/html'); +``` + +or + +```js +response.setHeader('Set-Cookie', ['type=ninja', 'language=javascript']); +``` + +Attempting to set a header field name or value that contains invalid characters +will result in a [`TypeError`][] being thrown. + +When headers have been set with [`response.setHeader()`][], they will be merged with +any headers passed to [`response.writeHead()`][], with the headers passed to +[`response.writeHead()`][] given precedence. + +```js +// returns content-type = text/plain +const server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Foo', 'bar'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); +}); +``` + +### response.setTimeout(msecs[, callback]) + + +* `msecs` {number} +* `callback` {Function} + +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 `response`. + +### response.socket + + +* {net.Socket} + +Reference to the underlying socket. Usually users will not want to access +this property. In particular, the socket will not emit `'readable'` events +because of how the protocol parser attaches to the socket. After +`response.end()`, the property is nulled. The `socket` may also be accessed +via `response.connection`. + +Example: + +```js +const http = require('http'); +const server = http.createServer((req, res) => { + const ip = req.socket.remoteAddress; + const port = req.socket.remotePort; + res.end(`Your IP address is ${ip} and your source port is ${port}.`); +}).listen(3000); +``` + +### response.statusCode + + +* {number} + +When using implicit headers (not calling [`response.writeHead()`][] explicitly), +this property controls the status code that will be sent to the client when +the headers get flushed. + +Example: + +```js +response.statusCode = 404; +``` + +After response header was sent to the client, this property indicates the +status code which was sent out. + +### response.statusMessage + + +* {string} + +Status message is not supported by HTTP/2 (RFC7540 8.1.2.4). It returns +an empty string. + +#### response.stream + + +* {http2.Http2Stream} + +The [`Http2Stream`][] object backing the response. + +### response.write(chunk[, encoding][, callback]) + + +* `chunk` {string|Buffer} +* `encoding` {string} +* `callback` {Function} +* Returns: {boolean} + +If this method is called and [`response.writeHead()`][] has not been called, +it will switch to implicit header mode and flush the implicit headers. + +This sends a chunk of the response body. This method may +be called multiple times to provide successive parts of the body. + +Note that in the `http` module, the response body is omitted when the +request is a HEAD request. Similarly, the `204` and `304` responses +_must not_ include a message body. + +`chunk` can be a string or a buffer. If `chunk` is a string, +the second parameter specifies how to encode it into a byte stream. +By default the `encoding` is `'utf8'`. `callback` will be called when this chunk +of data is flushed. + +*Note*: This is the raw HTTP body and has nothing to do with +higher-level multi-part body encodings that may be used. + +The first time [`response.write()`][] is called, it will send the buffered +header information and the first chunk of the body to the client. The second +time [`response.write()`][] is called, Node.js assumes data will be streamed, +and sends the new data separately. That is, the response is buffered up to the +first chunk of the body. + +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. +`'drain'` will be emitted when the buffer is free again. + +### response.writeContinue() + + +Does nothing. Added for parity with [HTTP/1](). + +### response.writeHead(statusCode[, statusMessage][, headers]) + + +* `statusCode` {number} +* `statusMessage` {string} +* `headers` {Object} + +Sends a response header to the request. The status code is a 3-digit HTTP +status code, like `404`. The last argument, `headers`, are the response headers. +For compatibility with [HTTP/1](), one can give a human-readable `statusMessage` as the second argument, which will be silenty ignored and emit a warning. + +Example: + +```js +const body = 'hello world'; +response.writeHead(200, { + 'Content-Length': Buffer.byteLength(body), + 'Content-Type': 'text/plain' }); +``` + +This method must only be called once on a message and it must +be called before [`response.end()`][] is called. + +If [`response.write()`][] or [`response.end()`][] are called before calling +this, the implicit/mutable headers will be calculated and call this function. + +When headers have been set with [`response.setHeader()`][], they will be merged with +any headers passed to [`response.writeHead()`][], with the headers passed to +[`response.writeHead()`][] given precedence. + +```js +// returns content-type = text/plain +const server = http2.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Foo', 'bar'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); +}); +``` + +Note that Content-Length is given in bytes not characters. The above example +works because the string `'hello world'` contains only single byte characters. +If the body contains higher coded characters then `Buffer.byteLength()` +should be used to determine the number of bytes in a given encoding. +And Node.js does not check whether Content-Length and the length of the body +which has been transmitted are equal or not. + +Attempting to set a header field name or value that contains invalid characters +will result in a [`TypeError`][] being thrown. + +### response.createPushResponse(headers, callback) + + +Call [`stream.pushStream()`]() with the given headers, and wraps the +given newly created [`Http2Stream`] on `Http2ServerRespose`. +The callback will be called with an error with code `ERR_HTTP2_STREAM_CLOSED` +if the stream is closed. [HTTP/2]: https://tools.ietf.org/html/rfc7540 [HTTP/1]: http.html +[https]: https.html [`net.Socket`]: net.html [`tls.TLSSocket`]: tls.html [`tls.createServer()`]: tls.html#tls_tls_createserver_options_secureconnectionlistener [`ClientHttp2Stream`]: #http2_class_clienthttp2stream [Compatibility API]: #http2_compatibility_api +[alpn-negotiation]: #http2_alpn_negotiation [`Duplex`]: stream.html#stream_class_stream_duplex [Headers Object]: #http2_headers_object [`Http2Stream`]: #http2_class_http2stream @@ -1720,3 +2485,7 @@ TBD [Using options.selectPadding]: #http2_using_options_selectpadding [error code]: #error_codes [`'unknownProtocol'`]: #http2_event_unknownprotocol +[`'request'`]: #http2_event_request +[Readable Stream]: stream.html#stream_class_stream_readable +[`ServerRequest`]: #http2_class_server_request +[`stream.pushStream()`]: #http2_stream-pushstream diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index c258079356183d..1d93ae14a0bfa2 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -192,7 +192,7 @@ class Http2ServerRequest extends Readable { if (stream) { stream.resume(); } else { - throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED')); } } @@ -391,6 +391,18 @@ class Http2ServerResponse extends Stream { this[kBeginSend](); } + get statusMessage() { + if (statusMessageWarned === false) { + process.emitWarning( + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)', + 'UnsupportedWarning' + ); + statusMessageWarned = true; + } + + return ''; + } + writeHead(statusCode, statusMessage, headers) { if (typeof statusMessage === 'string' && statusMessageWarned === false) { process.emitWarning( @@ -411,6 +423,7 @@ class Http2ServerResponse extends Stream { } } this.statusCode = statusCode; + // TODO mcollina this should probably call sendInfo } write(chunk, encoding, cb) { @@ -497,7 +510,8 @@ class Http2ServerResponse extends Stream { createPushResponse(headers, callback) { const stream = this[kStream]; if (stream === undefined) { - throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED')); + return; } stream.pushStream(headers, {}, function(stream, headers, options) { const response = new Http2ServerResponse(stream); @@ -529,6 +543,11 @@ class Http2ServerResponse extends Stream { this[kStream] = undefined; this.emit('finish'); } + + // added for parity with HTTP/1 + writeContinue() { + // TODO mcollina this should probably be sendContinue + } } function onServerStream(stream, headers, flags) { diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js index 68e438d62ff96d..9679215abb0772 100644 --- a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js +++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js @@ -22,6 +22,16 @@ const server = h2.createServer((request, response) => { assert.strictEqual(push.stream.id % 2, 0); push.end(pushExpect); response.end(); + + // wait for a tick, so the stream is actually closed + setImmediate(function() { + response.createPushResponse({ + ':path': '/pushed', + ':method': 'GET' + }, common.mustCall((error) => { + assert.strictEqual(error.code, 'ERR_HTTP2_STREAM_CLOSED'); + })); + }); })); }); diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js new file mode 100644 index 00000000000000..50d2970d9ea95f --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js @@ -0,0 +1,47 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.statusMessage should warn + +const unsupportedWarned = common.mustCall(1); +process.on('warning', ({ name, message }) => { + const expectedMessage = + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)'; + if (name === 'UnsupportedWarning' && message === expectedMessage) + unsupportedWarned(); +}); + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.on('finish', common.mustCall(function() { + assert.strictEqual(response.statusMessage, ''); + server.close(); + })); + response.end(); + })); + + 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('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200); + }, 1)); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +}));