Skip to content

Commit 49eb679

Browse files
Miles ElamMayaLekova
Miles Elam
authored andcommitted
http: process 100, 102-199 according to specs.
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: nodejs#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]>
1 parent 5c1e7ce commit 49eb679

File tree

4 files changed

+118
-7
lines changed

4 files changed

+118
-7
lines changed

doc/api/http.md

+40
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,37 @@ Emitted when the server sends a '100 Continue' HTTP response, usually because
400400
the request contained 'Expect: 100-continue'. This is an instruction that
401401
the client should send the request body.
402402

403+
### Event: 'information'
404+
<!-- YAML
405+
added: REPLACEME
406+
-->
407+
408+
Emitted when the server sends a 1xx response (excluding 101 Upgrade). This
409+
event is emitted with a callback containing an object with a status code.
410+
411+
```js
412+
const http = require('http');
413+
414+
const options = {
415+
hostname: '127.0.0.1',
416+
port: 8080,
417+
path: '/length_request'
418+
};
419+
420+
// Make a request
421+
const req = http.request(options);
422+
req.end();
423+
424+
req.on('information', (res) => {
425+
console.log('got information prior to main response: ' + res.statusCode);
426+
});
427+
```
428+
429+
101 Upgrade statuses do not fire this event due to their break from the
430+
traditional HTTP request/response chain, such as web sockets, in-place TLS
431+
upgrades, or HTTP 2.0. To be notified of 101 Upgrade notices, listen for the
432+
[`'upgrade'`][] event instead.
433+
403434
### Event: 'response'
404435
<!-- YAML
405436
added: v0.1.0
@@ -1384,6 +1415,14 @@ which has been transmitted are equal or not.
13841415
Attempting to set a header field name or value that contains invalid characters
13851416
will result in a [`TypeError`][] being thrown.
13861417

1418+
### response.writeProcessing()
1419+
<!-- YAML
1420+
added: REPLACEME
1421+
-->
1422+
1423+
Sends a HTTP/1.1 102 Processing message to the client, indicating that
1424+
the request body should be sent.
1425+
13871426
## Class: http.IncomingMessage
13881427
<!-- YAML
13891428
added: v0.1.17
@@ -1937,6 +1976,7 @@ not abort the request or do anything besides add a `timeout` event.
19371976
[`'checkContinue'`]: #http_event_checkcontinue
19381977
[`'request'`]: #http_event_request
19391978
[`'response'`]: #http_event_response
1979+
[`'upgrade'`]: #http_event_upgrade
19401980
[`Agent`]: #http_class_http_agent
19411981
[`Duplex`]: stream.html#stream_class_stream_duplex
19421982
[`EventEmitter`]: events.html#events_class_eventemitter

lib/_http_client.js

+22-7
Original file line numberDiff line numberDiff line change
@@ -447,16 +447,25 @@ function socketOnData(d) {
447447
socket.destroy();
448448
}
449449
} else if (parser.incoming && parser.incoming.complete &&
450-
// When the status code is 100 (Continue), the server will
451-
// send a final response after this client sends a request
452-
// body. So, we must not free the parser.
453-
parser.incoming.statusCode !== 100) {
450+
// When the status code is informational (100, 102-199),
451+
// the server will send a final response after this client
452+
// sends a request body, so we must not free the parser.
453+
// 101 (Switching Protocols) and all other status codes
454+
// should be processed normally.
455+
!statusIsInformational(parser.incoming.statusCode)) {
454456
socket.removeListener('data', socketOnData);
455457
socket.removeListener('end', socketOnEnd);
456458
freeParser(parser, req, socket);
457459
}
458460
}
459461

462+
function statusIsInformational(status) {
463+
// 100 (Continue) RFC7231 Section 6.2.1
464+
// 102 (Processing) RFC2518
465+
// 103 (Early Hints) RFC8297
466+
// 104-199 (Unassigned)
467+
return (status < 200 && status >= 100 && status !== 101);
468+
}
460469

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

483-
if (res.statusCode === 100) {
484-
// restart the parser, as this is a continue message.
492+
if (statusIsInformational(res.statusCode)) {
493+
// Restart the parser, as this is a 1xx informational message.
485494
req.res = null; // Clear res so that we don't hit double-responses.
486-
req.emit('continue');
495+
// Maintain compatibility by sending 100-specific events
496+
if (res.statusCode === 100) {
497+
req.emit('continue');
498+
}
499+
// Send information events to all 1xx responses except 101 Upgrade.
500+
req.emit('information', { statusCode: res.statusCode });
501+
487502
return 1; // Skip body but don't treat as Upgrade.
488503
}
489504

lib/_http_server.js

+4
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ ServerResponse.prototype.writeContinue = function writeContinue(cb) {
188188
this._sent100 = true;
189189
};
190190

191+
ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
192+
this._writeRaw(`HTTP/1.1 102 Processing${CRLF}${CRLF}`, 'ascii', cb);
193+
};
194+
191195
ServerResponse.prototype._implicitHeader = function _implicitHeader() {
192196
this.writeHead(this.statusCode);
193197
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
const Countdown = require('../common/countdown');
6+
7+
const test_res_body = 'other stuff!\n';
8+
const countdown = new Countdown(3, () => server.close());
9+
10+
const server = http.createServer((req, res) => {
11+
console.error('Server sending informational message #1...');
12+
res.writeProcessing();
13+
console.error('Server sending informational message #2...');
14+
res.writeProcessing();
15+
console.error('Server sending full response...');
16+
res.writeHead(200, {
17+
'Content-Type': 'text/plain',
18+
'ABCD': '1'
19+
});
20+
res.end(test_res_body);
21+
});
22+
23+
server.listen(0, function() {
24+
const req = http.request({
25+
port: this.address().port,
26+
path: '/world'
27+
});
28+
req.end();
29+
console.error('Client sending request...');
30+
31+
let body = '';
32+
33+
req.on('information', function(res) {
34+
console.error('Client got 102 Processing...');
35+
countdown.dec();
36+
});
37+
38+
req.on('response', function(res) {
39+
assert.strictEqual(countdown.remaining, 1,
40+
'Full response received before all 102 Processing');
41+
assert.strictEqual(200, res.statusCode,
42+
`Final status code was ${res.statusCode}, not 200.`);
43+
res.setEncoding('utf8');
44+
res.on('data', function(chunk) { body += chunk; });
45+
res.on('end', function() {
46+
console.error('Got full response.');
47+
assert.strictEqual(body, test_res_body);
48+
assert.ok('abcd' in res.headers);
49+
countdown.dec();
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)