Skip to content

Commit 0cbb389

Browse files
pimterryaduh95
authored andcommitted
http: add writeInformation to send arbitrary 1xx status codes
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #63155 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
1 parent fe127a9 commit 0cbb389

7 files changed

Lines changed: 306 additions & 34 deletions

File tree

‎doc/api/http.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2696,6 +2696,35 @@ been transmitted are equal or not.
26962696
Attempting to set a header field name or value that contains invalid characters
26972697
will result in a [`TypeError`][] being thrown.
26982698

2699+
### `response.writeInformation(statusCode[, headers][, callback])`
2700+
2701+
<!-- YAML
2702+
added: REPLACEME
2703+
-->
2704+
2705+
*`statusCode` {number} An HTTP 1xx informational status code, between `100`
2706+
and `199` inclusive, excluding `101` (Switching Protocols) which is only
2707+
available through the [`'upgrade'`][] event.
2708+
*`headers` {Object|Array} An optional set of headers to send with the
2709+
informational response. Accepts the same shapes as
2710+
[`response.writeHead()`][].
2711+
*`callback` {Function} Optional, called once the message has been written
2712+
to the socket.
2713+
2714+
Sends an arbitrary HTTP/1.1 1xx informational response to the client. This
2715+
is a generic equivalent of [`response.writeContinue()`][],
2716+
[`response.writeProcessing()`][] and [`response.writeEarlyHints()`][], and
2717+
can be called multiple times before the final response. After the final
2718+
response headers have been sent (via [`response.writeHead()`][] or an
2719+
implicit header), calling this method throws `ERR_HTTP_HEADERS_SENT`.
2720+
2721+
Clients receive these responses via the [`'information'`][information event]
2722+
event on `http.ClientRequest`.
2723+
2724+
```js
2725+
response.writeInformation(110, { 'X-Progress':'50%' });
2726+
```
2727+
26992728
### `response.writeProcessing()`
27002729

27012730
<!-- YAML
@@ -4694,7 +4723,9 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
46944723
[`response.write()`]: #responsewritechunk-encoding-callback
46954724
[`response.write(data, encoding)`]: #responsewritechunk-encoding-callback
46964725
[`response.writeContinue()`]: #responsewritecontinue
4726+
[`response.writeEarlyHints()`]: #responsewriteearlyhintshints-callback
46974727
[`response.writeHead()`]: #responsewriteheadstatuscode-statusmessage-headers
4728+
[`response.writeProcessing()`]: #responsewriteprocessing
46984729
[`server.close()`]: #serverclosecallback
46994730
[`server.headersTimeout`]: #serverheaderstimeout
47004731
[`server.keepAliveTimeoutBuffer`]: #serverkeepalivetimeoutbuffer
@@ -4715,4 +4746,5 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
47154746
[`writable.destroyed`]:stream.md#writabledestroyed
47164747
[`writable.uncork()`]:stream.md#writableuncork
47174748
[`writable.write()`]:stream.md#writablewritechunk-encoding-callback
4749+
[information event]: #event-information
47184750
[initial delay]:net.md#socketsetkeepaliveenable-initialdelay

