Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP/1 createServer() options to pass custom Request and Response classes #15752

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -1661,10 +1661,17 @@ A collection of all the standard HTTP response status codes, and the
short description of each. For example, `http.STATUS_CODES[404] === 'Not
Found'`.

## http.createServer([requestListener])
## http.createServer([options][, requestListener])
<!-- YAML
added: v0.1.13
-->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add the fact that the options argument is supported now to the changelog? i.e.

<!-- YAML
added: v0.1.13
changes:
  - version: REPLACEME
    pr-url: https://github.com/nodejs/node/pull/15752
    description: The `options` argument is supported now.
-->

(This could also be done when landing, I guess, so I’m not removing the ready label.)

- `options` {Object}
* `IncomingMessage` {http.IncomingMessage} Specifies the IncomingMessage class to
be used. Useful for extending the original `IncomingMessage`.
Defaults to: `IncomingMessage`
* `ServerResponse` {http.ServerResponse} Specifies the ServerResponse class to
be used. Useful for extending the original `ServerResponse`.
Defaults to: `ServerResponse`
- `requestListener` {Function}

* Returns: {http.Server}
Expand Down
6 changes: 6 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,12 @@ changes:
used to determine the padding. See [Using options.selectPadding][].
* `settings` {[Settings Object][]} The initial settings to send to the
remote peer upon connection.
* `Http1IncomingMessage` {http.IncomingMessage} Specifies the IncomingMessage class to
used for HTTP/1 fallback. Useful for extending the original `http.IncomingMessage`.
Defaults to: `http.IncomingMessage`
* `Http1ServerResponse` {http.ServerResponse} Specifies the ServerResponse class to
used for HTTP/1 fallback. Useful for extending the original `http.ServerResponse`.
Defaults to: `http.ServerResponse`
* `onRequestHandler` {Function} See [Compatibility API][]
* Returns: {Http2Server}

Expand Down
4 changes: 3 additions & 1 deletion doc/api/https.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ See [`http.Server#keepAliveTimeout`][].
<!-- YAML
added: v0.3.4
-->
- `options` {Object} Accepts `options` from [`tls.createServer()`][] and [`tls.createSecureContext()`][].
- `options` {Object} Accepts `options` from [`tls.createServer()`][],
[`tls.createSecureContext()`][] and [`http.createServer()`][].
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a link at the bottom for http.createServer().

- `requestListener` {Function} A listener to be added to the `request` event.

