Skip to content
Open
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
62 changes: 62 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -2086,6 +2086,61 @@ emitted when the last segment of the response headers and body have been
handed off to the operating system for transmission over the network. It
does not imply that the client has received anything yet.

### Event: `'sendingHeaders'`

<!-- YAML
added: REPLACEME
-->

Emitted synchronously, exactly once, immediately before the status line and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe it will be useful to put a sentence as warning when trying to use a promise for a listener.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done, and I just added tests as well

response headers are serialized and sent to the client. The listener is called
with `this` bound to the response.

This is the moment at which the response is about to become committed: when the
event fires, [`response.headersSent`][] is still `false`, so a listener may make
final changes to the outgoing message. From inside the listener it is valid to:

* read headers with [`response.getHeader()`][] / [`response.getHeaders()`][];
* add, replace, or remove headers with [`response.setHeader()`][],
[`outgoingMessage.appendHeader()`][], and [`response.removeHeader()`][];
* change [`response.statusCode`][] and [`response.statusMessage`][].

The event is emitted regardless of how the headers are flushed: an explicit call
to [`response.writeHead()`][], implicit headers triggered by the first
[`response.write()`][] or [`response.end()`][], or [`response.flushHeaders()`][].
Because it is a single emission point, multiple independent listeners (for
example logging, session, and content-negotiation middleware) can each register
without coordinating with one another. Listeners run in registration order.

The listener runs synchronously and must not be an `async` function: work
deferred past an `await` runs after the headers are already sent, so only
changes made synchronously take effect.

```js
const server = http.createServer((req, res) => {
res.on('sendingHeaders', function() {
// Set a header at the last possible moment, based on final state.
this.setHeader('X-Response-Time', `${Date.now() - req.startTime}ms`);
if (!this.getHeader('Content-Type')) {
this.setHeader('Content-Type', 'text/plain');
}
});

res.end('hello');
});
```

A header passed inline to `response.writeHead()` is visible to the listener and
can be modified there, including the array form:

```js
res.on('sendingHeaders', function() {
// 'X-Inline' was passed to writeHead() below; it can still be removed here.
this.removeHeader('X-Inline');
});
res.writeHead(200, ['X-Inline', 'value', 'Content-Type', 'text/plain']);
```

### `response.addTrailers(headers)`

<!-- YAML
Expand Down Expand Up @@ -4741,6 +4796,7 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
[`net.Socket`]: net.md#class-netsocket
[`net.createConnection()`]: net.md#netcreateconnectionoptions-connectlistener
[`new URL()`]: url.md#new-urlinput-base
[`outgoingMessage.appendHeader()`]: #outgoingmessageappendheadername-value
[`outgoingMessage.setHeader(name, value)`]: #outgoingmessagesetheadername-value
[`outgoingMessage.setHeaders()`]: #outgoingmessagesetheadersheaders
[`outgoingMessage.socket`]: #outgoingmessagesocket
Expand All @@ -4758,9 +4814,15 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
[`request.writableFinished`]: #requestwritablefinished
[`request.write(data, encoding)`]: #requestwritechunk-encoding-callback
[`response.end()`]: #responseenddata-encoding-callback
[`response.flushHeaders()`]: #responseflushheaders
[`response.getHeader()`]: #responsegetheadername
[`response.getHeaders()`]: #responsegetheaders
[`response.headersSent`]: #responseheaderssent
[`response.removeHeader()`]: #responseremoveheadername
[`response.setHeader()`]: #responsesetheadername-value
[`response.socket`]: #responsesocket
[`response.statusCode`]: #responsestatuscode
[`response.statusMessage`]: #responsestatusmessage
[`response.strictContentLength`]: #responsestrictcontentlength
[`response.writableEnded`]: #responsewritableended
[`response.writableFinished`]: #responsewritablefinished
Expand Down
26 changes: 25 additions & 1 deletion lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,14 @@ function writeHead(statusCode, reason, obj) {
}
this.statusCode = statusCode;

// When a pre-serialization headers listener is registered we always
// materialize the header collection (kOutHeaders), including headers passed
// inline to writeHead(), so the listener gets a single, unified, mutable
// view of every outgoing header.
const hasHeadersListeners = this.listenerCount('sendingHeaders') > 0;

