diff --git a/doc/api/http2.md b/doc/api/http2.md index f10bd26773..8bed0c935a 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -921,6 +921,27 @@ added: REPLACEME Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer. +#### http2stream.headersSent + + +* Value: {boolean} + +Boolean (read-only). True if headers were sent, false otherwise. + +#### http2stream.pushAllowed + + +* Value: {boolean} + +Read-only property mapped to the `SETTINGS_ENABLE_PUSH` flag of the remote +client's most recent `SETTINGS` frame. Will be `true` if the remote peer +accepts push streams, `false` otherwise. Settings are the same for every +`Http2Stream` in the same `Http2Session`. + #### http2stream.pushStream(headers[, options], callback) diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index 89381ce639..9195285097 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -46,29 +46,29 @@ function isPseudoHeader(name) { } function onStreamData(chunk) { - var request = this[kRequest]; + const request = this[kRequest]; if (!request.push(chunk)) this.pause(); } function onStreamEnd() { // Cause the request stream to end as well. - var request = this[kRequest]; + const request = this[kRequest]; request.push(null); } function onStreamError(error) { - var request = this[kRequest]; + const request = this[kRequest]; request.emit('error', error); } function onRequestPause() { - var stream = this[kStream]; + const stream = this[kStream]; stream.pause(); } function onRequestResume() { - var stream = this[kStream]; + const stream = this[kStream]; stream.resume(); } @@ -78,22 +78,22 @@ function onRequestDrain() { } function onStreamResponseDrain() { - var response = this[kResponse]; + const response = this[kResponse]; response.emit('drain'); } function onStreamResponseError(error) { - var response = this[kResponse]; + const response = this[kResponse]; response.emit('error', error); } function onStreamClosedRequest() { - var req = this[kRequest]; + const req = this[kRequest]; req.push(null); } function onStreamClosedResponse() { - var res = this[kResponse]; + const res = this[kResponse]; res.writable = false; res.emit('finish'); } @@ -133,12 +133,12 @@ class Http2ServerRequest extends Readable { } get closed() { - var state = this[kState]; + const state = this[kState]; return Boolean(state.closed); } get code() { - var state = this[kState]; + const state = this[kState]; return Number(state.closedCode); } @@ -155,11 +155,11 @@ class Http2ServerRequest extends Readable { } get rawHeaders() { - var headers = this[kHeaders]; + const headers = this[kHeaders]; if (headers === undefined) return []; - var tuples = Object.entries(headers); - var flattened = Array.prototype.concat.apply([], tuples); + const tuples = Object.entries(headers); + const flattened = Array.prototype.concat.apply([], tuples); return flattened.map(String); } @@ -188,7 +188,7 @@ class Http2ServerRequest extends Readable { } _read(nread) { - var stream = this[kStream]; + const stream = this[kStream]; if (stream) { stream.resume(); } else { @@ -197,21 +197,21 @@ class Http2ServerRequest extends Readable { } get method() { - var headers = this[kHeaders]; + const headers = this[kHeaders]; if (headers === undefined) return; return headers[constants.HTTP2_HEADER_METHOD]; } get authority() { - var headers = this[kHeaders]; + const headers = this[kHeaders]; if (headers === undefined) return; return headers[constants.HTTP2_HEADER_AUTHORITY]; } get scheme() { - var headers = this[kHeaders]; + const headers = this[kHeaders]; if (headers === undefined) return; return headers[constants.HTTP2_HEADER_SCHEME]; @@ -226,27 +226,27 @@ class Http2ServerRequest extends Readable { } get path() { - var headers = this[kHeaders]; + const headers = this[kHeaders]; if (headers === undefined) return; return headers[constants.HTTP2_HEADER_PATH]; } set path(path) { - var headers = this[kHeaders]; + let headers = this[kHeaders]; if (headers === undefined) headers = this[kHeaders] = Object.create(null); headers[constants.HTTP2_HEADER_PATH] = path; } setTimeout(msecs, callback) { - var stream = this[kStream]; + const stream = this[kStream]; if (stream === undefined) return; stream.setTimeout(msecs, callback); } [kFinish](code) { - var state = this[kState]; + const state = this[kState]; if (state.closed) return; state.closedCode = code; @@ -262,7 +262,6 @@ class Http2ServerResponse extends Stream { this[kState] = { sendDate: true, statusCode: constants.HTTP_STATUS_OK, - headersSent: false, headerCount: 0, trailerCount: 0, closed: false, @@ -281,17 +280,17 @@ class Http2ServerResponse extends Stream { } get finished() { - var stream = this[kStream]; + const stream = this[kStream]; return stream === undefined || stream._writableState.ended; } get closed() { - var state = this[kState]; + const state = this[kState]; return Boolean(state.closed); } get code() { - var state = this[kState]; + const state = this[kState]; return Number(state.closedCode); } @@ -300,8 +299,8 @@ class Http2ServerResponse extends Stream { } get headersSent() { - var state = this[kState]; - return state.headersSent; + const stream = this[kStream]; + return stream.headersSent; } get sendDate() { @@ -317,7 +316,7 @@ class Http2ServerResponse extends Stream { } set statusCode(code) { - var state = this[kState]; + const state = this[kState]; code |= 0; if (code >= 100 && code < 200) throw new errors.RangeError('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED'); @@ -327,23 +326,23 @@ class Http2ServerResponse extends Stream { } addTrailers(headers) { - var trailers = this[kTrailers]; - var keys = Object.keys(headers); - var key = ''; + let trailers = this[kTrailers]; + const keys = Object.keys(headers); + let key = ''; if (keys.length > 0) return; if (trailers === undefined) trailers = this[kTrailers] = Object.create(null); for (var i = 0; i < keys.length; i++) { key = String(keys[i]).trim().toLowerCase(); - var value = headers[key]; + const value = headers[key]; assertValidHeader(key, value); trailers[key] = String(value); } } getHeader(name) { - var headers = this[kHeaders]; + const headers = this[kHeaders]; if (headers === undefined) return; name = String(name).trim().toLowerCase(); @@ -351,19 +350,19 @@ class Http2ServerResponse extends Stream { } getHeaderNames() { - var headers = this[kHeaders]; + const headers = this[kHeaders]; if (headers === undefined) return []; return Object.keys(headers); } getHeaders() { - var headers = this[kHeaders]; + const headers = this[kHeaders]; return Object.assign({}, headers); } hasHeader(name) { - var headers = this[kHeaders]; + const headers = this[kHeaders]; if (headers === undefined) return false; name = String(name).trim().toLowerCase(); @@ -371,7 +370,7 @@ class Http2ServerResponse extends Stream { } removeHeader(name) { - var headers = this[kHeaders]; + const headers = this[kHeaders]; if (headers === undefined) return; name = String(name).trim().toLowerCase(); @@ -381,14 +380,14 @@ class Http2ServerResponse extends Stream { setHeader(name, value) { name = String(name).trim().toLowerCase(); assertValidHeader(name, value); - var headers = this[kHeaders]; + let headers = this[kHeaders]; if (headers === undefined) headers = this[kHeaders] = Object.create(null); headers[name] = String(value); } flushHeaders() { - if (this[kState].headersSent === false) + if (this[kStream].headersSent === false) this[kBeginSend](); } @@ -404,8 +403,8 @@ class Http2ServerResponse extends Stream { headers = statusMessage; } if (headers) { - var keys = Object.keys(headers); - var key = ''; + const keys = Object.keys(headers); + let key = ''; for (var i = 0; i < keys.length; i++) { key = keys[i]; this.setHeader(key, headers[key]); @@ -415,7 +414,7 @@ class Http2ServerResponse extends Stream { } write(chunk, encoding, cb) { - var stream = this[kStream]; + const stream = this[kStream]; if (typeof encoding === 'function') { cb = encoding; @@ -423,7 +422,7 @@ class Http2ServerResponse extends Stream { } if (stream === undefined) { - var err = new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED'); if (cb) process.nextTick(cb, err); else @@ -435,35 +434,33 @@ class Http2ServerResponse extends Stream { } end(chunk, encoding, cb) { - var stream = this[kStream]; + const stream = this[kStream]; if (typeof chunk === 'function') { cb = chunk; - chunk = ''; + chunk = null; encoding = 'utf8'; } else if (typeof encoding === 'function') { cb = encoding; encoding = 'utf8'; } - - if (stream === undefined) { - var err = new errors.Error('ERR_HTTP2_STREAM_CLOSED'); - if (cb) - process.nextTick(cb, err); - else - throw err; - return; + if (chunk !== null && chunk !== undefined) { + this.write(chunk, encoding); } - if (chunk !== undefined) - this.write(chunk, encoding); + if (typeof cb === 'function' && stream !== undefined) { + stream.once('finish', cb); + } this[kBeginSend]({endStream: true}); - stream.end(); + + if (stream !== undefined) { + stream.end(); + } } destroy(err) { - var stream = this[kStream]; + const stream = this[kStream]; if (stream === undefined) { // nothing to do, already closed return; @@ -472,7 +469,7 @@ class Http2ServerResponse extends Stream { } setTimeout(msecs, callback) { - var stream = this[kStream]; + const stream = this[kStream]; if (stream === undefined) return; stream.setTimeout(msecs, callback); } @@ -482,48 +479,48 @@ class Http2ServerResponse extends Stream { } sendInfo(code, headers) { - var state = this[kState]; - if (state.headersSent === true) { + const stream = this[kStream]; + if (stream.headersSent === true) { throw new errors.Error('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND'); } if (headers && typeof headers !== 'object') throw new errors.TypeError('ERR_HTTP2_HEADERS_OBJECT'); - var stream = this[kStream]; if (stream === undefined) return; code |= 0; if (code < 100 || code >= 200) throw new errors.RangeError('ERR_HTTP2_INVALID_INFO_STATUS', code); - state.headersSent = true; headers[constants.HTTP2_HEADER_STATUS] = code; stream.respond(headers); } createPushResponse(headers, callback) { - var stream = this[kStream]; + const stream = this[kStream]; if (stream === undefined) { throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); } stream.pushStream(headers, {}, function(stream, headers, options) { - var response = new Http2ServerResponse(stream); + const response = new Http2ServerResponse(stream); callback(null, response); }); } [kBeginSend](options) { - var state = this[kState]; - var stream = this[kStream]; - if (state.headersSent === false) { - state.headersSent = true; + const stream = this[kStream]; + if (stream !== undefined && stream.headersSent === false) { + const state = this[kState]; const headers = this[kHeaders] || Object.create(null); headers[constants.HTTP2_HEADER_STATUS] = state.statusCode; - if (stream.destroyed === false) + if (stream.finished === true) + options.endStream = true; + if (stream.destroyed === false) { stream.respond(headers, options); + } } } [kFinish](code) { - var state = this[kState]; + const state = this[kState]; if (state.closed) return; state.closedCode = code; @@ -535,12 +532,12 @@ class Http2ServerResponse extends Stream { } function onServerStream(stream, headers, flags) { - var server = this; - var request = new Http2ServerRequest(stream, headers); - var response = new Http2ServerResponse(stream); + const server = this; + const request = new Http2ServerRequest(stream, headers); + const response = new Http2ServerResponse(stream); // Check for the CONNECT method - var method = headers[constants.HTTP2_HEADER_METHOD]; + const method = headers[constants.HTTP2_HEADER_METHOD]; if (method === 'CONNECT') { if (!server.emit('connect', request, response)) { response.statusCode = constants.HTTP_STATUS_METHOD_NOT_ALLOWED; diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 014ea1f850..b0dd59021e 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1536,6 +1536,11 @@ class ServerHttp2Stream extends Http2Stream { debug(`[${sessionName(session[kType])}] created serverhttp2stream`); } + // true if the HEADERS frame has been sent + get headersSent() { + return this[kState].headersSent; + } + // true if the remote peer accepts push streams get pushAllowed() { return this[kSession].remoteSettings.enablePush; @@ -2008,7 +2013,7 @@ function sessionOnPriority(stream, parent, weight, exclusive) { } function connectionListener(socket) { - debug('server received a connection'); + debug('[server] received a connection'); const options = this[kOptions] || {}; if (this.timeout) { diff --git a/test/parallel/test-http2-compat-serverresponse-end.js b/test/parallel/test-http2-compat-serverresponse-end.js index 0234d7b442..1274f3d6b3 100644 --- a/test/parallel/test-http2-compat-serverresponse-end.js +++ b/test/parallel/test-http2-compat-serverresponse-end.js @@ -1,22 +1,28 @@ // Flags: --expose-http2 'use strict'; +const { strictEqual } = require('assert'); const { mustCall, mustNotCall } = require('../common'); -const { createServer, connect } = require('http2'); - -// Http2ServerResponse.end +const { + createServer, + connect, + constants: { + HTTP2_HEADER_STATUS, + HTTP_STATUS_OK + } +} = require('http2'); { - const server = createServer(); - server.listen(0, mustCall(() => { - const port = server.address().port; - server.once('request', mustCall((request, response) => { - response.on('finish', mustCall(() => { - server.close(); - })); - response.end(); + // Http2ServerResponse.end callback is called only the first time, + // but may be invoked repeatedly without throwing errors. + const server = createServer(mustCall((request, response) => { + response.end(mustCall(() => { + server.close(); })); - + response.end(mustNotCall()); + })); + server.listen(0, mustCall(() => { + const { port } = server.address(); const url = `http://localhost:${port}`; const client = connect(url, mustCall(() => { const headers = { @@ -33,3 +39,39 @@ const { createServer, connect } = require('http2'); })); })); } + +{ + // Http2ServerResponse.end is not necessary on HEAD requests since the stream + // is already closed. Headers, however, can still be sent to the client. + const server = createServer(mustCall((request, response) => { + strictEqual(response.finished, true); + response.writeHead(HTTP_STATUS_OK, { foo: 'bar' }); + response.flushHeaders(); + 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 + strictEqual(headers.foo, 'bar'); + })); + request.on('data', mustNotCall()); + request.on('end', mustCall(() => { + client.destroy(); + server.close(); + })); + request.end(); + request.resume(); + })); + })); +} diff --git a/test/parallel/test-http2-connect.js b/test/parallel/test-http2-connect.js new file mode 100644 index 0000000000..305ea034c9 --- /dev/null +++ b/test/parallel/test-http2-connect.js @@ -0,0 +1,29 @@ +// Flags: --expose-http2 +'use strict'; + +const { mustCall } = require('../common'); +const { doesNotThrow } = require('assert'); +const { createServer, connect } = require('http2'); + +const server = createServer(); +server.listen(0, mustCall(() => { + const authority = `http://localhost:${server.address().port}`; + const options = {}; + const listener = () => mustCall(); + + const clients = new Set(); + doesNotThrow(() => clients.add(connect(authority))); + doesNotThrow(() => clients.add(connect(authority, options))); + doesNotThrow(() => clients.add(connect(authority, options, listener()))); + doesNotThrow(() => clients.add(connect(authority, listener()))); + + for (const client of clients) { + client.once('connect', mustCall((headers) => { + client.destroy(); + clients.delete(client); + if (clients.size === 0) { + server.close(); + } + })); + } +})); diff --git a/test/parallel/test-http2-respond-file-compat.js b/test/parallel/test-http2-respond-file-compat.js new file mode 100644 index 0000000000..be256ee2bf --- /dev/null +++ b/test/parallel/test-http2-respond-file-compat.js @@ -0,0 +1,23 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const path = require('path'); + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); + +const server = http2.createServer(common.mustCall((request, response) => { + response.stream.respondWithFile(fname); +})); +server.listen(0, () => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall()); + req.on('end', common.mustCall(() => { + client.destroy(); + server.close(); + })); + req.end(); + req.resume(); +}); diff --git a/test/parallel/test-http2-respond-file.js b/test/parallel/test-http2-respond-file.js index d5b65a1caa..81babb58fa 100644 --- a/test/parallel/test-http2-respond-file.js +++ b/test/parallel/test-http2-respond-file.js @@ -34,7 +34,6 @@ server.listen(0, () => { const req = client.request(); req.on('response', common.mustCall((headers) => { - console.log(headers); assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length); assert.strictEqual(headers[HTTP2_HEADER_LAST_MODIFIED],