HTTP 標頭與內容 — 傳什麼、用什麼格式
標頭是往返書信的附註。閱讀作為收件地址的 Host、表示格式與字元編碼的 Content-Type、表示數量的 Content-Length、傳達希望的 Accept 系列,以及在無狀態之上打造 session 的 Cookie 與 Set-Cookie。
本文明明是對的,卻莫名變亂碼・回來的東西不一樣
HTML 明明寫對了,日文卻化成像「譁�蟄�」的樣子。明明向 API 送了 JSON,卻被以「格式不正確」拒絕。明明是同一個 URL,不同人卻看到不同語言的頁面 — 這類「本文明明是對的」系的問題,答案多半不在本文,而在標頭。
標頭是附在往返書信上、名稱: 值 形式的附註。用書信來比喻,就是收件人・「易碎品」的標籤・重量的標示・回信用的名牌 — 雖然不是本文本身,但決定本文該如何被對待的資訊都聚集在這裡。這就是 3 點組合的第 3 點「條件與補充」。
標頭有數百種,但不需要背。本章只閱讀回答「寄給誰・是什麼有多少・帶著什麼希望・來自誰」的代表性 6 個。
標頭是附在本文上的標籤。收件地址(Host)・內容物的標示(Content-Type / Content-Length)・希望(Accept 系列)・名牌(Cookie / Set-Cookie)— 依職責捆起來看,數百種標頭也不再可怕。
Host — 一個地址,多個門牌
請求的必要標頭 Host: example.com,是信件的收件人。你也許會想「既然已經連上 IP 位址,收件人不是不言自明嗎?」,但實際上在 1 台伺服器(1 個 IP 位址)上,讓 www.example.com 與 shop.example.com 這樣多個網站同居的營運方式才是常態。
TCP 的連線只會把你帶到建築物的大門口。寄給哪個房間(網站),要靠信上的收件人 — Host 標頭 — 來傳達。伺服器看了它來切換回傳的內容。這就是名稱式虛擬主機;缺少 Host 的請求在 HTTP/1.1 會變成錯誤,因為沒有收件人的信是無法投遞的。
Content-Type 與 Content-Length — 內容物是什麼、有多少
回應的 Content-Type: text/html; charset=UTF-8 是「內容物是 HTML,字元編碼是 UTF-8」的標籤。瀏覽器靠前半的 text/html(MIME 類型)決定「是要渲染的 HTML、要顯示的圖片,還是要讓人下載的檔案」,靠後半的 charset 決定用哪種字元編碼把位元組序列還原成文字。
這裡就是亂碼的入口。實體以 UTF-8 儲存,卻宣告 charset=Shift_JIS,瀏覽器就會一板一眼地用錯誤的規則讀位元組序列,文字就亂了。麻煩之處在於它不會變成錯誤,而是亂著就顯示出來;對策只有一條 —「讓標籤與內容物一致」。另外,Content-Type 不是回應專用 — 用 POST 送 JSON 時,要在請求側加上 Content-Type: application/json 來傳達內容物的格式。
Content-Length: 1256 是「內文是 1256 位元組」的數量標示。在重複使用同一條 TCP 連線往來多封信件的 HTTP/1.1 中,接收方需要知道這次的內文到哪裡為止,這個數字就是那個分界。
練習 4-1 — 收件地址與內容物的附註
Host 與 Content-Type、Content-Length — 閱讀「寄給誰・是什麼・有多少」。
Q1. 在 1 台伺服器(1 個 IP 位址)上營運 www.example.com 與 shop.example.com 這 2 個網站。能讓伺服器判別請求寄給哪個網站的線索標頭是哪一個?
TCP 連線是對 IP 位址建立的,光看連線無法知道是寄給哪個網站。因此在請求的 Host 標頭寫上「寄給 www.example.com」這個收件地址,伺服器看了它來切換要回傳的網站(名稱式虛擬主機)。就像一棟掛著多個門牌的建築,靠信上的收件人把信分送到房間的機制。Host 在 HTTP/1.1 是必要標頭,原因正在於此。
Q2. 回傳以 UTF-8 儲存的日文 HTML 的回應,帶著 Content-Type: text/html; charset=Shift_JIS 這個標頭。會發生什麼事?
瀏覽器原則上相信標頭的宣告來解讀位元組序列。實體是 UTF-8 卻自稱 Shift_JIS,同一串位元組就會被讀成不同的字而變成亂碼。內容物與標籤不一致 — 這是亂碼入口的典型,要修的是讓標籤(charset 的宣告)或內容物(儲存時的字元編碼)兩者一致。麻煩的是它不會變成錯誤,而是就這樣亂著顯示出來。
Q3. 回應標頭 Content-Length: 1256 的正確意義是哪一個?
Content-Length 是內文的位元組數。在持續重複使用同一條連線的 HTTP/1.1 中,接收方需要知道「這次的內文到哪裡為止」,這個數字就是那個分界。既不是行數也不是連接埠號,有效期限則是下一章 Cache-Control 的工作。在與開發者工具 Size 欄的值對照的場面也會登場。
Accept 系列 — 傳達希望,請對方挑選
同一個 URL,想要的形式也因對象而異。瀏覽器想要 HTML,程式想要 JSON。對日文使用者想回傳日文頁面。傳達這種「希望」的,就是 Accept 系列的請求標頭。
伺服器從可提供的表現形式中挑選回傳。這就是內容協商(content negotiation)。重點在於這是協商而非強制 — 希望也可能不被採納,實際回來的是什麼,要看回應側的 Content-Type 等確認。「同一個 URL,不同人看到的卻不一樣」的謎底之一就在這裡。
Cookie 與 Set-Cookie — 在無狀態之上打造 session
來回收第 1 章的伏筆。HTTP 是無狀態的,伺服器不記得「剛才的後續」。那登入狀態是怎麼實現的 — 答案是 2 個標頭的連動。
Set-Cookie: session=abc123。「之後請出示這面名牌」Cookie: session=abc123。伺服器看了名牌就知道「是剛才那個人」換句話說,session 就是在無狀態的往返書信的每一封信上,持續附上同一面名牌所打造出來的「連續劇」。伺服器記住的只有「名牌 abc123 是誰」這張對應表,HTTP 本身從頭到尾都是無狀態的 — 讀懂這個構圖,「刪了 Cookie 就被登出」「換一個瀏覽器就被當成未登入」這些日常現象就全都能解釋。
練習 4-2 — 傳達希望・掛上名牌
Accept 系列的內容協商,以及 Cookie 打造 session 的機制。
Q4. 在請求上加 Accept: application/json 或 Accept-Language: ja 的「內容協商」,正確的說明是哪一個?
Accept 系列標頭是「可以的話請給 JSON」「可以的話請給日文」這種希望的表明,伺服器從備得出來的表現形式中挑選回傳(挑了什麼,可從回應的 Content-Type 等得知)。是協商而非強制,所以希望也可能不被採納。加密方式的合意是 TLS 交握、速度調整是 TCP 的工作,不在 HTTP 標頭的守備範圍。
Q5. 關於登入的機制,正確的說明是哪一個?
HTTP 是無狀態的(第 1 章),伺服器不會「記住」連線。取而代之的是伺服器用 Set-Cookie 交付名牌(session ID 等),瀏覽器在之後每個請求的 Cookie 標頭附上它,伺服器就知道「是剛才那個人」。把密碼放進 URL 的方式會留在歷史紀錄與記錄檔中,是糟糕的做法;IP 位址會共用・變動,當不了識別子。
Q6. 關於 Content-Type 標頭,正確的說明是哪一個?
Content-Type 是「這個訊息的內文格式是這個」的附註,只要那一側帶有內文,雙向都會加。用 POST/PUT 送出表單或 JSON 時,請求側的 Content-Type 左右著請託的解讀,API 也可能看了它回傳 415 等。它不是 HTML 專用(JSON・圖片・PDF 都行),text 系還會用 charset 一併指定字元編碼。
本章帶回家的重點
- 標頭是往返書信的附註。決定本文如何被對待的資訊都聚集在這裡
- Host 是收件地址 — 支撐一個 IP 上多個網站同居的名稱式虛擬主機的必要標頭
- Content-Type(+ charset)是標籤。標籤與內容物不一致就是亂碼的入口。Content-Length 是內文的位元組數,告知訊息的分界
- Accept 系列是希望的表明(是協商而非強制)。Set-Cookie → Cookie 的名牌往返,在無狀態之上打造出 session
下一章是標頭的應用篇,也是 Web 速度的心臟 — 快取。「同樣的東西不搬運兩次」的條件與補充,用手算與模擬器來確認。