ClientRequest 跟 ServerResponse 都繼承它之前在 Node.js stream 入門 那篇文章有提到這些 Class 的關係,這邊再統整一次
client side code
import http from "http";
// ✅ ClientRequest
const clientRequest = http.get({
host: "example.com",
port: 80,
path: "/",
});
// ✅ IncomingMessage
clientRequest.on("response", (response: http.IncomingMessage) =>
response.resume(),
);
server side code
import http from "http";
const server = http.createServer();
// ✅ IncomingMessage & ServerResponse
server.on("request", (req: http.IncomingMessage, res: http.ServerResponse) =>
res.end(),
);
server.listen(5000);
ClientRequest 跟 ServerResponse本文聚焦在 ClientRequest / ServerResponse 的「寫入」面;
IncomingMessage 的「讀取」(headers / body)算是更基礎的內容,這邊不特別展開
Node.js 提供了以下 methods 可以設定 headers
setHeader
setHeaders
appendHeader
flushHeaders
removeHeader
writeHead(這是 ServerResponse 獨有的 method)
以下 methods 可以取得 headers
getHeader
getHeaderNames
getHeaders
hasHeader
以下 properties 可以讀取 headers 狀態
headersSent
Node.js 把整個 headers 的操作分成三個階段,我們可以用 git 的概念來類比
| git | Local Staging | Local Commit (immutable) | Actual Push |
|---|---|---|---|
OutgoingMessage |
kOutHeaders (object) |
_headers (string) |
_send() |
writeHead使用者呼叫 writeHead 之後,kOutHeaders 會被清空,且後續的寫入會拋出同步錯誤
import http from "http";
import assert from "assert";
const server = http.createServer();
server.listen(5000);
server.on("request", (req, res) => {
res.writeHead(200, { a: "1", b: "2" });
assert(res.headersSent);
// ✅ Can't get header after headersSent
assert(res.getHeader("a") === undefined);
assert(Object.keys(res.getHeaders()).length === 0);
assert(res.getHeaderNames().length === 0);
assert(res.hasHeader("a") === false);
// ✅ Can't set header after headersSent
try {
res.setHeader("a", "1");
} catch (e) {
assert(e instanceof Error);
assert((e as any).code === "ERR_HTTP_HEADERS_SENT");
}
// ✅ Can't set header after headersSent
try {
res.setHeaders(new Headers({ a: "1" }));
} catch (e) {
assert(e instanceof Error);
assert((e as any).code === "ERR_HTTP_HEADERS_SENT");
}
// ✅ Can't remove header after headersSent
try {
res.removeHeader("a");
} catch (e) {
assert(e instanceof Error);
assert((e as any).code === "ERR_HTTP_HEADERS_SENT");
}
// ✅ If all assert is truthy, print ok
console.log("ok");
});
用 curl http://localhost:5000 -v 測試
ok
* Request completely sent off,因為 server 還沒實際回傳 headers 跟 body用 Wireshark 抓 Loopback: lo0,加上篩選 tcp.port == 5000,確認 server 真的沒有提前送 response headers

