本篇 GET vs. POST,主要介紹 GET 與 POST 的差異、性質,及各自 HTML 表單用法。
並解答常見的問題:『GET 真的不具備 酬載 (payload) 嗎?』、
『上傳檔案 時常用的 multipart/form-data 其格式、編碼為何?』…,
以延伸出 HTTP 的精髓 — 語意差異 (Semantic Difference)。
目錄
GET
GET 是最常見的 HTTP 方法:
傳輸 目標資源 目前的 表示 (representation)。
Transfer a current representation of the target resource.
GET 可想成是動詞的 獲取、查詢 或 檢索 (retrieval),
就像在說 給我 『https://notfalse.net』 這個 目標資源,
於是 Server 傳回此 資源 目前的 『表示』 — — 網頁 (html) ❗️
當有人說: 藉由 HTTP 上網、檢索、看片…,
絕大多數都是使用 GET 方法!
[包括你現在 看到 (GET) 此頁面]
因此:
GET 做為資訊檢索的主要機制,
幾乎是所有效能優化的重點。
屬性 (property)
安全性: Yes
冪等性: Yes
可快取性: Yes
除非有 Cache-Control 表頭欄位指定,
GET 請求 的 回應是 可快取的,
可用於滿足後續等效的 GET 和 HEAD 請求,以提升效能。
詳見 (4-1) 請求方法、安全性、冪等性 與 (6-1) 快取 (Cache)。
參數 與 表單 (parameter & form)
如 (4-1) 請求方法、安全性、冪等性 所述:
對於 GET 請求訊息,酬載 (payload) 是 未定義的語意,
在 GET 請求上發送 酬載,可能導致一些 Server 拒絕該請求。
使用 GET方法,需提供額外的 參數 (parameter) [pəˋræmətɚ] 時,
透過將表單欄位值 進行 百分比編碼 (Percent-Encoding),
並以 鍵值對 (key-value) 的方式,置於 URI 的 查詢 (query) 部分:
https://echo.paw.cloud:443/hello/world?age=24&gender=female
詳見 (2-1) 統一資源識別符 (URI)。
使用 GET 表單,即在 <form>
標籤 的 method 屬性中設置 "get" (此為預設選項,可不設置):
<form action="https://echo.paw.cloud" method="get">
Search: <input type="search" name="search"> <input type="submit" value="Submit">
</form>
放在 網址列 不就被看光光了? 😱
是的,因此 敏感資料,不應 使用 GET。
[註1]:
儘管 HTML 與 許多程式語言,常以 小寫字母 代稱,
實際送出訊息時,請求方法 仍為 大寫 字母。
回應 (response)
幾乎所有 (4-6) 回應狀態碼 都可能在 GET 請求 的回應中被接收。
以下舉一些情境為例,不代表所有可能性:
範圍 (range)
Client 可以使用 Range 表頭欄位,
將 GET 語義用於 範圍請求 (range request),
使 Server 僅傳輸所選 表示 (representation) 的一部分。
(e.g., 大型文件 的單個頁面)
Server 若藉由請求中的 Range 表頭欄位,
成功獲取 並傳輸 一或多個滿足該範圍的 目標資源 之 表示 (representation),
則回應 206 (Partial Content) — 部分內容。
詳見 (4-6) 回應狀態碼 (Response Status Codes)。
POST
POST 是僅次於 GET,最常見的 HTTP 方法:
對請求的 酬載 (payload),執行 特定 資源的 處理。
Perform resource-specific processing on the request payload.
POST 一般可想成是動詞的 遞交、新增 或 發佈,
但是仍須 依資源特性決定語意 (e.g., post message 可能是 發送 訊息)。
例如:
- 遞交 一個 資料區塊 (表單)。
- 在 公告欄、討論區 或 部落格…,發佈 訊息 or 文章。
- 新增 或 建立 資源至 Server (e.g., 常見的上傳檔案):
(需使用 enctype=”multipart/form-data”)<form action="https://echo.paw.cloud" method="post" enctype="multipart/form-data" > <input type="file" name="file"><input type="submit" value="Submit"> </form>
屬性 (property)
安全性: No
冪等性: No
可快取性: Yes
唯有在 回應 包含明確的 新鮮度 資訊時,
POST 請求 的 回應才可以被 快取,
儘管 POST 的快取 沒有被廣泛實現。
詳見 (4-1) 請求方法、安全性、冪等性 與 (6-1) 快取 (Cache)。
參數 與 表單 (parameter & form)
不同於 GET 將參數放置於 URI query 部分,
POST 方法 主要將 參數 [pəˋræmətɚ] 置於 訊息主體 (message body) 中。
若 表單欄位 為帳號、密碼,使用 POST 即不會出現於 URI 之中 😆,
因此 相對 GET 表單 安全 (當然,也能同時使用 URI 的 query 部分)。
使用 POST 表單 (form),需於 <form>
標籤的 method 屬性中設置 post:
<form action="https://echo.paw.cloud" method="post">
Search: <input type="search" name="search"> <input type="submit" value="Submit">
</form>
但是,只使用 POST 方法 仍不安全,
若無使用 HTTPS,中介/中間人 (intermediaries) 能輕易的查看、修改內容,
此外,還必須考慮到 CSRF 攻擊:
另外,一定要記住:
表單 (form),只是 請求方法 (e.g., GET、POST) 的一種應用,
例如: 平時瀏覽頁面,即是使用 GET 請求,此時並沒有表單存在!
Content-Type (內容類型)
POST 表單,有三種 編碼方式 (enctype),
藉此將 表單資料 轉換為 酬載,並置放於 訊息主體 中:
- application/x-www-form-urlencoded (預設)
- multipart/form-data
- text/plain
HTML 表單 在提交前,會根據 enctype 屬性 對表單進行編碼:
<form action="/" method="post" enctype="multipart/form-data"> ....
並置於 Content-Type (內容類型) 表頭,以告知 Server 表示 (representation) 的 媒體類型:
content-type: multipart/form-data;
然而,這僅是 POST 表單 的範疇:
不代表 POST 方法 僅能使用這三種 Content-Type,
表示 (representation),可以是任意類型 (e.g., application/octet-stream、application/json)。
例如,本篇開頭之範例 — Content-Type: application/json; charset=utf-8:
POST /task?id=1 HTTP/1.1
Host: echo.paw.cloud
Content-Type: application/json; charset=utf-8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0
Connection: close
Content-Length: 136
{
"status": "ok",
"extended": true,
"results": [
{"value": 0, "type": "int64"},
{"value": 1.0e+3, "type": "decimal"}
]
}
application/x-www-form-urlencoded
當 <form>
標籤 的 method 屬性設置為 "post",
application/x-www-form-urlencoded 即為預設的 編碼方式 (enctype)。
application/x-www-form-urlencoded 與 GET 表單非常相似,
透過將表單欄位值 進行 百分比編碼 (Percent-Encoding) 成 鍵值對 (key-value) 的形式。
不同的是,POST 將參數置於 訊息主體 中 (而非 URI 的 query 部分)。
且 沒有 URI query 的「?」開頭,
編碼時 空白的部分 應以「+」代換 百分比編碼 的「%20」。
(原先內容若有「+」,則編碼為「%2B」)
例如:
即送出:
POST / HTTP/1.1
Host: echo.paw.cloud
Content-Type: application/x-www-form-urlencoded; charset=utf-8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0
Connection: close
Content-Length: 38
name=Jason+Zheng&motto=948794%E7%8B%82
實務範例
如 (2-1) 統一資源識別符 (URI) 所述:
開發時,需 記得處理 query 字串 (尤其是 客戶端應用),
以免送出保留字元或錯誤的編碼。
當然,也包含 POST 時的酬載啦!
以 APP 欲傳送 user 大頭照網址 至 Server 為例,
若無使用 HTML 表單 (Form) 幫助編碼,
且 Content-Type 表頭欄位 為 application/x-www-form-urlencoded,
需 自行 使用 百分比編碼 (以 Java 為例):
@Test
public void encodeTest() throws UnsupportedEncodingException {
// user 大頭照網址
String imgUrl = "https://d3ojx0qwvsjea2.cloudfront.net" +
"/wp-content/uploads/2017/05/23110017/its-you.jpg";
// 編碼結果
String result;
try {
result = URLEncoder.encode(imgUrl, "utf-8");
} catch (UnsupportedEncodingException e) {
result = e.toString();
}
println(result);
}
Result:
https%3A%2F%2Fd3ojx0qwvsjea2.cloudfront.net%2Fwp-content%2Fuploads%2F2017%2F05%2F23110017%2Fits-you.jpg
請求訊息:
POST / HTTP/1.1
Host: echo.paw.cloud
Content-Type: application/x-www-form-urlencoded; charset=utf-8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0
Connection: close
Content-Length: 129
img_url=https%3A%2F%2Fd3ojx0qwvsjea2.cloudfront.net%2Fwp-content%2Fuploads%2F2017%2F05%2F23110017%2Fits-you.jpg
使用 JavaScript 處理酬載也是大同小異,
你或許會想使用 犀牛本 作者 David 寫的簡易函式 — encodeFormData
:
var data = {name: "中勝", city: "Taipei"};
var encodedData = encodeFormData(data);
console.log(encodedData);
function encodeFormData(data) {
if (!data) return ""; // Always return a string
var pairs = []; // To hold name=value pairs
for (var name in data) { // For each name
if (!data.hasOwnProperty(name)) continue; // Skip inherited
if (typeof data[name] === "function") continue; // Skip methods
var value = data[name].toString(); // Value as string
name = encodeURIComponent(name.replace(" ", "+")); // Encode name
value = encodeURIComponent(value.replace(" ", "+")); // Encode value
pairs.push(name + "=" + value); // Remember name=value pair
}
return pairs.join('&'); // Return joined pairs separated with &
}
Result:
name=%E4%B8%AD%E5%8B%9D&city=Taipei
multipart/form-data
上傳檔案 時需使用 multipart/form-data。
就像 「URI 的 query」 或 「application/x-www-form-urlencoded」,以 「&」做為分隔符號,
應用於 「多個部分」的 multipart 也需使用 分隔符號 — — 邊界 (Boundary)。
邊界 (Boundary) 是由 「--」、「value」、「CRLF」構成,
value 的內容由不大重要,確保不會在資料中出現即可,
通常由 瀏覽器 產生,例如:
----9487QQ
並做為 Content-Type 的參數,置於其值之後,
告知 Server,邊界 (分隔符號) 的內容:
Content-Type: multipart/form-data; boundary=----9487QQ
以擁有 「2 個不同類型」的表單 為例:
<form action="https://echo.paw.cloud" method="post" enctype="multipart/form-data">
File: <input type="file" name="file"><br>
username: <input type="text" name="username"><br>
<input type="submit" value="Submit">
</form>
送出一張圖片:「apple.jpg」,並輸入 username:「Jason」:
POST / HTTP/1.1
Host: echo.paw.cloud
Content-Type: multipart/form-data; boundary=----9487QQ
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0
Connection: close
Content-Length: 3170
----9487QQ
Content-Disposition: form-data; name="file"; filename="apple.jpg"
Content-Type: image/jpeg
(影像位元組)
----9487QQ
Content-Disposition: form-data; name="username"
Jason
----9487QQ--
如上例,每個部分 皆以 邊界 (boundary) 包住 (開始 與 結束),
包含 必填的 Content-Disposition (內容配置) 子表頭欄位,用以辨識此欄位名稱、檔案名稱:
Content-Disposition: form-data; name="欄位名稱";
與 可選的 Content-Type (內容類型) 子表頭欄位,預設值為 text/plain,依媒體類型而更動:
Content-Type: type/subtype
並且,最後的 邊界 (boundary) 結尾需再加上「--」,說明 酬載 的結束。
需注意,儘管稱為 子表頭,
其依然在 訊息主體 (message body) 中。
text/plain
顧名思義,text/plain 是指純文字,
相較另外兩者,較少使用。
與 application/x-www-form-urlencoded 相似,
將參數以 鍵值對 (key-value) 的形式,置於 訊息主體 中 (而非 URI 的 query 部分),
且 沒有 URI query 的「?」開頭。
(少數 User Agent 甚至不進行任何處理)
不同的是,text/plain 並 不會 對表單欄位值進行 百分比編碼,
而 分隔符號 (delimiter) 也並非「&」,而是 CRLF (\r\n)。
(少數 User Agent 會將空白代換為「+」)
例如,此 POST 表單 (以 text/plain 編碼):
<form action="https://echo.paw.cloud" method="post" enctype="text/plain" >
Hello <input type="text" name="Hello" value=", World!"><br>
Foo <input type="text" name="Foo" value="罷"><br>
<input type="submit" value="Submit">
</form>
請求 訊息主體 (message body) 為:
Hello=, World!
Foo=罷
ps. 相同的表單 若以 application/x-www-form-urlencoded 編碼,則為:
Hello=%2C+World%21&Foo=%E7%BD%B7
回應 (response)
排除 206 (Partial Content)、304 (Not Modified)、416 (Range Not Satisfiable) 之外,
幾乎所有 回應狀態碼 (Response Status Codes) 都可能在 POST 請求 的回應中被接收。
以下僅一些情境為例,不代表所有可能性:
新增
例如,以 POST 請求 成功在 源服務器 (Origin Server) 上建立了一或多個資源,
則 源伺服器 應發送一個 201 (Created) — 建立 的回應,
且包含一個 Location 表頭欄位,指出建立的主要資源的 URI,
以及引用新資源時,描述請求狀態的 表示 (representation)。
以 表示 呈現結果
如果處理 POST 請求 的結果,等同於某個存在資源的 表示 (representation),
則 源伺服器 可以通過在 Location 表頭欄位 中,
發送具有該資源的 辨識符 (URI) 的 303 (See Other) — 參閱其他 回應,
將 使用者代理 (User Agent) 重新導向到該資源。
這樣做的好處是,可以將與 POST 回應相對應的資訊,
單獨辨識、增加書籤 和 快取,而不依賴於 原始請求。
如此,便能以更佳的方式 傳輸 表示 (representation),
若 使用者代理 (User Agent) 尚未擁有該 表示 的快取,
則以額外的請求為代價,否則共享快取。
儘管大部分實作,僅使用 200 (OK) (包括我 😂)。
GET vs. POST
除了上述的種種屬性、耳熟能詳的 GET 沒有訊息主體;POST 有…,
接著將說明 GET 與 POST 訊息的「真正」差異之處:
這是一個 GET 請求範例:
GET /?id=1 HTTP/1.1
Host: echo.paw.cloud
Content-Type: application/json; charset=utf-8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0
Connection: close
Content-Length: 136
{
"status": "ok",
"extended": true,
"results": [
{"value": 0, "type": "int64"},
{"value": 1.0e+3, "type": "decimal"}
]
}
這是一個 POST 請求範例:
POST /?id=1 HTTP/1.1
Host: echo.paw.cloud
Content-Type: application/json; charset=utf-8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0
Connection: close
Content-Length: 136
{
"status": "ok",
"extended": true,
"results": [
{"value": 0, "type": "int64"},
{"value": 1.0e+3, "type": "decimal"}
]
}
唯一的差別僅在於 請求行 的 方法處,分別為 GET 與 POST。
(廢話 😂)
咦 !? GET 請求不是 "沒辦法" 放置 酬載嗎 ?
非也 !
語意差異 (Semantic Difference)
事實上,排除 TRACE 方法 外,HTTP 並無 嚴格限制 「請求方法 不准攜帶 酬載」,
換句話說,只要 Server 支援,一樣能在 GET 訊息 附加酬載送出。
(儘管這是不建議的做法)
TCP/IP 送出去的就是這一連串字串,而之所以對待它們的方式相差甚遠,
即是在於 — — 方法語意 (Method Semantic)。
// 詳見 (4-5) 以 C Socket 實作 HTTP Client
其精髓正是 (1-3) 統一介面 所述之 REST 介面約束 :
自我描述的訊息 (self-descriptive messages)
HTTP/1.1 的主架構師 — Dr. Roy Thomas Fielding:
Web 應用,依賴於多個 Client 對 語意 的正確實作,
因此,Server 能將 Web 應用之 (會話) 狀態 置於 Client (e.g., Cookie),
以減少對一致應用行為的控制。
Q: 依賴語意/規則 是 REST 的獨到之處嗎?
A: No,這正是所有 介面 (interface) 在做的事。
再宏觀一點,你所接觸到的數位科技,都是 1 0 1 0…
只是用不同指令、文字、像素…等方式,表示出含義,
掌握這點不僅能快速上手 HTTP,更對 Coding 技巧大有助益 😆。