























這幾天在處理公司的第三方 API 介接,其中有一個部分是將 token 放在 HTTP header 裡面當作彼此驗證的方式,雖然這不是什麼特別的方式,但卻在我使用 Ruby 處理時掉進了時空裂縫。
主要規格是在 HTTP header 裡面放置 api_key: 'token' 進行驗證請求。
這裡我使用了幾種常見的 http request 封裝 Library
全部都會發生驗證失敗的情況,一致性的提到 API KEY 沒有夾帶,但在我開啟 Log 模式時,非常確定有發送。
突然覺得是不是學 Ruby 錯了 XD
於是乎很納悶的我打算換個程式語言來做交叉測試,就嘗試用 node.js 來發送請求至 API server,結果如預期,是可以的。
為了找到問題,我又花了一些時間起了一個 server 在 local 端,將請求往本機發送時發現,無論使用上述哪一個套件,其最後都會將 HTTP header 中夾帶的 api_key 轉為 api-key
然後我就開始看這些套件的 source code,發現在處理的過程中會把 header 做字串的替換,其真正原因是因為 HTTP header 其實在 RFC 中有明確的協議規範。
以 RestClient 這個套件來說,在處理 header 上方就有詳細的註解
# Convert headers hash into canonical form.
#
# Header names will be converted to lowercase symbols with underscores
# instead of hyphens.
#
# Headers specified multiple times will be joined by comma and space,
# except for Set-Cookie, which will always be an array.
#
# Per RFC 2616, if a server sends multiple headers with the same key, they
# MUST be able to be joined into a single header by a comma. However,
# Set-Cookie (RFC 6265) cannot because commas are valid within cookie
# definitions. The newer RFC 7230 notes (3.2.2) that Set-Cookie should be
# handled as a special case.
#
# http://tools.ietf.org/html/rfc2616#section-4.2
# http://tools.ietf.org/html/rfc7230#section-3.2.2
# http://tools.ietf.org/html/rfc6265
在 http 這個封裝請求的套件也有看到
# Matches HTTP header names when in "Canonical-Http-Format"
CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/.freeze
# Matches valid header field name according to RFC.
# @see http://tools.ietf.org/html/rfc7230#section-3.2
COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#\$%&'*+\-.^_`|~]+\z/.freeze
接著我去看了 RFC 2616 4.2 的章節部分,提到
Request (section 5) and Response (section 6) messages use the generic message format of RFC 822 [9] for transferring entities (the payload of the message)
請求和回應的部分採用 RFC 822 中的通用格式來進行傳輸。
接著看 RFC 822 3.1.2 的部分,對於格式說明有提到
The field-name must be composed of printable ASCII characters (i.e., characters that have values between 33. and 126., decimal, except colon).
也就是說,關於 HTTP 中的 Header 可以由可輸出的 ASCII 字符來組成,括號中提到可以使用 10 進制在 33 至 126 的字符,不包含冒號。
這裡不含冒號可以理解,因為通常 field name 和 value 會以冒號作為分割。
當我們查詢 ASCII 表可以發現,其實底線 _ (underscore) 的 10 進制值為 95,其實是在規範的區間內,也就是說在 HTTP header 使用底線是「合法」的。
那為什麼大多數的 Library 亦或是 Web Server 如 nginx / apache 都是默認不支援的呢?
是因為這是一個關於 CGI 的歷史遺留問題,無論是 underscore _ 還是 dash - 都會轉為 CGI 系統變數的底線 _,這樣可能會有混淆的問題,詳細可以查看 RFC 3875 4.1.18
所以為了避免這樣的問題,通用的法則就是不要在 HTTP header 中設計帶有底線 _ 的 Field name
基本上常用的 web server 例如 nginx / apache 都是預設將 http header 中帶有底線 _ 的參數給丟掉的。
當 API 設計時使用 X_API_TOKEN 時,可能會遇到從 application 發送到 web server 時卻不見的情況。
雖然有可以 workaround 的解法,但都要特別去處理
underscores_in_headers on從前面兩個小節可以看到,如果不按照規範來做設計,可能會遇到 Application 在實作時,遇到大多數 HTTP request 封裝的套件無法支援 http header 夾帶 underscore 的參數,必須自己做 monkey patch(到底誰才是 monkey?)
然後等你好不容易解決了這段,卻又遇到往 web server 丟消失的情況,最後再去爬文件設定,會不會有那麼一點累?
如果遇到介接的廠商,開這個規格給你,請他改。
如果對方不改,可以丟 RFC 協議或是本篇文章給他看 XD
除非對方真的有什麼非這樣設計不可的理由,不然會建議設計時遵循規範比較不會被雷到。
在 Ruby 使用的部分,多數的 HTTP request 封裝 gem 都會是繼承原本的 net/http library。
所以你會發現,無論你在這個 gem 如何 patch header,他到下面一層 dependence 都還是會再次處理。
我自己的解法是使用 http 這個 gem 來做修改,因為這套 gem 在處理 request 的部分沒有去依賴 net/http
以下解法供參考處理 HTTP header 中處理底線請求
先安裝 gem
gem "http"
解法是在啟動時先打一層 monkey patch
module DontNormalizeUnderscoreHeaders
def normalize_header(name)
return name if name.include?('_')
super
end
end
module HTTP
class Headers
prepend DontNormalizeUnderscoreHeaders
end
end
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。