Skip to content

Expose and write headers on 1xx intermediate status codes #27921

@awwright

Description

@awwright

Background: I'm currently working on a document for standardizing how servers may communicate intermediate status of a long-running operation over HTTP (operations running minutes to days). Part of this involves use of 1xx status codes, available in HTTP/1.1 and HTTP/2.

HTTP specifies that a request may have multiple 1xx responses before a final 2xx-5xx response. These responses, like any other, may have headers:101 Switching Protocols, 102 Processing, and 103 Early Hints are all known to use headers to convey additional information about the intermediate status. (For example, 101 uses Upgrade, 102 uses Status-URI, and 103 uses Link).

There is currently seemingly no way to write or read these headers. Given the definition of 1xx (which is mandatory in HTTP/1.1 and HTTP/2), I would expect to be able to call ServerResponse#writeHead multiple times with a 1xx status code, however, this does not flush the headers to the response, and appears to cause an error to be thrown once the final headers are written! Node.js added ServerResponse#writeProcessing in v10.0.0, however, this is specific to a single status code, does not support headers, and is not forward compatible with future status codes.

I would also expect the ClientRequest#on("information") event to include a headers object, so I can read this data.

The only known workarounds are to use ServerResponse#_writeRaw which is not a public API; and the only available option for clients is to parse responses manually.


Here is a script demonstrating the expected behavior:

const http = require('http');
const server = http.createServer(handleRequest);
server.listen(0);
console.error('Listening on port '+server.address().port);

function handleRequest(req, res){
    var tasks = [
        'Herding cats',
        'Digging holes',
        'Filling in holes',
        'Making tea',
    ];
    function writeProgress(i){
        console.error('writeProgress('+i+')');
        if(false){
            res.writeHead(102, 'Processing', {
                'Progress': `${i}/${tasks.length} (${tasks[i]})`,
            });
        }else{
            res._writeRaw('HTTP/1.1 102 Processing\r\n');
            res._writeRaw('Progress: '+i+'/'+tasks.length+' ('+tasks[i]+')\r\n');
            res._writeRaw('\r\n');
        }
    }
    function next(){
        if(++i===tasks.length){
            res.setHeader('Content-Type', 'text/plain');
            res.end('All done!\r\n');
        }else{
            writeProgress(i);
            setTimeout(next, 100);
        }
    }
    var i = 0;
    writeProgress(i);
    setTimeout(next, 100);
}

var req = http.request({
    host: server.address().address,
    port: server.address().port,
    path: '/',
});
req.end();
req.on('information', function(res){
    console.log(res);
});
req.on('response', function(res){
    res.pipe(process.stdout);
    res.on('end', console.error);
});

When _writeRaw is used, the Node.js client sees only the statusCode:

$ node -v
v12.1.0
$ node demo.js 
Listening on port 49213
writeProgress(0)
{ statusCode: 102 }
writeProgress(1)
{ statusCode: 102 }
writeProgress(2)
{ statusCode: 102 }
writeProgress(3)
{ statusCode: 102 }
All done!

When _writeRaw is used, curl is able to parse the response as expected:

$ curl -v http://localhost:49213/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 49213 (#0)
> GET / HTTP/1.1
> Host: localhost:49213
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 102 Processing
< Progress: 0/4 (Herding cats)
< HTTP/1.1 102 Processing
< Progress: 1/4 (Digging holes)
< HTTP/1.1 102 Processing
< Progress: 2/4 (Filling in holes)
< HTTP/1.1 102 Processing
< Progress: 3/4 (Making tea)
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sun, 26 May 2019 21:02:58 GMT
< Connection: keep-alive
< Content-Length: 11
< 
All done!
* Connection #0 to host localhost left intact

However, when writeHead is used, the data never makes it out of the socket, and an error is emitted towards the end:

$ node -v
v12.1.0
$ node demo.js 
Listening on port 49224
writeProgress(0)
writeProgress(1)
writeProgress(2)
writeProgress(3)
_http_outgoing.js:467
    throw new ERR_HTTP_HEADERS_SENT('set');
    ^

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ServerResponse.setHeader (_http_outgoing.js:467:11)
    at Timeout.next [as _onTimeout] (.../demo.js:28:17)
    at listOnTimeout (internal/timers.js:531:17)
    at processTimers (internal/timers.js:475:7)

$ curl -v http://localhost:49226/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 49226 (#0)
> GET / HTTP/1.1
> Host: localhost:49226
> User-Agent: curl/7.54.0
> Accept: */*
> 
* Empty reply from server
* Connection #0 to host localhost left intact
curl: (52) Empty reply from server

In summary:

  • ClientRequest#on("information") should expose a res-like object with headers, and
  • ServerResponse#writeHead should allow multiple calls with a 1xx status code that is immediately flushed to the socket.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestIssues that request new features to be added to Node.js.httpIssues or PRs related to the http subsystem.stale

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions