小村軟體有限公司
第 4 章

HTTP 標頭與內容 — 傳什麼、用什麼格式

標頭是往返書信的附註。閱讀作為收件地址的 Host、表示格式與字元編碼的 Content-Type、表示數量的 Content-Length、傳達希望的 Accept 系列,以及在無狀態之上打造 session 的 Cookie 與 Set-Cookie。

本文明明是對的,卻莫名變亂碼・回來的東西不一樣

HTML 明明寫對了,日文卻化成像「譁�蟄�」的樣子。明明向 API 送了 JSON,卻被以「格式不正確」拒絕。明明是同一個 URL,不同人卻看到不同語言的頁面 — 這類「本文明明是對的」系的問題,答案多半不在本文,而在標頭

標頭是附在往返書信上、名稱: 值 形式的附註。用書信來比喻,就是收件人・「易碎品」的標籤・重量的標示・回信用的名牌 — 雖然不是本文本身,但決定本文該如何被對待的資訊都聚集在這裡。這就是 3 點組合的第 3 點「條件與補充」。

標頭有數百種,但不需要背。本章只閱讀回答「寄給誰・是什麼有多少・帶著什麼希望・來自誰」的代表性 6 個。

顯示 HTTP 的往返書信附有收件地址的 Host、格式的 Content-Type、數量的 Content-Length、希望的 Accept、名牌的 Cookie 等附註的圖

標頭是附在本文上的標籤。收件地址(Host)・內容物的標示(Content-Type / Content-Length)・希望(Accept 系列)・名牌(Cookie / Set-Cookie)— 依職責捆起來看,數百種標頭也不再可怕。

Host — 一個地址,多個門牌

請求的必要標頭 Host: example.com,是信件的收件人。你也許會想「既然已經連上 IP 位址,收件人不是不言自明嗎?」,但實際上在 1 台伺服器(1 個 IP 位址)上,讓 www.example.comshop.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.comshop.example.com 這 2 個網站。能讓伺服器判別請求寄給哪個網站的線索標頭是哪一個?

Q2. 回傳以 UTF-8 儲存的日文 HTML 的回應,帶著 Content-Type: text/html; charset=Shift_JIS 這個標頭。會發生什麼事?

Q3. 回應標頭 Content-Length: 1256 的正確意義是哪一個?

Accept 系列 — 傳達希望,請對方挑選

同一個 URL,想要的形式也因對象而異。瀏覽器想要 HTML,程式想要 JSON。對日文使用者想回傳日文頁面。傳達這種「希望」的,就是 Accept 系列的請求標頭。

Accept: application/json
「可以的話請給 JSON」— 格式的希望
Accept-Language: ja
「可以的話請給日文」— 語言的希望
Accept-Encoding: gzip
「壓縮後再送來也沒關係」— 傳輸方式的希望

伺服器從可提供的表現形式中挑選回傳。這就是內容協商(content negotiation)。重點在於這是協商而非強制 — 希望也可能不被採納,實際回來的是什麼,要看回應側的 Content-Type 等確認。「同一個 URL,不同人看到的卻不一樣」的謎底之一就在這裡。

Cookie 與 Set-Cookie — 在無狀態之上打造 session

來回收第 1 章的伏筆。HTTP 是無狀態的,伺服器不記得「剛才的後續」。那登入狀態是怎麼實現的 — 答案是 2 個標頭的連動。

Step 1 — 伺服器發行名牌
在登入成功的回應附上 Set-Cookie: session=abc123。「之後請出示這面名牌」
Step 2 — 瀏覽器保管名牌
瀏覽器把 Cookie 記憶為該網站專用
Step 3 — 之後的請求每次附上
對同一網站的請求會自動帶上 Cookie: session=abc123。伺服器看了名牌就知道「是剛才那個人」

換句話說,session 就是在無狀態的往返書信的每一封信上,持續附上同一面名牌所打造出來的「連續劇」。伺服器記住的只有「名牌 abc123 是誰」這張對應表,HTTP 本身從頭到尾都是無狀態的 — 讀懂這個構圖,「刪了 Cookie 就被登出」「換一個瀏覽器就被當成未登入」這些日常現象就全都能解釋。

練習 4-2 — 傳達希望・掛上名牌

Accept 系列的內容協商,以及 Cookie 打造 session 的機制。

Q4. 在請求上加 Accept: application/jsonAccept-Language: ja 的「內容協商」,正確的說明是哪一個?

Q5. 關於登入的機制,正確的說明是哪一個?

Q6. 關於 Content-Type 標頭,正確的說明是哪一個?

本章帶回家的重點

  • 標頭是往返書信的附註。決定本文如何被對待的資訊都聚集在這裡
  • Host 是收件地址 — 支撐一個 IP 上多個網站同居的名稱式虛擬主機的必要標頭
  • Content-Type(+ charset)是標籤。標籤與內容物不一致就是亂碼的入口Content-Length 是內文的位元組數,告知訊息的分界
  • Accept 系列是希望的表明(是協商而非強制)。Set-Cookie → Cookie 的名牌往返,在無狀態之上打造出 session

下一章是標頭的應用篇,也是 Web 速度的心臟 — 快取。「同樣的東西不搬運兩次」的條件與補充,用手算與模擬器來確認。