let headers;
if (this[kOutHeaders]) {
if (this[kOutHeaders] || hasHeadersListeners) {
// Slow-case: when progressive API and header fields are passed.
let k;
if (ArrayIsArray(obj)) {
Expand Down Expand Up @@ -467,6 +473,24 @@ function writeHead(statusCode, reason, obj) {
headers = obj;
}

// Fire the pre-serialization hook. Listeners run synchronously with `this`
// bound to the response, immediately before the status line and header block
// are serialized to the wire. They may still mutate headers via
// setHeader()/appendHeader()/removeHeader() and change statusCode/
// statusMessage. It is a single choke point reached by every code path
// (explicit writeHead(), implicit headers from write()/end(), and
// flushHeaders()).
if (hasHeadersListeners) {
this.emit('sendingHeaders');
// Re-read state a listener may have changed. Assigning `res.statusCode`
// does not validate, so re-check the range after the listener ran.
headers = this[kOutHeaders];
statusCode = this.statusCode | 0;
if (statusCode < 100 || statusCode > 999) {
throw new ERR_HTTP_INVALID_STATUS_CODE(statusCode);
}
}

if (checkInvalidHeaderChar(this.statusMessage))
throw new ERR_INVALID_CHAR('statusMessage');

Expand Down
133 changes: 133 additions & 0 deletions test/parallel/test-http-sending-headers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const http = require('http');

// 1) Fires once, before serialization, for an implicit-header response (end()).
{
const server = http.createServer(common.mustCall((req, res) => {
let fired = 0;
res.on('sendingHeaders', common.mustCall(function() {
fired++;
// Headers not yet sent, still mutable.
assert.strictEqual(this.headersSent, false);
this.setHeader('X-Added-In-Hook', 'yes');
}));
res.end('body');
// Synchronous: hook has already run by the time end() returns.
assert.strictEqual(fired, 1);
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.headers['x-added-in-hook'], 'yes');
res.resume().on('end', common.mustCall(() => server.close()));
}));
}));
}

// 2) Listener may change the status code, and it is reflected on the wire.
{
const server = http.createServer(common.mustCall((req, res) => {
res.on('sendingHeaders', common.mustCall(function() {
this.statusCode = 503;
this.statusMessage = 'Service Unavailable';
}));
res.writeHead(200);
res.end();
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 503);
res.resume().on('end', common.mustCall(() => server.close()));
}));
}));
}

// 3) Listener sees and can remove headers passed INLINE to writeHead(),
// including the array form.
{
const server = http.createServer(common.mustCall((req, res) => {
res.on('sendingHeaders', common.mustCall(function() {
assert.strictEqual(this.getHeader('X-Inline'), 'a');
this.removeHeader('X-Inline');
this.setHeader('X-From-Hook', 'b');
}));
res.writeHead(200, ['X-Inline', 'a', 'Content-Type', 'text/plain']);
res.end('ok');
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.headers['x-inline'], undefined);
assert.strictEqual(res.headers['x-from-hook'], 'b');
assert.strictEqual(res.headers['content-type'], 'text/plain');
res.resume().on('end', common.mustCall(() => server.close()));
}));
}));
}

// 4) Multiple listeners all run (FIFO, EventEmitter semantics).
{
const order = [];
const server = http.createServer(common.mustCall((req, res) => {
res.on('sendingHeaders', common.mustCall(() => order.push(1)));
res.on('sendingHeaders', common.mustCall(() => order.push(2)));
res.end();
assert.deepStrictEqual(order, [1, 2]);
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
res.resume().on('end', common.mustCall(() => server.close()));
}));
}));
}

// 5) flushHeaders() also triggers the hook exactly once.
{
const server = http.createServer(common.mustCall((req, res) => {
res.on('sendingHeaders', common.mustCall(function() {
this.setHeader('X-Flushed', '1');
}));
res.flushHeaders();
res.end();
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.headers['x-flushed'], '1');
res.resume().on('end', common.mustCall(() => server.close()));
}));
}));
}

// 6) The hook is synchronous: only changes made before an `await` take effect.
// Work deferred past `await` runs after the headers are sent, so a later
// setHeader() throws and a statusCode change is not reflected on the wire.
{
const server = http.createServer(common.mustCall((req, res) => {
res.on('sendingHeaders', common.mustCall(async function() {
this.setHeader('X-Sync', 'before');
await null;
assert.strictEqual(this.headersSent, true);
this.statusCode = 503; // No effect: headers already sent.
assert.throws(() => this.setHeader('X-Async', 'after'), {
code: 'ERR_HTTP_HEADERS_SENT',
});
server.close();
}));
res.end('hello');
}));

server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers['x-sync'], 'before');
assert.strictEqual(res.headers['x-async'], undefined);
res.resume();
}));
}));
}
Loading