Example:
Expand Down Expand Up @@ -258,6 +259,7 @@ const req = https.request(options, (res) => {
[`http.Server#setTimeout()`]: http.html#http_server_settimeout_msecs_callback
[`http.Server#timeout`]: http.html#http_server_timeout
[`http.Server`]: http.html#http_class_http_server
[`http.createServer()`]: http.html#httpcreateserveroptions-requestlistener
[`http.close()`]: http.html#http_server_close_callback
[`http.get()`]: http.html#http_http_get_options_callback
[`http.request()`]: http.html#http_http_request_options_callback
Expand Down
10 changes: 8 additions & 2 deletions lib/_http_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const {

const debug = require('util').debuglog('http');

const kIncomingMessage = Symbol('IncomingMessage');
const kOnHeaders = HTTPParser.kOnHeaders | 0;
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
const kOnBody = HTTPParser.kOnBody | 0;
Expand Down Expand Up @@ -73,7 +74,11 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
parser._url = '';
}

parser.incoming = new IncomingMessage(parser.socket);
// Parser is also used by http client
var ParserIncomingMessage = parser.socket && parser.socket.server ?
parser.socket.server[kIncomingMessage] : IncomingMessage;

parser.incoming = new ParserIncomingMessage(parser.socket);
parser.incoming.httpVersionMajor = versionMajor;
parser.incoming.httpVersionMinor = versionMinor;
parser.incoming.httpVersion = `${versionMajor}.${versionMinor}`;
Expand Down Expand Up @@ -300,5 +305,6 @@ module.exports = {
freeParser,
httpSocketSetup,
methods,
parsers
parsers,
kIncomingMessage
};
23 changes: 19 additions & 4 deletions lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const {
continueExpression,
chunkExpression,
httpSocketSetup,
kIncomingMessage,
_checkInvalidHeaderChar: checkInvalidHeaderChar
} = require('_http_common');
const { OutgoingMessage } = require('_http_outgoing');
Expand All @@ -41,9 +42,12 @@ const {
defaultTriggerAsyncIdScope,
getOrSetAsyncId
} = require('internal/async_hooks');
const { IncomingMessage } = require('_http_incoming');
const errors = require('internal/errors');
const Buffer = require('buffer').Buffer;

const kServerResponse = Symbol('ServerResponse');

const STATUS_CODES = {
100: 'Continue',
101: 'Switching Protocols',
Expand Down Expand Up @@ -263,9 +267,19 @@ function writeHead(statusCode, reason, obj) {
// Docs-only deprecated: DEP0063
ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead;

function Server(options, requestListener) {
if (!(this instanceof Server)) return new Server(options, requestListener);

if (typeof options === 'function') {
requestListener = options;
options = {};
} else if (options == null || typeof options === 'object') {
options = util._extend({}, options);
}

this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
this[kServerResponse] = options.ServerResponse || ServerResponse;

function Server(requestListener) {
if (!(this instanceof Server)) return new Server(requestListener);
net.Server.call(this, { allowHalfOpen: true });

if (requestListener) {
Expand Down Expand Up @@ -587,7 +601,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
}
}

var res = new ServerResponse(req);
var res = new server[kServerResponse](req);
res._onPendingData = updateOutgoingData.bind(undefined, socket, state);

res.shouldKeepAlive = keepAlive;
Expand Down Expand Up @@ -690,5 +704,6 @@ module.exports = {
STATUS_CODES,
Server,
ServerResponse,
_connectionListener: connectionListener
_connectionListener: connectionListener,
kServerResponse
};
6 changes: 6 additions & 0 deletions lib/https.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const { inherits } = util;
const debug = util.debuglog('https');
const { urlToOptions, searchParamsSymbol } = require('internal/url');
const errors = require('internal/errors');
const { IncomingMessage, ServerResponse } = require('http');
const { kIncomingMessage } = require('_http_common');
const { kServerResponse } = require('_http_server');

function Server(opts, requestListener) {
if (!(this instanceof Server)) return new Server(opts, requestListener);
Expand All @@ -57,6 +60,9 @@ function Server(opts, requestListener) {
opts.ALPNProtocols = ['http/1.1'];
}

this[kIncomingMessage] = opts.IncomingMessage || IncomingMessage;
this[kServerResponse] = opts.ServerResponse || ServerResponse;

tls.Server.call(this, opts, _connectionListener);

this.httpAllowHalfOpen = false;
Expand Down
15 changes: 14 additions & 1 deletion lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require('internal/util').assertCrypto();

const { async_id_symbol } = process.binding('async_wrap');
const http = require('http');
const binding = process.binding('http2');
const assert = require('assert');
const { Buffer } = require('buffer');
Expand Down Expand Up @@ -69,6 +70,8 @@ const NETServer = net.Server;
const TLSServer = tls.Server;

const kInspect = require('internal/util').customInspectSymbol;
const { kIncomingMessage } = require('_http_common');
const { kServerResponse } = require('_http_server');

const kAlpnProtocol = Symbol('alpnProtocol');
const kAuthority = Symbol('authority');
Expand Down Expand Up @@ -2454,8 +2457,11 @@ function connectionListener(socket) {

if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') {
// Fallback to HTTP/1.1
if (options.allowHTTP1 === true)
if (options.allowHTTP1 === true) {
socket.server[kIncomingMessage] = options.Http1IncomingMessage;
socket.server[kServerResponse] = options.Http1ServerResponse;
return httpConnectionListener.call(this, socket);
}
// Let event handler deal with the socket
if (!this.emit('unknownProtocol', socket))
socket.destroy();
Expand Down Expand Up @@ -2486,6 +2492,13 @@ function initializeOptions(options) {
options.allowHalfOpen = true;
assertIsObject(options.settings, 'options.settings');
options.settings = Object.assign({}, options.settings);

// Used only with allowHTTP1
options.Http1IncomingMessage = options.Http1IncomingMessage ||
http.IncomingMessage;
options.Http1ServerResponse = options.Http1ServerResponse ||
http.ServerResponse;

return options;
}

Expand Down
41 changes: 41 additions & 0 deletions test/parallel/test-http-server-options-incoming-message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

/**
* This test covers http.Server({ IncomingMessage }) option:
* With IncomingMessage option the server should use
* the new class for creating req Object instead of the default
* http.IncomingMessage.
*/
const common = require('../common');
const assert = require('assert');
const http = require('http');

class MyIncomingMessage extends http.IncomingMessage {
getUserAgent() {
return this.headers['user-agent'] || 'unknown';
}
}

const server = http.Server({
IncomingMessage: MyIncomingMessage
}, common.mustCall(function(req, res) {
assert.strictEqual(req.getUserAgent(), 'node-test');
res.statusCode = 200;
res.end();
}));
server.listen();

server.on('listening', function makeRequest() {
http.get({
port: this.address().port,
headers: {
'User-Agent': 'node-test'
}
}, (res) => {
assert.strictEqual(res.statusCode, 200);
res.on('end', () => {
server.close();
});
res.resume();
});
});
35 changes: 35 additions & 0 deletions test/parallel/test-http-server-options-server-response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

/**
* This test covers http.Server({ ServerResponse }) option:
* With ServerResponse option the server should use
* the new class for creating res Object instead of the default
* http.ServerResponse.
*/
const common = require('../common');
const assert = require('assert');
const http = require('http');

class MyServerResponse extends http.ServerResponse {
status(code) {
return this.writeHead(code, { 'Content-Type': 'text/plain' });
}
}

const server = http.Server({
ServerResponse: MyServerResponse
}, common.mustCall(function(req, res) {
res.status(200);
res.end();
}));
server.listen();

server.on('listening', function makeRequest() {
http.get({ port: this.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
res.on('end', () => {
server.close();
});
res.resume();
});
});
90 changes: 90 additions & 0 deletions test/parallel/test-http2-https-fallback-http-server-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Flags: --expose-http2
'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');

if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const url = require('url');
const tls = require('tls');
const http2 = require('http2');
const https = require('https');
const http = require('http');

const key = fixtures.readKey('agent8-key.pem');
const cert = fixtures.readKey('agent8-cert.pem');
const ca = fixtures.readKey('fake-startcom-root-cert.pem');

function onRequest(request, response) {
const { socket: { alpnProtocol } } = request.httpVersion === '2.0' ?
request.stream.session : request;
response.status(200);
response.end(JSON.stringify({
alpnProtocol,
httpVersion: request.httpVersion,
userAgent: request.getUserAgent()
}));
}

class MyIncomingMessage extends http.IncomingMessage {
getUserAgent() {
return this.headers['user-agent'] || 'unknown';
}
}

class MyServerResponse extends http.ServerResponse {
status(code) {
return this.writeHead(code, { 'Content-Type': 'application/json' });
}
}

// HTTP/2 & HTTP/1.1 server
{
const server = http2.createSecureServer(
{
cert,
key, allowHTTP1: true,
Http1IncomingMessage: MyIncomingMessage,
Http1ServerResponse: MyServerResponse
},
common.mustCall(onRequest, 1)
);

server.listen(0);

server.on('listening', common.mustCall(() => {
const { port } = server.address();
const origin = `https://localhost:${port}`;

// HTTP/1.1 client
https.get(
Object.assign(url.parse(origin), {
secureContext: tls.createSecureContext({ ca }),
headers: { 'User-Agent': 'node-test' }
}),
common.mustCall((response) => {
assert.strictEqual(response.statusCode, 200);
assert.strictEqual(response.statusMessage, 'OK');
assert.strictEqual(
response.headers['content-type'],
'application/json'
);

response.setEncoding('utf8');
let raw = '';
response.on('data', (chunk) => { raw += chunk; });
response.on('end', common.mustCall(() => {
const { alpnProtocol, httpVersion, userAgent } = JSON.parse(raw);
assert.strictEqual(alpnProtocol, false);
assert.strictEqual(httpVersion, '1.1');
assert.strictEqual(userAgent, 'node-test');

server.close();
}));
})
);
}));
}
Loading