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
64 changes: 64 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -2096,6 +2096,63 @@ 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
Comment thread
bjohansebas marked this conversation as resolved.
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) => {
const startTime = Date.now();

res.on('sendingHeaders', function() {
// Set a header at the last possible moment, based on final state.
this.setHeader('X-Response-Time', `${Date.now() - 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 @@ -4781,6 +4838,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 @@ -4798,9 +4856,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
23 changes: 23 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -4403,6 +4403,29 @@ does not imply that the client has received anything yet.

After this event, no more events will be emitted on the response object.

#### Event: `'sendingHeaders'`

<!-- YAML
added: REPLACEME
-->

Emitted synchronously, exactly once, immediately before the response headers are
serialized and the `HEADERS` frame is sent. The listener is called with `this`
bound to the response, while `response.headersSent` is still `false`, so it may
make final changes to the outgoing message: read or modify headers with
[`response.setHeader()`][], `response.appendHeader()`, and
`response.removeHeader()`, or change `response.statusCode`.

The event is reached by every header-flushing path
([`response.writeHead()`][], implicit headers from the first
[`response.write()`][] or [`response.end()`][], and `response.flushHeaders()`),
so independent middleware can each register a listener without coordinating.
Listeners run in registration order and must be synchronous; changes deferred
past an `await` run after the headers are already sent and have no effect.

This is the HTTP/2 counterpart of the HTTP/1
[`'sendingHeaders'`](http.md#event-sendingheaders) event.

#### `response.addTrailers(headers)`

<!-- YAML
Expand Down
41 changes: 38 additions & 3 deletions lib/_http_server.js
Comment thread
bjohansebas marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const onResponseFinishChannel = dc.channel('http.server.response.finish');
const kServerResponse = Symbol('ServerResponse');
const kServerResponseStatistics = Symbol('ServerResponseStatistics');
const kUpgradeStream = Symbol('UpgradeStream');
const kSendingHeadersEmitted = Symbol('SendingHeadersEmitted');

const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');

Expand Down Expand Up @@ -210,6 +211,8 @@ function ServerResponse(req, options) {
this.sendDate = true;
this._sent100 = false;
this._expect_continue = false;
// Guards the one-shot 'sendingHeaders' emit against re-entrancy.
this[kSendingHeadersEmitted] = false;

if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te);
Expand Down Expand Up @@ -419,18 +422,22 @@ function writeHead(statusCode, reason, obj) {
}


if (typeof reason === 'string') {
const hasExplicitReason = typeof reason === 'string';
if (hasExplicitReason) {
// writeHead(statusCode, reasonPhrase[, headers])
this.statusMessage = reason;
} else {
// writeHead(statusCode[, headers])
this.statusMessage ||= STATUS_CODES[statusCode] || 'unknown';
obj ??= reason;
}
this.statusCode = statusCode;

// With a 'sendingHeaders' listener, always materialize kOutHeaders (including
// inline writeHead() headers) so the listener sees one mutable header view.
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 +474,34 @@ function writeHead(statusCode, reason, obj) {
headers = obj;
}

// Pre-serialization hook, reached by every path (writeHead(), implicit
// headers, flushHeaders()): listeners run synchronously right before the
// status line and headers are serialized and may still mutate them.
if (hasHeadersListeners && !this[kSendingHeadersEmitted]) {
this[kSendingHeadersEmitted] = true;
this.emit('sendingHeaders');

// A listener that flushed the headers itself re-entered writeHead(); the
// guard stops a re-emit, but serializing again would double the header
// block, so fail like a second writeHead() call.
if (this._header) {
throw new ERR_HTTP_HEADERS_SENT('write');
}

// Re-read state; `res.statusCode` isn't validated on assignment.
headers = this[kOutHeaders];
statusCode = this.statusCode | 0;
if (statusCode < 100 || statusCode > 999) {
throw new ERR_HTTP_INVALID_STATUS_CODE(statusCode);
}
}

// Default the reason phrase from the final status code when none was set;
// runs on every path so a listener changing the code is reflected.
if (!hasExplicitReason && !this.statusMessage) {
this.statusMessage = STATUS_CODES[statusCode] || 'unknown';
}

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

Expand Down
15 changes: 15 additions & 0 deletions lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ class Http2ServerResponse extends Stream {
headRequest: false,
sendDate: true,
statusCode: HTTP_STATUS_OK,
sendingHeadersEmitted: false,
};
this[kHeaders] = { __proto__: null };
this[kTrailers] = { __proto__: null };
Expand Down Expand Up @@ -795,6 +796,20 @@ class Http2ServerResponse extends Stream {
}

state.statusCode = statusCode;

// Pre-serialization hook mirroring the HTTP/1 'sendingHeaders' event:
// listeners run synchronously right before the HEADERS frame is sent.
if (this.listenerCount('sendingHeaders') > 0 &&
!state.sendingHeadersEmitted) {
state.sendingHeadersEmitted = true;
this.emit('sendingHeaders');

// A listener that flushed the headers itself re-entered writeHead(); the
// guard stops a re-emit, but sending the HEADERS frame again is invalid.
if (this[kStream].headersSent)
throw new ERR_HTTP2_HEADERS_SENT();
}

this[kBeginSend]();

return this;
Expand Down
Loading
Loading