iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
JavaScript

Learn HTTP With JS(2)系列 第 20

Node.js http 冷知識:1xx、Upgrade、clientError 事件解析

  • 分享至 

  • xImage
  •  

前言

這是 2026 iThome 鐵人賽的第 20 篇文章,同時也是 Node.js EventEmitterstreamnet.Socket 一路到 http 模組,這系列文章的最後一篇。這篇文章會把 http 模組一些比較冷門的 methods 跟 events 統整起來

request.on("information")

會在 ClientRequest 收到 1xx status code(除了 101 Switching Protocols)的時候觸發,通常包含

server

import net from "net";

const server = net.createServer((socket) =>
  socket.on("data", () => {
    socket.write("HTTP/1.1 199 WhatTheHack\r\nFoo: bar\r\n\r\n");
  }),
);
server.listen(5000);

client

import http from "http";

const clientRequest = http.request({ host: "localhost", port: 5000 });
clientRequest.end();
clientRequest.on("information", console.log);

output

{
  statusCode: 199,
  statusMessage: 'WhatTheHack',
  httpVersion: '1.1',
  httpVersionMajor: 1,
  httpVersionMinor: 1,
  headers: { foo: 'bar' },
  rawHeaders: [ 'Foo', 'bar' ]
}

100 Continue

會在第 26 篇文章介紹到

103 Early Hints

103 Early Hints 最常用的情境就是告訴瀏覽器 "response body 還在準備中,但你可以先載入這些 Link 的資源"

http-103-early-hints

server

import http from "http";
import { readFileSync } from "fs";
import { join } from "path";

const httpServer = http.createServer();
httpServer.listen(5000);
httpServer.on("request", (req, res) => {
  if (req.url === "/") {
    const indexHTML = readFileSync(join(import.meta.dirname, "index.html"));
    res.writeEarlyHints({ link: "</style.css>; rel=preload; as=style" });
    setTimeout(() => res.end(indexHTML), 1000);
    return;
  }
  if (req.url === "/style.css") {
    res.end("body { color: red; }");
    return;
  }
  res.writeHead(404).end();
});

index.html

<h1>hello world</h1>

瀏覽器打開 http://localhost:5000/ ,發現 103 Early Hints 提供的 /style.css 沒正確被載入

http1.1-no-early-hints

但還是可以用 curl -v http://localhost:5000/ 戳看看

< HTTP/1.1 103 Early Hints
< Link: </style.css>; rel=preload; as=style
<
< HTTP/1.1 200 OK
< Connection: keep-alive
< Keep-Alive: timeout=5
< Content-Length: 20
<
<h1>hello world</h1>

Browser compatibility

查詢 MDN 文件關於 103 Early Hints 的描述,發現主流瀏覽器都是在 HTTP/2 才有支援~

至於為何 HTTP/1.1 不建議使用呢?原因是ㄧ些老舊的 proxy 不支援 1xx Informational response

如果把 103 Early Hints 當成正常 HTTP response 的話

HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style


就有可能把 final rsponse 留在 TCP socket,造成 Response Queue Poisoning

HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style

HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 20

<h1>hello world</h1>

通常 MDN 這種面對大眾的文件都會寫的比較隱晦

For compatibility and security reasons, it is recommended to only send HTTP 103 Early Hints responses over HTTP/2 or later unless the client is known to handle informational responses correctly.

Multiple 103 Early Hints

我們接著看看 RFC 9110 Section 15.2. Informational 1xx 的介紹

A client MUST be able to parse one or more 1xx responses received prior to a final response, even if the client does not expect one. A user agent MAY ignore unexpected 1xx responses.

調整 Node.js http.Server,回傳兩個 Early Hints

import http from "http";

const httpServer = http.createServer();
httpServer.listen(5000);

httpServer.on("request", (req, res) => {
  if (req.url === "/") {
    res.writeEarlyHints({ link: "</style.css>; rel=preload; as=style" });
    res.writeEarlyHints({ link: "</script.js>; rel=preload; as=script" });
    res.end("hello world");
    return;
  }
  res.writeHead(404).end();
});

curl -v http://localhost:5000/ 戳看看

< HTTP/1.1 103 Early Hints
< Link: </style.css>; rel=preload; as=style
<
< HTTP/1.1 103 Early Hints
< Link: </script.js>; rel=preload; as=script
<
< HTTP/1.1 200 OK
< Connection: keep-alive
< Keep-Alive: timeout=5
< Content-Length: 11
<
hello world

client 改用 Node.js

import http from "http";

const clientRequest = http.request({ host: "localhost", port: 5000 });
clientRequest.on("information", console.log);
clientRequest.end();

