HTTP 是什麼?伺服器管理員必懂的請求與回應
從伺服器管理角度理解 HTTP:掌握方法、狀態碼、重要標頭、nginx access log,以及 HTTP/3 與 TLS 的關係。
什麼是 HTTP?
HTTP(超文字傳輸協定)是用戶端與 Web 伺服器用來提出要求並傳回結果的一組規則。用戶端會傳送請求,其中包括方法,例如 GET、路徑,例如 /pricing、協定版本、標頭清單,有時也包括主體。伺服器會以狀態碼回應,例如 200,接著傳送自己的標頭,通常也會傳送主體。伺服器上的每次頁面檢視與每次 API(應用程式介面)呼叫,都是這項交換的重複執行。
HTTP 本身不會保留狀態。伺服器不會記住你一秒前要求的內容,因此任何需要記憶功能的行為,例如登入工作階段,都必須在每個請求的標頭中傳送。這項特性說明了後續許多內容:快取完全由標頭驅動,負載平衡器也能將下一個請求傳送到不同的後端,而不會造成問題。
以下內容說明這個模型在伺服器端的呈現方式,包括 access log 與 nginx 設定。
原始請求與回應註解
以下是完整的 HTTP/1.1 請求。空白行會結束標頭,該行之後的內容都是本文。GET 通常沒有本文。
GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzipGET是方法,用來指定要執行的操作。GET讀取資料,POST傳送資料,PUT取代資料,DELETE移除資料,HEAD要求取得GET的標頭,但不傳送本文。/pricing是路徑。主機名稱不屬於請求列,因此才需要下一個標頭。HTTP/1.1是用戶端使用的通訊協定版本。Host: example.com指定用戶端要存取的網站。HTTP/1.1 要求此標頭,因此 nginx 對缺少此標頭的請求會回應400 Bad Request。- 其餘標頭是偏好設定。
Accept-Encoding: gzip表示用戶端可以解壓縮,因此伺服器可以壓縮本文。
回應的結構相同,但頂端是狀態列。
HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300
<!doctype html>...200 OK是狀態碼及其原因短語。重要的是狀態碼。原因短語只是裝飾,用戶端會忽略它。Content-Type告知用戶端如何處理後續的位元組。Content-Length是本文的位元組大小,讓用戶端知道本文在哪裡結束。若事先不知道大小,伺服器會改用Transfer-Encoding: chunked傳送,並以長度為 0 的區塊標示結尾。Cache-Control告知瀏覽器及中間的快取可以保留此回應多久。- 在兩個方向中,標頭後的空白行都會將標頭與本文分隔開來。
標頭名稱不區分大小寫,每一行都以歸位字元後接換行字元結尾,而不是單獨使用換行字元。你不會手動輸入這些內容,但會在封包擷取中看到它們。
若要觀察實際的請求與回應,請對你擁有的網站執行以下指令:
curl -sS -o /dev/null -D - https://example.com/-D - 會將回應標頭寫入終端機,-o /dev/null 會丟棄本文。請優先使用這種方式,而不要使用 curl -I,因為 -I 會傳送 HEAD 請求。處理 HEAD 的應用程式伺服器若與處理 GET 的方式不同,而許多伺服器確實如此,便會顯示瀏覽器永遠不會收到的標頭。curl -v 會列印兩個方向的內容,並以 > 標示請求列、以 < 標示回應列。
nginx access log 中的請求列格式
nginx 內建 combined 日誌格式,定義如下:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';這是由該格式產生的一行日誌:
203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"203.0.113.45是$remote_addr,也就是建立 TCP(傳輸控制協定)連線的來源位址。若流量經過 proxy,這裡記錄的是 proxy,而不是訪客的位址。- 第一個
-是固定的佔位符。第二個是$remote_user,只有使用 HTTP basic authentication 時才會填入。 "GET /pricing HTTP/1.1"是$request,也就是原封不動複製收到的請求列。200是伺服器回傳的狀態碼,不是訪客實際看到的狀態。5310是$body_bytes_sent,只計算回應本文。回應標頭不列入計算,因此這個數值一定小於實際傳送的位元組數。- 最後兩個加引號的欄位是
Referer與User-Agent。兩者都來自用戶端,因此內容可能是任意資料。
由於 $request 會逐字複製,無效資料也會原樣出現。若用戶端在純文字的 80 埠上使用 TLS(傳輸層安全性)通訊,400 日誌列的請求欄位開頭會出現類似 "\x16\x03\x01\x02\x00\x01" 的逸出位元組。\x16 是 TLS handshake record type,因此這些位元組是 ClientHello 的開頭,根本不是請求列。伺服器的行為是正確的。表示有某個來源將 HTTPS 指向 HTTP 埠。
也將 $server_protocol 加入日誌格式。它會顯示 HTTP/1.1、HTTP/2.0 或 HTTP/3.0,是確認通訊協定變更確實生效的最快方法。
網站回傳常見狀態碼時的含義
第一個數字代表類別,而你應先查看類別。
2xx 表示處理成功。 200 OK 用於一般讀取。建立資源的 POST 後會回傳 201 Created。204 No Content 表示成功但沒有內容可回傳,通常用於回應 DELETE。
3xx 表示應改往其他位置。 301 表示永久重新導向,瀏覽器會長時間快取,有時甚至要等使用者清除設定檔才會解除。因此,指向錯誤主機名稱的 301 很難復原。仍在測試重新導向時,請使用 302。304 Not Modified 表示成功,不是錯誤:用戶端送出帶有仍可辨識之 ETag(entity tag)的 If-None-Match,因此你只回傳標頭,不回傳本文。日誌中充滿 304,表示快取正在運作。
4xx 表示請求有誤。 400 Bad Request 表示輸入格式錯誤。401 Unauthorized 實際上表示未通過驗證,且必須帶有指名驗證機制的 WWW-Authenticate 標頭。403 Forbidden 表示請求已被理解,但仍遭到拒絕。404 Not Found 表示路徑不存在。405 Method Not Allowed 表示路徑正確,但使用了錯誤的方法;對靜態檔案位置執行 POST 時,就會得到此回應。413 表示本文大於 nginx 的 client_max_body_size;該設定預設為 1 megabyte,錯誤日誌會以 client intended to send too large body 確認此問題。
靜態檔案出現 403 時,幾乎總是檔案系統問題,而不是 HTTP 規則問題。修改任何設定前,先查看 /var/log/nginx/error.log。open() "/srv/site/index.html" failed (13: Permission denied) 表示 nginx worker 使用者無法讀取檔案,最常見的原因是父目錄未對其他使用者授予執行權限。directory index of "/srv/site/" is forbidden 表示路徑解析為目錄,但其中沒有 index 檔案,且 autoindex 已關閉。
5xx 表示你的端發生錯誤。 500 表示應用程式發生未處理的錯誤。502 Bad Gateway 表示 nginx 無法從 upstream 取得可用回應;錯誤日誌會指出原因:connect() failed (111: Connection refused) while connecting to upstream 表示 proxy_pass 指定的位址上沒有任何程序監聽。504 Gateway Timeout 表示 upstream 接受連線後,在 proxy_read_timeout 內未回應;預設為 60 seconds,日誌會記錄為 upstream timed out (110: Connection timed out) while reading response header from upstream。503 Service Unavailable 表示刻意拒絕。請注意,nginx 自己的速率限制器會回傳 503,因為 limit_req_status 預設為 503。如果你在日誌中尋找 429 Too Many Requests,卻找到 503,原因就在這裡。設定 limit_req_status 429;,即可取得正確的狀態碼。
執行伺服器時的重要標頭
Host 用來選擇網站。同一個 IP 位址可以提供數百個主機名稱,而 nginx 會比對 Host 與 server_name,決定由哪個 server 區塊回應。如果沒有相符項目,nginx 會使用預設伺服器;除非其他區塊標記為 default_server,否則預設伺服器就是在該位址與連接埠上接聽的第一個區塊。新增 virtual host 後收到錯誤網站,幾乎總是因為名稱不相符,請求因此落入預設伺服器。不必修改 DNS 即可測試:
curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/User-Agent 是 client 撰寫的自我描述,內容是自由文字。讀取日誌時,可將它視為提示。不要用它作為控制依據,因為想要偽造的 client 只要修改即可,所以依 User-Agent 封鎖 scraper 只會攔下守規矩的 scraper。
Content-Type 決定如何解讀位元組:API 請求使用 application/json,頁面使用 text/html; charset=utf-8。nginx 會透過 /etc/nginx/mime.types 將檔案副檔名對應到類型,而套件提供的 nginx.conf 會設定 default_type application/octet-stream;。因此,對 nginx 不認識副檔名的檔案,伺服器會將其提供為下載,而不是直接呈現。常見症狀是頁面載入但沒有樣式,同時瀏覽器主控台顯示 Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type。這裡的 MIME 是 multipurpose internet mail extensions,亦即這些類型字串所採用的命名機制。
Cache-Control 用來控制伺服器與讀取者之間的所有快取。public, max-age=31536000, immutable 適合用於檔名包含內容雜湊的資產,因為內容變更時檔名也會變更。任何具有使用者個人資料的內容都應使用 no-store,因為共用快取若保留已登入頁面,會將該頁面提供給下一個要求相同 URL 的人。private 是中間設定:瀏覽器可以保留內容,但共用快取不可以。
X-Forwarded-For 存在的原因是 proxy 會隱藏訪客位址。請求通過 reverse proxy 後,$remote_addr 會是 proxy 的位址,因此日誌、地理位置判定與速率限制都會將所有請求視為來自同一個 client。proxy 必須轉送原始位址:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;接收請求的伺服器也必須設定為信任該標頭,並明確指定要信任的來源:
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;只列出由你控制的範圍。X-Forwarded-For 是任何 client 都能傳送的純文字,因此 set_real_ip_from 0.0.0.0/0; 會讓訪客自行指定日誌記錄的位址,以及速率限制所計算的位址。
X-Forwarded-Proto 可避免一種特定且非常常見的失敗。你的 proxy 終止 TLS,並透過純 HTTP 將請求轉送到應用程式。應用程式看到的是純 HTTP 請求,判定訪客應使用 HTTPS,並回應 301 https://example.com/。瀏覽器遵循該回應,proxy 再次終止 TLS,然後再次轉送純 HTTP;迴圈會持續,直到瀏覽器放棄並顯示 ERR_TOO_MANY_REDIRECTS。傳送 X-Forwarded-Proto: https 可告知應用程式訪客已使用 HTTPS,因此應用程式會停止重新導向。
HTTP/1.1、HTTP/2 與 HTTP/3:對你而言有哪些變化
HTTP/1.1 使用文字格式,且每個連線一次只能處理一個請求。Connection: keep-alive 可讓下一個請求重複使用相同的 TCP 連線,省去建立連線的成本,但回應仍會依請求順序返回。只要其中一個回應速度很慢,就會阻塞後方所有排隊中的請求。這稱為隊頭阻塞,瀏覽器會同時對相同主機名稱建立多個連線,以降低其影響。
HTTP/2 保留相同的方法與狀態碼,並將訊框格式改為二進位。多個請求可在同一個連線上以彼此獨立的串流傳輸,重複的標頭文字也會經過壓縮;現代請求通常包含大量標頭,因此這點很重要。連線仍使用 TCP,因此只要有一個封包遺失,在重傳封包抵達前,該連線上的所有串流都會停滯。隊頭阻塞並未消失,而是從 HTTP 層下移至傳輸層。Server push 原本是 HTTP/2 的功能,但實務上已不再使用,因為 Chrome 已在 2022 移除支援。
HTTP/3 再次保留相同的語意,並以 QUIC 取代 TCP。QUIC 是建構在 UDP(user datagram protocol)上的傳輸協定。QUIC 串流在傳輸層以下仍彼此獨立,因此封包遺失只會阻塞所屬的串流。TLS 1.3 內建於 QUIC handshake,而不是另外疊加在其上,因此建立新連線時需要的往返次數較少。這會帶來兩項實務上的影響:路徑上的每個 firewall 都必須開放 UDP port 443,而任何限制或封鎖 UDP 的網路,都會讓 client 回退至 HTTP/2。
具體而言,這會如何影響你?瀏覽器不會一開始就使用 HTTP/3。它們會先透過 HTTP/2 或 HTTP/1.1 連線,從回應中的 Alt-Svc: h3=":443"; ma=86400 header 得知 HTTP/3,之後對該主機建立的連線才會使用 HTTP/3。因此,這個 header 不是可有可無的裝飾,而是服務探索機制。在 nginx 中,HTTP/2 在 1.25.1 版本成為獨立 directive(在 server 區塊中使用 http2 on;,取代舊的 listen ... http2 參數),而 QUIC 則在 mainline 1.25.0 中加入;HTTP/3 網站需要搭配一般的 listen 443 ssl; 使用 listen 443 quic reuseport;。
各種 proxy 在這方面的成熟度不同,應依實際執行的版本確認。截至 August 2026,Caddy 預設提供 HTTP/3,不需要額外設定。nginx 需要明確的 quic listener,以及上述的 Alt-Svc header。Traefik 則可透過明確的 http3 option,針對各 entry point 啟用 HTTP/3。如果你在 多個 Docker 應用程式前方使用 Traefik 終止 TLS,訪客取得的 protocol version 會由 Traefik 決定,而 proxy 到 container 的連線通常是 plain HTTP/1.1,無論瀏覽器協商出哪個版本。
不要假設,應實際驗證。只有在 curl -V 的 features 清單包含 HTTP3 時,curl --http3 -sS -o /dev/null -D - https://example.com/ 才能運作,而大多數 distribution build 都未包含它。可靠的檢查方式是查看你自己的 log:將 $server_protocol 加入 format,並讀取實際瀏覽器協商出的結果。在進行上述檢查前,先確認 UDP 443 確實已開放,因為只允許 TCP 443 的 firewall 會讓 HTTP/3 悄悄失敗,而網站仍會透過 HTTP/2 正常運作。了解 Linux server 上哪些 port 已開放並正在 listening,是第一個應檢查的事項。
HTTPS:HTTP 是通訊協定,TLS 是包裝層
HTTPS 不是獨立的通訊協定。它是在 TLS 工作階段中傳送相同的請求與狀態碼。連接埠 80 以明文傳送,連接埠 443 則以加密方式傳送。TLS 交握會先完成,接著 HTTP 請求才會在加密通道中傳送。這個順序說明了為何憑證問題不會附帶狀態碼:錯誤發生在任何 HTTP 位元組送出之前,因此沒有可編號的回應。
在同一台伺服器代管多個網站時,有一項順序細節很重要。伺服器會使用 SNI(server name indication)選擇憑證。SNI 是 TLS 交握中的欄位,會在任何 HTTP 標頭存在之前,以明文傳送主機名稱。因此,伺服器會先依據 SNI 選擇憑證,再依據 Host 標頭選擇虛擬主機。這是兩次分開的查找,通常會得到一致的結果。若兩者不一致,瀏覽器會顯示類似 NET::ERR_CERT_COMMON_NAME_INVALID 的名稱不相符錯誤,且完全不會送出請求,因為你的預設伺服器提供了不涵蓋該名稱的憑證。
對公開網站而言,請取得正式憑證,並讓系統自動續期。在 nginx 上使用 Let's Encrypt 的 Certbot 會將憑證路徑寫入伺服器區塊,並替你安裝續期計時器。若沒有任何公開憑證機構能驗證主機名稱,例如內部名稱或自有網路中的裸 IP 位址,在 Ubuntu 上建立自簽憑證 是誠實的選項,但前提是你接受每個用戶端都必須設定為信任該憑證。
TLS 運作正常後,將連接埠 80 上的所有請求轉送到連接埠 443:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}只有在確定後才加入 Strict-Transport-Security。add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; 標頭會告知瀏覽器,未來兩年內拒絕透過明文 HTTP 存取該主機名稱。瀏覽器會依據自身快取執行這項規則,因此之後移除標頭也無法立即取消。請先將 max-age 設為數小時,確認每個子網域確實都使用 HTTPS,再提高其值。
FAQ
HTTP 和 HTTPS 有何差異?
HTTPS 是在 TLS (transport layer security) 工作階段中傳送的 HTTP。方法與狀態碼完全相同。差異在於,從用戶端到 TLS termination 端點之間的位元組會經過加密,預設連接埠也會從 80 改為 443。由於 TLS handshake 會在傳送第一個 HTTP 位元組前完成,因此憑證失敗不會產生狀態碼。這就是瀏覽器的憑證警告會顯示 NET::ERR_CERT_COMMON_NAME_INVALID 這類錯誤名稱,而不是 403 這類數字的原因。
為什麼我的網站會回傳 502 Bad Gateway?
nginx 回傳 502,表示 nginx 無法從它代理的 upstream 取得可用回應。因此,訪客的請求沒有問題,問題出在 nginx 後方的某個元件。請閱讀 /var/log/nginx/error.log。connect() failed (111: Connection refused) while connecting to upstream 表示 proxy_pass 中指定的位址與連接埠沒有任何服務在監聽,因此請確認應用程式正在執行,且已繫結至預期的位置。no live upstreams while connecting to upstream 表示 upstream block 中的所有伺服器,在重複失敗後都已被標記為停止服務。請與 504 Gateway Timeout 比較;後者表示 upstream 已接受連線,但未能在 proxy_read_timeout 內回應。
為什麼我的存取日誌對每位訪客都顯示相同的 IP 位址?
因為 $remote_addr 記錄的是建立 TCP 連線的位址;在反向代理或 content delivery network 後方,該位址就是代理伺服器。訪客的位址則會放在 X-Forwarded-For 標頭中。先在代理伺服器上設定 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;,再在接收請求的 nginx 上將 set_real_ip_from 設為代理伺服器的位址範圍,並設定 real_ip_header X-Forwarded-For;。只列出由你管理的位址範圍,因為該標頭只是用戶端可任意傳送的文字。若信任整個網際網路傳來的值,訪客就能自行指定你記錄的位址,以及你套用速率限制的位址。
我需要啟用 HTTP/2 或 HTTP/3 嗎?
值得啟用 HTTP/2,因為對於已設定 TLS 的網站,只需加入一個 directive,就能移除每條連線的請求數量限制。這項限制會讓包含許多小檔案的頁面變慢。HTTP/3 帶來的效益較小,也較不確定,且需要開放 UDP port 443,以及支援 QUIC 的代理伺服器建置版本。請注意,瀏覽器只有在較早的回應中看到 Alt-Svc 標頭後,才會切換至 HTTP/3。因此,如果沒有該標頭,無論 listen line 如何設定,都不會發生任何變化。請將 $server_protocol 加入 log format,先測量訪客實際協商使用的協定,再決定是否投入時間設定。
檔案存在時,403 Forbidden 代表什麼?
在靜態網站中,403 通常是檔案系統權限問題,而不是 HTTP 規則問題。open() ... failed (13: Permission denied) 位於 /var/log/nginx/error.log 表示 nginx worker user 無法讀取檔案。最常見的原因是上層目錄沒有對其他使用者開放 execute permission,而不是檔案本身的 mode 設定錯誤。directory index of ... is forbidden 表示請求解析到一個沒有 index file 的目錄,而 autoindex 處於關閉狀態。若相符的 location block 中有明確的 deny rule,也會回傳 403。因此,當 error log 沒有提供資訊時,請檢查該 block。