HTTP

HTTP GET vs POST

本篇 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" (此為預設選項,可不設置):

Search: (可測試 並觀察網址列的變化)
<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:

Search: (可測試 並觀察網址列 沒有 變化)
<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」)
 
例如:

name:
motto:

 
即送出:

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 個不同類型」的表單 為例:

File:
username:

<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 編碼):

Hello
Foo

<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"} ] }

 
 
唯一的差別僅在於 請求行 的 方法處,分別為 GETPOST
(廢話 😂)
 
 
 
咦 !? 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) 在做的事。
 
DIP-electricity-ex
 
再宏觀一點,你所接觸到的數位科技,都是 1 0 1 0…
只是用不同指令、文字、像素…等方式,表示出含義,
掌握這點不僅能快速上手 HTTP,更對 Coding 技巧大有助益 😆。

 
 

作者: 鄭中勝

喜愛音樂,但不知為何總在打程式 ?
期許能重新審視、整理自身所學,幫助有需要的人。

發表迴響