印出以下資訊,Node.js 有正確處理多個 1xx Informational response

{
  statusCode: 103,
  statusMessage: 'Early Hints',
  httpVersion: '1.1',
  httpVersionMajor: 1,
  httpVersionMinor: 1,
  headers: { link: '</style.css>; rel=preload; as=style' },
  rawHeaders: [ 'Link', '</style.css>; rel=preload; as=style' ]
}
{
  statusCode: 103,
  statusMessage: 'Early Hints',
  httpVersion: '1.1',
  httpVersionMajor: 1,
  httpVersionMinor: 1,
  headers: { link: '</script.js>; rel=preload; as=script' },
  rawHeaders: [ 'Link', '</script.js>; rel=preload; as=script' ]
}

RFC 8297

不過很有趣的是,雖然 103 Early Hints 並不在 RFC 9110: HTTP Semantics 的規範內

而是定義在 RFC8297: An HTTP Status Code for Indicating Hints

雖然只是 Proposed Standard,但主流瀏覽器在 HTTP/2 以後都有實作

server.on("checkExpectation")

RFC 9110 Section 10.1.1. Expect 有提到

The only expectation defined by this specification is "100-continue" (with no defined parameters).

不過 Expect 在語意上是可以支援其他 Expectation 的,所以 Node.js 預留了一個空間

import http from "http";

const httpServer = http.createServer();
httpServer.listen(5000);
httpServer.on("checkExpectation", (req, res) => {
  res.writeHead(417);
  res.end(`Sorry, ${req.headers.expect} is not supported`);
});

curl -H "Expect: 104-helloworld" -v http://localhost:5000/ 戳看看

< HTTP/1.1 417 Expectation Failed
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
Sorry, 104-helloworld is not supported

若 user program 沒有監聽 server.on("checkExpectation"),則 Node.js 預設也會回 417 Expectation Failed

HTTP/1.1 417 Expectation Failed
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

0


server.on("clientError")

觸發情境包含但不限於以下:

Node.js Error Code Description
ERR_HTTP_REQUEST_TIMEOUT Incomplete request exceeded headersTimeout or requestTimeout
HPE_CHUNK_EXTENSIONS_OVERFLOW Transfer-Encoding: chunkedchunked extensions exceeds maximum
HPE_HEADER_OVERFLOW Exceeded maxHeaderSize
  • 這些情境,基本上都是 DoS 或是 HTTP request smuggling 的溫床
  • Node.js 會在 user program 沒有註冊 on("clientError") 的情況,直接用 socket.destroy 關閉連線
  • 如果 user program 有註冊 "clientError" 的話,則需要自行調用 socket.destroy 來關閉連線
  • 細節使用方法,在 Node.js http.Server 資安防護 有介紹到

on("upgrade")

Node.js 在 ClientRequesthttp.Server 分別提供了 on("upgrade") 事件

import http from "http";

const upgradeResponse =
  "HTTP/1.1 101 Switching Protocols\r\n" +
  "Connection: Upgrade\r\n" +
  "Upgrade: Websocket\r\n\r\n";
const httpServer = http.createServer();
httpServer.listen(5000);
httpServer.on("upgrade", (req, socket, head) => {
  // ✅ client 會觸發 `request.on("upgrade")`
  socket.write(upgradeResponse);
});

// ✅ client 若送 Upgrade 請求,就會觸發 `server.on("upgrade")`
const clientRequest = http.request({
  host: "localhost",
  port: 5000,
  headers: {
    connection: "Upgrade",
    upgrade: "Websocket",
  },
});
clientRequest.end();
clientRequest.on("upgrade", (response, socket, head) => {
  // ✅ It's now your responsibility to handle TCP socket
});
  • 99% 的使用情境是需要 Upgrade 到 WebSocket,server 才必須監聽此事件
  • 不過 ws: a Node.js WebSocket library 已經處理好這個細節了
  • 如果真的要學習的話,我會等到之後需要學習 WebSocket,再去翻 ws 的原始碼來讀

小結

在這篇文章,我們學到了

  • ClientRequeston("information")
  • 103 Early Hints 的用法跟 Browser compatibility,以及為何瀏覽器僅在 HTTP/2 之後才支援
  • server.on("checkExpectation") 的觸發情境跟用法
  • server.on("clientError") 的觸發情境
  • on("upgrade") 的觸發情境跟用法

上一篇
Node.js v24.5.0 Built-in Proxy Support
下一篇
深入理解 HTTP/1.1 pipelining 與 HOL Blocking
系列文
Learn HTTP With JS(2)21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言