iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
JavaScript

Learn HTTP With JS(2)系列 第 26

Expect: 100-continue 解析:用途、Node.js 實作與 curl 行為

  • 分享至 

  • xImage
  •  

前言

2025/11,我在 Linkedin 看到了 James Kettle 發的這篇文章

簡單講就是利用 Frontend 跟 Backend 對 Expect: 100-continue 解析的不一致,來達成 HTTP Response Queue Poisoning,但細節不會在這篇談到

誠如 James Kettle 在 HTTP/1.1 Must Die 所述,會有越來越多不同種類的 HTTP Request Smuggling 被挖掘出來;在我們了解 HTTP Request Smuggling 之前,先來把 HTTP 的基礎打好,就從 Expect: 100-continue 這個實務上很少用的 request header 開始研究吧!

正常的使用情境

Expect: 100-continue 設計的初衷,是用來優化大型檔案的傳輸

假設 client 想要上傳一個大型圖片,但不確定 server 會不會拒絕這個 request,就可以先送 Expect: 100-continue

POST /image HTTP/1.1
Host: example.com
Content-Type: image/jpg
Content-Length: 123456789
Expect: 100-continue


server 收到後,若允許這個 request,則立即回傳

HTTP/1.1 100 Continue


client 就可以開始(在同一個 TCP connection)傳送 request body 了

若 server 收到後,不允許這個 request,則可以根據情況回傳

  • 401 Unauthorized
  • 405 Method Not Allowed
  • 413 Content Too Large

或是其他不同的狀態碼

Expect other than 100-continue

根據 RFC 9110 section-10.1.1 的描述

A server that receives an Expect field value containing a member other than 100-continue MAY respond with a 417 (Expectation Failed) status code to indicate that the unexpected expectation cannot be met.

目前實測下來,有找到 Apache Web Server 會處理這個 edge case

request

GET /robots.txt HTTP/1.1
Host: example.com
Expect: 123


response

HTTP/1.1 417 Expectation Failed
Server: Apache
Content-Length: 355
Content-Type: text/html; charset=iso-8859-1
Expires: Mon, 15 Dec 2025 07:13:09 GMT
Cache-Control: max-age=0, no-cache, no-store
Pragma: no-cache
Connection: keep-alive

HTML 渲染結果如下圖

417-expectation-failed

raw HTTP request / response

若支援 100-continue 的 server,則會看到以下

request

GET / HTTP/1.1
Host: example.com
Expect: 100-continue


response

HTTP/1.1 100 Continue
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 10
Connection: keep-alive

1234567890

Node.js http.Server 預設支援 100 Continue

根據 Event: 'checkContinue' 的描述

Emitted each time a request with an HTTP Expect: 100-continue is received. If this event is not listened for, the server will automatically respond with a 100 Continue as appropriate.

架一個 http.Server

import http from "http";
const httpServer = http.createServer((req, res) => {
  res.end("ok");
});
httpServer.listen(5000);

client 直接送

GET / HTTP/1.1
Host: localhost:5000
Expect: 100-continue


server 會回傳

HTTP/1.1 100 Continue

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

ok

建議使用 Burp Suite Repeater 或是可以完整印出 raw HTTP response 的 HTTP client,才可以觀察到這個結果

Node.js http.Server 自行處理 100 Continue

實務上,幾乎不需要在 Application Layer 自行處理 100 Continue,因為很少有 client 會發這種 request,但 Node.js http.Server 還是有提供這個 API 可以用

架一個 http.Server

import http from "http";

const httpServer = http.createServer();
httpServer.listen(5000);
httpServer.on("checkContinue", function checkContinueListener(req, res) {
  // >= 1MB 就 reject
  if (parseInt(req.headers["content-length"] || "0") >= 1024 * 1024) {
    res.statusCode = 413;
    res.end(http.STATUS_CODES[413]);
    return;
  }
  // < 1MB,讀取 request body,並且原封不動寫入 response body
  res.writeContinue();
  const chunks: Buffer[] = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => res.end(Buffer.concat(chunks)));
});

client side 使用 net.Socket 手動構造 raw HTTP request

import net from "net";

const rawHttpRequestWithoutBody = `POST / HTTP/1.1
Host: localhost:5000
Expect: 100-continue
Content-Length: 3

`.replaceAll("\n", "\r\n");

function createSocket(url: URL) {
  return new Promise<net.Socket>((resolve) => {
    const socket = net.connect(parseInt(url.port), url.hostname);
    socket.once("connect", () => resolve(socket));
  });
}

async function main() {
  const url = new URL("http://localhost:5000");
  const socket = await createSocket(url);
  socket.write(rawHttpRequestWithoutBody);
  socket.setEncoding("utf8");
  socket.on("data", (chunk: string) => {
    console.log(chunk);
    if (chunk === "HTTP/1.1 100 Continue\r\n\r\n") socket.write("123");
  });
}

main();

從 client side 的 log 可以觀察到,會先印出

HTTP/1.1 100 Continue


緊接著,會馬上印出

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

123

另外,Node.js 的 ClientRequest 其實也支援 100 continue

import http from "http";

const clientRequest = http.request({
  host: "localhost",
  port: 5000,
  path: "/",
  method: "POST",
  // ✅ 正常送出 expect: 100-continue 這個 request header
  headers: { expect: "100-continue", "content-length": 3 },
});
// ✅ 等同於幫使用者處理 `if (chunk === "HTTP/1.1 100 Continue\r\n\r\n")`
clientRequest.on("continue", () => clientRequest.end("123"));
clientRequest.on("response", (response) => {
  response.setEncoding("utf8");
  response.on("data", console.log); // 123
});

curl 預設有支援 Expect: 100-continue

根據 everything.curl.dev 的描述

curl sends this Expect: header by default if the POST it does is known or suspected to be larger than one megabyte.

實測用 curl 構造一個 1048577 Bytes(1MB + 1byte)的 POST 請求,確實預設會帶上 Expect: 100-continue

curl-data-larger-than-1MB

同時 curl 也有提到,因為很多 server 不支援 100 Continue;所以 curl 只會等待 1 秒,即便沒收到 100 Continue,也會繼續傳送 request body

Unfortunately, lots of servers in the world do not properly support the Expect: header or do not handle it correctly, so curl only waits 1000 milliseconds for that first response before it continues anyway.

根據 RFC 9110 section-10.1.1,確定這是符合規範的做法

A client that sends a 100-continue expectation is not required to wait for any specific length of time

小結

在這篇文章,我們學到了

  • Expect: 100-continue 的使用情境
  • HTTP server 收到 Expect other than 100-continue 要如何處理
  • Node.js http 模組的 client / server 怎麼處理 100 continue 的 request / response
  • curl 針對大檔案上傳的優化

參考資料


上一篇
HTTP Link header 教學:比 HTML <link> 更快載入資源
下一篇
為何 response header 會出現 Nncoection 這個怪異拼法?從 TCP Checksum 到 F5 BIG-IP 架構,一步步破解
系列文
Learn HTTP With JS(2)29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言