flushHeadersNode.js 的設計哲學是 "盡量把 headers 延遲到跟著 body 一起發送",從 flushHeaders() 的官方文件可以得知
For efficiency reason, Node.js normally buffers the message headers until outgoingMessage.end() is called or the first chunk of message data is written. It then tries to pack the headers and data into a single TCP packet.
It is usually desired (it saves a TCP round-trip), but not when the first data is not sent until possibly much later. outgoingMessage.flushHeaders() bypasses the optimization and kickstarts the message.
呼叫 flushHeaders 可以先送出 headers,通常會用在 "body 還在等待中,但 headers 已經決定好"
httpServer.on("request", (req, res) => {
// ✅ 目前都還在 kOutHeaders 這邊 get / set headers,尚未送出
res.setHeader("a", "1");
assert(res.getHeader("a") === "1");
assert(res.headersSent === false);
// ✅ 目前都還在 kOutHeaders 這邊 get / set headers,尚未送出
res.setHeaders(new Headers({ b: "2" }));
assert(res.hasHeader("b"));
assert(JSON.stringify(res.getHeaderNames()) === JSON.stringify(["a", "b"]));
assert(res.headersSent === false);
// ✅ 實際送出
res.flushHeaders();
assert(res.headersSent);
// ✅ Can't set header after headersSent
try {
res.setHeader("a", "1");
} catch (e) {
assert(e instanceof Error);
assert((e as any).code === "ERR_HTTP_HEADERS_SENT");
}
// ✅ Can't set header after headersSent
try {
res.setHeaders(new Headers({ a: "1" }));
} catch (e) {
assert(e instanceof Error);
assert((e as any).code === "ERR_HTTP_HEADERS_SENT");
}
// ✅ Can't remove header after headersSent
try {
res.removeHeader("a");
} catch (e) {
assert(e instanceof Error);
assert((e as any).code === "ERR_HTTP_HEADERS_SENT");
}
});
用 curl http://localhost:5000 -v 測試,確實有收到 response headers,但我們沒送 body,所以連線會 timeout
< HTTP/1.1 200 OK
< a: 1
< b: 2
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
* transfer closed with outstanding read data remaining
* Closing connection
curl: (18) transfer closed with outstanding read data remaining
Node.js 提供了以下 methods 可以寫入 body
Content-Length 跟 Transfer-Encoding 是 HTTP/1.1 定義 body 最重要的兩個 header,參考 RFC 9112 Section 6. Message Body
The presence of a message body in a request is signaled by a Content-Length or Transfer-Encoding header field.
Content-Length假設我要 serve 一個靜態網站,每個 HTML, CSS, JS 都是預先 build 好的檔案,這情況就屬於 "已知 body 長度"
httpServer.on("request", (req, res) => {
// ✅ Node.js 會自動設定 Content-Length = 寫入的 body byteLength
res.end(readFileSync(join(import.meta.dirname, "index.html")));
});
用 curl http://localhost:5000 -v 測試
< HTTP/1.1 200 OK
< Connection: keep-alive
< Keep-Alive: timeout=5
< Content-Length: 20
<
* Connection #0 to host localhost left intact
<h1>hello world</h1>
也可以顯式設定 Content-Length
httpServer.on("request", (req, res) => {
const fileBuffer = readFileSync(join(import.meta.dirname, "index.html"));
res.setHeader("Content-Length", fileBuffer.byteLength);
res.end(fileBuffer);
});
用 curl http://localhost:5000 -v 測試
< HTTP/1.1 200 OK
< Content-Length: 20
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
<h1>hello world</h1>
若檔案很大,則不建議用 readFileSync 把整個檔案讀進記憶體,可以使用 pipe 流式傳輸
httpServer.on("request", (req, res) => {
const path = join(import.meta.dirname, "demo-very-large-video.mp4");
// ✅ 先把 file size 設定到 Content-Length
const filestat = statSync(path);
res.setHeader("Content-Length", filestat.size);
// ✅ 流式傳輸,避免一次讀取大檔案,把記憶體撐爆
const readStream = createReadStream(path);
readStream.pipe(res);
// ❌ todo: res, readStream error handle
});
Transfer-Encoding: chunked現在很夯的 AI 工具在回應時,不會預先知道回應長度,這時候會使用 Transfer-Encoding: chunked
可參考我寫過的 SSE: Server-Sent Events
httpServer.on("request", (req, res) => {
// ✅ 呼叫 write 的當下,若 header 沒有明確指定 Content-Length
// ✅ 則 Node.js 會自動設定 Transfer-Encoding: chunked
res.write("first line");
res.write("second line");
res.end("third line");
});
用 curl http://localhost:5000 -v 測試
< HTTP/1.1 200 OK
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
first linesecond linethird line
以下 properties 跟 events 可以得知 body 送完以後的生命週期
writableEnded
on("prefinish")
on("finish")
writableFinished
時間軸如下
寫個 PoC 來測試
httpServer.on("request", (req, res) => {
res.on("prefinish", () => {
assert(res.writableEnded);
console.log("prefinish");
});
res.on("finish", () => {
assert(res.writableFinished);
console.log("finish");
});
res.end("123", () => console.log("end cb"));
});
// Prints
// prefinish
// finish
// end cb
outgoingMessage.on("prefinish") 其實是繼承 stream.Writable
不過 Node.js stream 官方文件 完全沒提到 prefinish,所以就當作一個小知識先記著就好~
在這篇文章,我們學到了
ClientRequest / ServerResponse / IncomingMessage / OutgoingMessage 之間的繼承關係kOutHeaders → _headers → _send()
writeHead / flushHeaders 如何提前送出 headersheadersSent 之後,get/set 系列 method 的行為差異(拋錯 vs 回傳空值)Content-Length 與 Transfer-Encoding: chunked 分別對應「已知 / 未知 body 長度」的使用情境on("prefinish") → on("finish") → end callback