Skip to content

Commit

Permalink
http: process 100, 102-199 according to specs.
Browse files Browse the repository at this point in the history
Adding ServerResponse.writeProcessing to send 102 status codes.

Added an `'information'` event to ClientRequest to handle
1xx status codes except 101 Upgrade.
101 Upgrade is excluded due to its non-informational
processing according to RFC7231, Section 6.2.2.

This affects several modules downstream that use the http
module, e.g., node-fetch, all of whom violate HTTP RFCs
due to this module. As such, this could introduce a
breaking change for downstream if HTTP standards were
ignored in an ad-hoc fashion.

See also RFC2518 RFC8297.

PR-URL: #18033
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Ruben Bridgewater <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Tiancheng "Timothy" Gu <[email protected]>
  • Loading branch information
miles-po authored and mcollina committed Feb 15, 2018
1 parent cfad441 commit baf8495
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 7 deletions.
40 changes: 40 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,37 @@ Emitted when the server sends a '100 Continue' HTTP response, usually because
the request contained 'Expect: 100-continue'. This is an instruction that
the client should send the request body.

### Event: 'information'
<!-- YAML
added: REPLACEME
-->

Emitted when the server sends a 1xx response (excluding 101 Upgrade). This
event is emitted with a callback containing an object with a status code.

```js
const http = require('http');

const options = {
hostname: '127.0.0.1',
port: 8080,
path: '/length_request'
};

// Make a request
const req = http.request(options);
req.end();

req.on('information', (res) => {
console.log('got information prior to main response: ' + res.statusCode);
});
```

101 Upgrade statuses do not fire this event due to their break from the
traditional HTTP request/response chain, such as web sockets, in-place TLS
upgrades, or HTTP 2.0. To be notified of 101 Upgrade notices, listen for the
[`'upgrade'`][] event instead.

### Event: 'response'
<!-- YAML
added: v0.1.0
Expand Down Expand Up @@ -1384,6 +1415,14 @@ 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.writeProcessing()
<!-- YAML
added: REPLACEME
-->

Sends a HTTP/1.1 102 Processing message to the client, indicating that
the request body should be sent.

## Class: http.IncomingMessage
<!-- YAML
added: v0.1.17
Expand Down Expand Up @@ -1937,6 +1976,7 @@ not abort the request or do anything besides add a `timeout` event.
[`'checkContinue'`]: #http_event_checkcontinue
[`'request'`]: #http_event_request
[`'response'`]: #http_event_response
[`'upgrade'`]: #http_event_upgrade
[`Agent`]: #http_class_http_agent
[`Duplex`]: stream.html#stream_class_stream_duplex
[`EventEmitter`]: events.html#events_class_eventemitter
Expand Down
29 changes: 22 additions & 7 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,16 +447,25 @@ function socketOnData(d) {
socket.destroy();
}
} else if (parser.incoming && parser.incoming.complete &&
// When the status code is 100 (Continue), the server will
// send a final response after this client sends a request
// body. So, we must not free the parser.
parser.incoming.statusCode !== 100) {
// When the status code is informational (100, 102-199),
// the server will send a final response after this client
// sends a request body, so we must not free the parser.
// 101 (Switching Protocols) and all other status codes
// should be processed normally.
!statusIsInformational(parser.incoming.statusCode)) {
socket.removeListener('data', socketOnData);
socket.removeListener('end', socketOnEnd);
freeParser(parser, req, socket);
}
}

function statusIsInformational(status) {
// 100 (Continue) RFC7231 Section 6.2.1
// 102 (Processing) RFC2518
// 103 (Early Hints) RFC8297
// 104-199 (Unassigned)
return (status < 200 && status >= 100 && status !== 101);
}

// client
function parserOnIncomingClient(res, shouldKeepAlive) {
Expand All @@ -480,10 +489,16 @@ function parserOnIncomingClient(res, shouldKeepAlive) {
return 2; // Skip body and treat as Upgrade.
}

if (res.statusCode === 100) {
// restart the parser, as this is a continue message.
if (statusIsInformational(res.statusCode)) {
// Restart the parser, as this is a 1xx informational message.
req.res = null; // Clear res so that we don't hit double-responses.
req.emit('continue');
// Maintain compatibility by sending 100-specific events
if (res.statusCode === 100) {
req.emit('continue');
}
// Send information events to all 1xx responses except 101 Upgrade.
req.emit('information', { statusCode: res.statusCode });

return 1; // Skip body but don't treat as Upgrade.
}

Expand Down
4 changes: 4 additions & 0 deletions lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ ServerResponse.prototype.writeContinue = function writeContinue(cb) {
this._sent100 = true;
};

ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
this._writeRaw(`HTTP/1.1 102 Processing${CRLF}${CRLF}`, 'ascii', cb);
};

ServerResponse.prototype._implicitHeader = function _implicitHeader() {
this.writeHead(this.statusCode);
};
Expand Down
52 changes: 52 additions & 0 deletions test/parallel/test-http-information-processing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';
require('../common');
const assert = require('assert');
const http = require('http');
const Countdown = require('../common/countdown');

const test_res_body = 'other stuff!\n';
const countdown = new Countdown(3, () => server.close());

const server = http.createServer((req, res) => {
console.error('Server sending informational message #1...');
res.writeProcessing();
console.error('Server sending informational message #2...');
res.writeProcessing();
console.error('Server sending full response...');
res.writeHead(200, {
'Content-Type': 'text/plain',
'ABCD': '1'
});
res.end(test_res_body);
});

server.listen(0, function() {
const req = http.request({
port: this.address().port,
path: '/world'
});
req.end();
console.error('Client sending request...');

let body = '';

req.on('information', function(res) {
console.error('Client got 102 Processing...');
countdown.dec();
});

req.on('response', function(res) {
assert.strictEqual(countdown.remaining, 1,
'Full response received before all 102 Processing');
assert.strictEqual(200, res.statusCode,
`Final status code was ${res.statusCode}, not 200.`);
res.setEncoding('utf8');
res.on('data', function(chunk) { body += chunk; });
res.on('end', function() {
console.error('Got full response.');
assert.strictEqual(body, test_res_body);
assert.ok('abcd' in res.headers);
countdown.dec();
});
});
});

0 comments on commit baf8495

Please sign in to comment.