‎doc/api/http2.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4832,6 +4832,30 @@ response.writeEarlyHints({
48324832
});
48334833
```
48344834

4835+
#### `response.writeInformation(statusCode[, headers])`
4836+
4837+
<!-- YAML
4838+
added: REPLACEME
4839+
-->
4840+
4841+
*`statusCode` {number} An HTTP 1xx informational status code, between `100`
4842+
and `199` inclusive, excluding `101` (Switching Protocols) which is not
4843+
allowed in HTTP/2.
4844+
*`headers` {Object} An optional object of headers to send with the
4845+
informational response.
4846+
4847+
Sends an arbitrary HTTP 1xx informational response, equivalent in HTTP/2 to a
4848+
`HEADERS` frame whose `:status` pseudo-header is a 1xx code. May be called
4849+
multiple times before the final response. After the final response headers
4850+
have been sent, this method is a no-op and returns `false`.
4851+
4852+
This is the generic equivalent of [`response.writeContinue()`][] and
4853+
[`response.writeEarlyHints()`][].
4854+
4855+
```js
4856+
response.writeInformation(110, { 'X-Progress':'50%' });
4857+
```
4858+
48354859
#### `response.writeHead(statusCode[, statusMessage][, headers])`
48364860

48374861
<!-- YAML
@@ -5041,6 +5065,7 @@ you need to implement any fall-back behavior yourself.
50415065
[`response.write()`]: #responsewritechunk-encoding-callback
50425066
[`response.write(data, encoding)`]: http.md#responsewritechunk-encoding-callback
50435067
[`response.writeContinue()`]: #responsewritecontinue
5068+
[`response.writeEarlyHints()`]: #responsewriteearlyhintshints
50445069
[`response.writeHead()`]: #responsewriteheadstatuscode-statusmessage-headers
50455070
[`server.close()`]: #serverclosecallback
50465071
[`server.maxHeadersCount`]: http.md#servermaxheaderscount

‎lib/_http_server.js‎

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -311,18 +311,66 @@ ServerResponse.prototype.detachSocket = function detachSocket(socket) {
311311
this.socket=null;
312312
};
313313

314+
ServerResponse.prototype.writeInformation=functionwriteInformation(
315+
statusCode,headers,cb){
316+
if(this._header){
317+
thrownewERR_HTTP_HEADERS_SENT('write');
318+
}
319+
320+
validateInteger(statusCode,'statusCode',100,199);
321+
if(statusCode===101){
322+
thrownewERR_HTTP_INVALID_STATUS_CODE(statusCode);
323+
}
324+
325+
conststatusMessage=STATUS_CODES[statusCode]||'unknown';
326+
lethead=`HTTP/1.1 ${statusCode}${statusMessage}\r\n`;
327+
328+
if(headers!==undefined&&headers!==null){
329+
if(ArrayIsArray(headers)){
330+
if(headers.length&&ArrayIsArray(headers[0])){
331+
for(leti=0;i<headers.length;i++){
332+
constentry=headers[i];
333+
head+=processInformationHeader(entry[0],entry[1]);
334+
}
335+
}else{
336+
if(headers.length%2!==0){
337+
thrownewERR_INVALID_ARG_VALUE('headers',headers);
338+
}
339+
for(leti=0;i<headers.length;i+=2){
340+
head+=processInformationHeader(headers[i],headers[i+1]);
341+
}
342+
}
343+
}else{
344+
validateObject(headers,'headers');
345+
constkeys=ObjectKeys(headers);
346+
for(leti=0;i<keys.length;i++){
347+
constkey=keys[i];
348+
head+=processInformationHeader(key,headers[key]);
349+
}
350+
}
351+
}
352+
353+
head+='\r\n';
354+
355+
returnthis._writeRaw(head,'ascii',cb);
356+
};
357+
358+
functionprocessInformationHeader(name,value){
359+
validateHeaderName(name);
360+
validateHeaderValue(name,value);
361+
return`${name}: ${value}\r\n`;
362+
}
363+
314364
ServerResponse.prototype.writeContinue=functionwriteContinue(cb){
315-
this._writeRaw('HTTP/1.1 100 Continue\r\n\r\n','ascii',cb);
365+
this.writeInformation(100,null,cb);
316366
this._sent100=true;
317367
};
318368

319369
ServerResponse.prototype.writeProcessing=functionwriteProcessing(cb){
320-
this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n','ascii',cb);
370+
this.writeInformation(102,null,cb);
321371
};
322372

323373
ServerResponse.prototype.writeEarlyHints=functionwriteEarlyHints(hints,cb){
324-
lethead='HTTP/1.1 103 Early Hints\r\n';
325-
326374
validateObject(hints,'hints');
327375

328376
if(hints.link===null||hints.link===undefined){
@@ -339,22 +387,16 @@ ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) {
339387
thrownewERR_INVALID_CHAR('header content','Link');
340388
}
341389

342-
head+='Link: '+link+'\r\n';
343-
390+
constheaders={__proto__: null,Link: link};
344391
constkeys=ObjectKeys(hints);
345392
for(leti=0;i<keys.length;i++){
346393
constkey=keys[i];
347394
if(key!=='link'){
348-
validateHeaderName(key);
349-
constvalue=hints[key];
350-
validateHeaderValue(key,value);
351-
head+=key+': '+value+'\r\n';
395+
headers[key]=hints[key];
352396
}
353397
}
354398

355-
head+='\r\n';
356-
357-
this._writeRaw(head,'ascii',cb);
399+
this.writeInformation(103,headers,cb);
358400
};
359401

360402
ServerResponse.prototype._implicitHeader=function_implicitHeader(){

‎lib/internal/http2/compat.js‎

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -901,17 +901,39 @@ class Http2ServerResponse extends Stream {
901901
this[kStream].respond(headers,options);
902902
}
903903

904-
// TODO doesn't support callbacks
905-
writeContinue(){
904+
writeInformation(statusCode,headers){
905+
if(typeofstatusCode!=='number'||
906+
statusCode<100||statusCode>199){
907+
thrownewERR_HTTP2_STATUS_INVALID(statusCode);
908+
}
909+
if(statusCode===101){
910+
thrownewERR_HTTP2_STATUS_INVALID(statusCode);
911+
}
912+
906913
conststream=this[kStream];
914+
907915
if(stream.headersSent||this[kState].closed)
908916
returnfalse;
909-
stream.additionalHeaders({
910-
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE,
911-
});
917+
918+
constoutHeaders={__proto__: null};
919+
if(headers!==undefined&&headers!==null){
920+
validateObject(headers,'headers');
921+
constkeys=ObjectKeys(headers);
922+
for(leti=0;i<keys.length;i++){
923+
outHeaders[keys[i]]=headers[keys[i]];
924+
}
925+
}
926+
outHeaders[HTTP2_HEADER_STATUS]=statusCode;
927+
928+
stream.additionalHeaders(outHeaders);
912929
returntrue;
913930
}
914931

932+
// TODO doesn't support callbacks
933+
writeContinue(){
934+
returnthis.writeInformation(HTTP_STATUS_CONTINUE);
935+
}
936+
915937
writeEarlyHints(hints){
916938
validateObject(hints,'hints');
917939

@@ -933,18 +955,9 @@ class Http2ServerResponse extends Stream {
933955
returnfalse;
934956
}
935957

936-
conststream=this[kStream];
958+
headers.Link=linkHeaderValue;
937959

938-
if(stream.headersSent||this[kState].closed)
939-
returnfalse;
940-
941-
stream.additionalHeaders({
942-
...headers,
943-
[HTTP2_HEADER_STATUS]: HTTP_STATUS_EARLY_HINTS,
944-
'Link': linkHeaderValue,
945-
});
946-
947-
returntrue;
960+
returnthis.writeInformation(HTTP_STATUS_EARLY_HINTS,headers);
948961
}
949962
}
950963

‎test/parallel/test-http-information-headers.js‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@ const countdown = new Countdown(2, () => server.close());
99

1010
constserver=http.createServer(common.mustCallAtLeast((req,res)=>{
1111
console.error('Server sending informational message #1...');
12-
// These function calls may rewritten as necessary
13-
// to call res.writeHead instead
14-
res._writeRaw('HTTP/1.1 102 Processing\r\n');
15-
res._writeRaw('Foo: Bar\r\n');
16-
res._writeRaw('\r\n',common.mustCall());
12+
res.writeInformation(102,{Foo: 'Bar'},common.mustCall());
1713
console.error('Server sending full response...');
1814
res.writeHead(200,{
1915
'Content-Type': 'text/plain',
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict';
2+
constcommon=require('../common');
3+
constassert=require('node:assert');
4+
consthttp=require('node:http');
5+
6+
// Happy flow: arbitrary 1xx status with custom headers, observed by the
7+
// client's 'information' event.
8+
{
9+
constserver=http.createServer(common.mustCall((req,res)=>{
10+
res.writeInformation(110,{'X-Progress': '50%','X-Stage': 'reading'});
11+
res.writeInformation(199,[['X-Custom','one'],['X-Custom-2','two']]);
12+
res.end('done');
13+
}));
14+
15+
server.listen(0,common.mustCall(()=>{
16+
constreq=http.request({port: server.address().port});
17+
18+
constseen=[];
19+
req.on('information',(res)=>{
20+
seen.push({
21+
statusCode: res.statusCode,
22+
headers: res.headers,
23+
});
24+
});
25+
26+
req.on('response',common.mustCall((res)=>{
27+
assert.strictEqual(res.statusCode,200);
28+
29+
assert.strictEqual(seen.length,2);
30+
assert.strictEqual(seen[0].statusCode,110);
31+
assert.strictEqual(seen[0].headers['x-progress'],'50%');
32+
assert.strictEqual(seen[0].headers['x-stage'],'reading');
33+
assert.strictEqual(seen[1].statusCode,199);
34+
assert.strictEqual(seen[1].headers['x-custom'],'one');
35+
assert.strictEqual(seen[1].headers['x-custom-2'],'two');
36+
37+
res.resume();
38+
res.on('end',common.mustCall(()=>server.close()));
39+
}));
40+
41+
req.end();
42+
}));
43+
}
44+
45+
// Headers argument is optional / nullable.
46+
{
47+
constserver=http.createServer(common.mustCall((req,res)=>{
48+
res.writeInformation(150);
49+
res.writeInformation(151,null);
50+
res.end();
51+
}));
52+
53+
server.listen(0,common.mustCall(()=>{
54+
constreq=http.request({port: server.address().port});
55+
letcount=0;
56+
req.on('information',()=>count++);
57+
req.on('response',common.mustCall((res)=>{
58+
assert.strictEqual(count,2);
59+
res.resume();
60+
res.on('end',common.mustCall(()=>server.close()));
61+
}));
62+
req.end();
63+
}));
64+
}
65+
66+
// Error cases.
67+
{
68+
constserver=http.createServer(common.mustCall((req,res)=>{
69+
assert.throws(()=>res.writeInformation(101),
70+
{code: 'ERR_HTTP_INVALID_STATUS_CODE'});
71+
assert.throws(()=>res.writeInformation(99),
72+
{code: 'ERR_OUT_OF_RANGE'});
73+
assert.throws(()=>res.writeInformation(200),
74+
{code: 'ERR_OUT_OF_RANGE'});
75+
assert.throws(()=>res.writeInformation('100'),
76+
{code: 'ERR_INVALID_ARG_TYPE'});
77+
assert.throws(()=>res.writeInformation(150,{'X-Bad\n': 'v'}),
78+
{code: 'ERR_INVALID_HTTP_TOKEN'});
79+
80+
res.writeHead(200);
81+
assert.throws(()=>res.writeInformation(150),
82+
{code: 'ERR_HTTP_HEADERS_SENT'});
83+
res.end();
84+
}));
85+
86+
server.listen(0,common.mustCall(()=>{
87+
constreq=http.request({port: server.address().port});
88+
req.on('response',common.mustCall((res)=>{
89+
res.resume();
90+
res.on('end',common.mustCall(()=>server.close()));
91+
}));
92+
req.end();
93+
}));
94+
}

0 commit comments

Comments
 (0)