HTTP Request Methods (上篇)
前言
我們平常 RESTFUL API 會用到的 HTTP Request Methods 就 GET, POST, PUT, PATCH, DELETE
- GET: 取資料
- POST: 新增資料
- PUT: 更新資料
- PATCH: 部分更新資料
- DELETE: 刪除資料
但,今天我想要深入理解,平常不會去使用那些 HTTP Request Methods,一起來看看吧!
HEAD
- 簡單理解:同 GET 請求,只是把 Response Body 拿掉
- 承上,如果 Response Body 有值,HTTP Client "MUST" 忽略它
- 使用情境:下載大型檔案前,先發一個 HEAD 請求,讀取 Response.Headers.Content-Length,就可以預先知道檔案大小
- 如果發了 HEAD 請求,Server 回傳說 "快取過期了"。此情況下,快取會被更新,即便 GET 請求沒有發送
- 承上,詳細的測試情境,我們放到未來的篇章 http-caching
curl --head with third party static website testing
測試看看 curl --head example.com
,結果如下
HTTP/1.1 200 OK
Content-Type: text/html
ETag: "84238dfc8092e5d9c0dac8ef93371a07:1736799080.121134"
Last-Modified: Mon, 13 Jan 2025 20:11:20 GMT
Cache-Control: max-age=3457
Date: Mon, 30 Jun 2025 00:06:32 GMT
Connection: keep-alive
為什麼沒有回傳 Content-Length
呢?根據 RFC9110 9.3.2. HEAD 的描述:
However, a server MAY omit header fields for which a value is determined only while generating the content.
所以說,透過 HEAD 請求預先讀取 Response.Headers.Content-Length,其實不一定有效的
我們再來嘗試 curl --head https://httpwg.org/specs/rfc9110.html
可以看到 HEAD 請求跟 GET 請求回傳的 Content-Length
也不一樣
curl --head with local static file
我們使用先前的文章 http-range-requests#send-套件的實作 介紹過的 send
套件來實作
index.ts
import send from "send";
import httpServer from "../httpServer";
import { faviconListener } from "../listeners/faviconListener";
import { notFoundListener } from "../listeners/notFoundlistener";
httpServer.on("request", function requestListener(req, res) {
if (req.url === "/favicon.ico") return faviconListener(req, res);
if (req.url === "/example.txt") {
return send(req, String(req.url), { root: __dirname }).pipe(res);
}
return notFoundListener(req, res);
});
example.txt
helloworld
嘗試 curl --head http://localhost:5000/example.txt
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Mon, 30 Jun 2025 00:33:43 GMT
ETag: W/"a-197be410daa"
Content-Type: text/plain; charset=utf-8
Content-Length: 10
Date: Mon, 30 Jun 2025 00:38:02 GMT
Connection: keep-alive
Keep-Alive: timeout=5
嘗試 curl -v http://localhost:5000/example.txt
,擷取 response header,可以看到跟 HEAD 請求是一樣的
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: public, max-age=0
< Last-Modified: Mon, 30 Jun 2025 00:33:43 GMT
< ETag: W/"a-197be410daa"
< Content-Type: text/plain; charset=utf-8
< Content-Length: 10
< Date: Mon, 30 Jun 2025 00:39:38 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
我們來看看 send
套件關於 HEAD 請求的實作(節錄部分):
SendStream.prototype.send = function send(path, stat) {
// other code...
// content-length
res.setHeader("Content-Length", len);
// HEAD support
if (req.method === "HEAD") {
res.end();
return;
}
this.stream(path, opts);
};
function 的上面已經把各種 Response Headers 都設定好了,最後要送出 Body 之前,檢查是否為 HEAD 請求,若是則直接調用 res.end()
我認為這個寫法很優美,並且也符合 Best Practice(HEAD 請求的 Response Headers 跟 GET 請求的一樣,只差在有沒有 Response Body)
HEAD 小結
使用 HEAD 請求來獲取 Content-Length
,影響的因素太多了,實務上:
- HTTP 請求通常不會直接打到 Application Server,中間都會過 Web Server, CDN, WAF 以及 Proxy 等等,中間每一層都有不同的機制去修改 HTTP Headers
- 但在 Application Server 這一層的實作,以
send
套件為例,確實是有 follow Best Practice - 通常後端工程師在寫 RESTFUL API 的時候,不會特別實現 HEAD 請求的商業邏輯,絕大部分都是各種 HTTP Framework, library 或是程式語言本身幫忙實現的
CONNECT
語法
跟一般的 HTTP 請求不一樣,這邊只要定義 host 跟 port 就好
CONNECT <host>:<port> HTTP/1.1
CONNECT www.google.com:443 HTTP/1.1
NodeJS HTTP Server 實作階段 1
NodeJS HTTP Server 提供原生的 Event 可以監聽 connect
事件,參考 NodeJS 官方文件 的描述
Emitted each time a client requests an HTTP CONNECT method.
我們實作 NodeJS HTTP Server,先簡單回傳 400 就好
httpServer.on("connect", function connectListener(req, socket, head) {
console.log({
url: req.url,
method: req.method,
headers: req.headers,
head: head.toString("utf8"),
});
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
return;
});
尋找支援 CONNECT METHOD 的 HTTP Client
根據 fetch.spec.whatwg.org 描述
A forbidden method is a method that is a byte-case-insensitive match for `CONNECT`, `TRACE`, or `TRACK`.
實際在 F12 > Console 輸入 fetch("www.google.com:443", { method: "CONNECT" })
也會報錯
Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'Window': 'CONNECT' HTTP method is unsupported.
at <anonymous>:1:1
POSTMAN 預設的 HTTP Request Methods 也沒有 CONNECT
好消息是,curl 有支援,輸入 curl --help all
,可以看到關於 proxy 的部分
-x, --proxy <[protocol://]host[:port]> Use this proxy
-p, --proxytunnel HTTP proxy tunnel (using CONNECT)
-v, --verbose Make the operation more talkative
我們在終端機輸入 curl -x http://localhost:5000 -p https://www.google.com -v
,可以看到 NodeJS 確實有回傳 HTTP/1.1 400 Bad Request
,整體過程看起來都蠻正常的。
* Host localhost:5000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:5000...
* CONNECT tunnel: HTTP/1.1 negotiated
* allocate connect buffer
* Establish HTTP proxy tunnel to www.google.com:443
> CONNECT www.google.com:443 HTTP/1.1
> Host: www.google.com:443
> User-Agent: curl/8.13.0
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 400 Bad Request
<
* CONNECT tunnel failed, response 400
* closing connection #0
curl: (56) CONNECT tunnel failed, response 400
同時也看看 NodeJS Log
{
url: 'www.google.com:443',
method: 'CONNECT',
headers: {
host: 'www.google.com:443',
'user-agent': 'curl/8.7.1',
'proxy-connection': 'Keep-Alive'
},
head: ''
}
NodeJS HTTP Server 實作階段 2
我們繼續把 NodeJS HTTP Server 功能補上
httpServer.on(
"connect",
function connectListener(clientToProxyReq, clientToProxySocket, head) {
console.log({
url: clientToProxyReq.url,
method: clientToProxyReq.method,
headers: clientToProxyReq.headers,
head: head.toString("utf8"),
});
// todo 驗證格式
const [host, portStr] = String(clientToProxyReq.url).split(":");
const port = parseInt(portStr);
const proxyToTargetSocket = createConnection(
port,
host,
function onConnect() {
clientToProxySocket.write(
"HTTP/1.1 200 [Custom Status Text]Connection Established\r\n\r\n",
);
proxyToTargetSocket.write(head);
proxyToTargetSocket.pipe(clientToProxySocket);
clientToProxySocket.pipe(proxyToTargetSocket);
},
);
// todo 處理錯誤情境, 關閉 TCP 連線
},
);
終端機輸入 curl -x http://localhost:5000 -p http://example.com -v
,節錄重點 HTTP round trip 的 Log
> CONNECT example.com:80 HTTP/1.1
> Host: example.com:80
> User-Agent: curl/8.7.1
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 [Custom Status Text]Connection Established
<
* CONNECT phase completed
* CONNECT tunnel established, response 200
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Type: text/html
< ETag: "84238dfc8092e5d9c0dac8ef93371a07:1736799080.121134"
< Last-Modified: Mon, 13 Jan 2025 20:11:20 GMT
< Cache-Control: max-age=2542
< Date: Tue, 01 Jul 2025 00:58:43 GMT
< Content-Length: 1256
< Connection: keep-alive
<
<!doctype html>
<html>
... 中間省略,都是 HTML 內容
</html>
P.S. 若對 Raw HTTP Request 跟 Response 不熟悉的朋友,可參考 anatomy-of-an-http-message 這篇文章~
我把整個 Round Trip 畫成循序圖,方便大家了解
CONNECT 小結
HTTP Request Method CONNECT 我認為比較難理解,原因是它需要對 TCP 有一定程度的理解,最好也要熟悉 NodeJS Net 模組。本篇章我盡量只講到 HTTP 的傳輸,對於 TCP 層連線的建立跟關閉都沒提到,這會在未來的篇章 TCP-Finite-State-Machine 跟大家詳細解釋,希望大家對 CONNECT 有初步的認識。
小結
沒想到兩個 HTTP Request Method 就可以講到這麼長的篇幅,本來以為很簡單,沒想到很多坑QQ
下一篇會跟大家講到 OPTIONS
跟 TRACE
~跟著我的腳步繼續探索吧!