iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
JavaScript

Learn HTTP With JS(2)系列 第 12

Node.js http 模組 Class 介紹:writeHead、flushHeaders 全解析

  • 分享至 

  • xImage
  •  

request 跟 response 的 Class 介紹

之前在 Node.js stream 入門 那篇文章有提到這些 Class 的關係,這邊再統整一次

http-client-server-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);

ClientRequestServerResponse

本文聚焦在 ClientRequest / ServerResponse 的「寫入」面;

IncomingMessage 的「讀取」(headers / body)算是更基礎的內容,這邊不特別展開

http-client-request-server-response

寫入流程 1:何時才會送出 header ? 了解 Node.js API 的設計

Node.js 提供了以下 methods 可以設定 headers

以下 methods 可以取得 headers

以下 properties 可以讀取 headers 狀態

Node.js 把整個 headers 的操作分成三個階段,我們可以用 git 的概念來類比

git Local Staging Local Commit (immutable) Actual Push
OutgoingMessage kOutHeaders (object) _headers (string) _send()

http-nodejs-headers-methods

測試 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 測試

  • Node.js 會輸出 ok
  • curl 會停在 * Request completely sent off,因為 server 還沒實際回傳 headers 跟 body

Wireshark 抓 Loopback: lo0,加上篩選 tcp.port == 5000,確認 server 真的沒有提前送 response headers

  • TCP 三方交握
  • client 傳送 HTTP Request, Server 回應收到 (TCP ACK)

wireshark-writehead

測試 flushHeaders

Node.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

寫入流程 2:送出 body

Node.js 提供了以下 methods 可以寫入 body

Content-LengthTransfer-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.

2-1: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
});

2-2: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

寫入流程 3:body 送完以後的生命週期

以下 properties 跟 events 可以得知 body 送完以後的生命週期

時間軸如下

end-to-prefinish-to-end-cb

寫個 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 之間的繼承關係
  • 送出 headers 的三個階段:kOutHeaders_headers_send()
  • writeHead / flushHeaders 如何提前送出 headers
  • headersSent 之後,get/set 系列 method 的行為差異(拋錯 vs 回傳空值)
  • Content-LengthTransfer-Encoding: chunked 分別對應「已知 / 未知 body 長度」的使用情境
  • body 送完後的生命週期:on("prefinish")on("finish")end callback

上一篇
Node.js http.Agent:TCP 連線池管理教學
下一篇
Node.js http.Server Graceful Shutdown 完整教學
系列文
Learn HTTP With JS(2)21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言