將 HTTP URL 路徑中的 // 合併為 / 並不是正規化。
如果這是由來源端或被授權代表來源端重寫 URL 的方執行,這可能被視為正典化或重寫,就像將 /sillyclownwithnonstandardname/ 重寫為 /%F0%9F%A4%A1/(/🤡/)一樣,如果是由來源端執行,這樣的正典化或重寫是正確的。
RFC 3986 定義了路徑組件及其段落語法,允許空段落存在。因此,雙斜線在語法上是有意義的,它代表兩個分隔符之間的零長度段落。
路徑組件包含資料,通常以階層形式組織,與非階層的查詢組件(第 3.4 節)一起,用於識別 URI 範圍內的資源。路徑以第一個問號("?")或井號("#")字元終止,或以 URI 結尾終止。
如果 URI 包含權限組件,則路徑組件必須為空或以斜線("/")字元開始。如果 URI 不包含權限組件,則路徑不能以兩個斜線字元("//")開始。此外,URI 參考(第 4.1 節)可能是相對路徑參考,此時第一個路徑段不能包含冒號(":")字元。ABNF 規則需要五個獨立規則來消除這些情況的歧義,且在給定 URI 參考中只有一個規則會匹配路徑子字串。我們使用通用術語「路徑組件」來描述解析器匹配到這些規則之一的 URI 子字串。
路徑由一系列由斜線("/")分隔的路徑段組成。URI 總是定義有路徑,儘管該路徑可能為空(長度為零)。斜線字元用於指示階層結構僅在 URI 用作相對參考上下文時是必要的。例如,URI mailto:[email protected] 的路徑是 "[email protected]",而 URI foo://info.example.com?fred 的路徑為空。
路徑段 "." 和 "..",也稱為點段,用於路徑名稱階層中的相對參考。它們用於相對路徑參考的開頭,以指示在階層樹中的相對位置,類似於某些作業系統檔案目錄結構中分別表示當前目錄和父目錄的角色。然而,與檔案系統不同,這些點段只在 URI 路徑階層中解釋,並在解析過程中被移除(第 5.2 節)。
除了階層路徑中的點段外,路徑段被通用語法視為不透明。
因為 segment = *pchar,空字串是有效的段落。因此 path-abempty = *( "/" segment ) 允許斜線後接空段。任何將 // 合併為 / 的轉換都會移除一個語法上有效的段落,從而改變解析後的段落序列。
HTTP(RFC 9110)使用 RFC 3986 的路徑語法作為請求目標。
URI 參考用於定位請求、指示重定向及定義關係。
「URI-reference」、「absolute-URI」、「relative-part」、「authority」、「port」、「host」、「path-abempty」、「segment」和「query」的定義均採自 URI 通用語法。為可包含非空路徑組件的協議元素定義了 "absolute-path" 規則(此規則與 RFC 3986 的 path-abempty 規則略有不同,後者允許空路徑,還有 path-absolute 規則不允許以 "//" 開頭的路徑)。為可包含相對 URI 但不包含片段組件的協議元素定義了 "partial-URI" 規則。
HTTP URI 的來源伺服器由權限組件識別,該組件包含主機識別符([URI] 第 3.2.2 節)及可選的埠號([URI] 第 3.2.3 節)。如果埠子組件為空或未指定,則預設為 TCP 埠 80(WWW 服務保留埠)。
階層路徑組件及可選查詢組件用於識別該來源伺服器命名空間中的目標資源。
合併 // 會改變段落序列,從而改變識別符。除非來源明確定義這兩個識別符等同,否則一般正規化器無權這麼做。只有來源端能在其命名空間內修改 URI。
RFC 3986 明確定義語法基礎的正規化包括:大小寫正規化、百分比編碼正規化及點段移除。它未列出任何移除空段或合併多重斜線的規則。
實作可根據本規範定義的邏輯來降低誤判的機率。此處理成本略高於字元逐一比較。例如,應用程式可合理地將以下兩個 URI 視為等同:
網頁使用者代理(如瀏覽器)通常在判斷是否有快取回應時會套用此類 URI 正規化。語法基礎的正規化包括大小寫正規化、百分比編碼正規化及點段移除。
路徑正規化範圍相當狹窄:僅針對相對參考中的 . 和 ..,不包括空段。
完整的路徑段 "." 和 ".." 僅用於相對參考(第 4.1 節),並在參考解析過程中被移除(第 5.2 節)。然而,一些已部署的實作錯誤地假設當參考已是 URI 時不需解析,因而未移除非相對路徑中的點段。URI 正規化器應依第 5.2.4 節所述,對路徑套用 remove_dot_segments 演算法移除點段。
值得注意的是,規則中沒有允許移除空段或合併重複分隔符的指令。
HTTP 增加了少數基於協議的正規化規則,且範圍仍相當狹窄。唯一涉及路徑的是空路徑組件(非路徑內空段):
4.2.3. http(s) 正規化與比較
帶有 "http" 或 "https" 協議的 URI 根據 [URI] 第 6 節定義的方法進行正規化與比較,並使用上述各協議的預設值。
HTTP 不要求使用特定方法判定等價。例如,快取鍵可能在語法基礎正規化後以簡單字串比較,或在協議基礎正規化後比較。
"http" 和 "https" URI 的協議基礎正規化([URI] 第 6.2.3 節)包含以下規則:
若埠號等於協議預設埠,則正規形式省略埠子組件。
當非 OPTIONS 請求目標時,空路徑組件等同於絕對路徑 "/",因此正規形式提供 "/" 路徑。
協議與主機名稱不區分大小寫,通常以小寫提供;其他組件則區分大小寫。
非「保留」字元等同於其百分比編碼八位元組,正規形式是不編碼它們(參見 [URI] 第 2.1 與 2.2 節)。
再次強調,規則中不包含合併路徑內的 //。
RFC 3986 的路徑語法明確允許空段(segment = *pchar)。因此路徑中的 // 在語法上有效,對應明確的空段。
通用語法宣告,除點段外,路徑段為不透明。合併 // 改變段序列,改變不透明資料,這超出正規化應有範圍。
HTTP 使用 RFC 3986 的路徑定義於 HTTP(S) URI,並指出階層路徑組件識別來源命名空間內的資源。也就是說,除非常有限的正規化規則外,精確的路徑字串是識別符的一部分。
RFC 3986 與 RFC 9110 的正規化規則不允許合併路徑內重複斜線。唯一允許的路徑相關正規化是點段移除(通用 URI)及空路徑轉為 /(HTTP)。
因此,在 HTTP URL 路徑段中將 // 合併為 / 並非正確正規化。除非來源明確定義兩者等同,否則此操作會產生不同且不等價的識別符。
例如,https://git.runxiyu.org/furweb.git// 與 https://git.runxiyu.org/furweb.git/ 根據標準語法與正規化規則是不同的識別符,且不可由通用正規化器重寫;事實上,這兩個 URL 提供不同內容。
有時在路徑不同部分間使用分隔符是有用的。
例如,考慮以下兩個例子:
在 URL 嵌入任意階層結構的地方,如群組路徑與 Git 參考,明確的分隔符有助於區分路徑不同部分。第二行使群組路徑與參考的結尾變得模糊(注意 Git 參考可能是像 runxiyu/fix-router 這樣的檔案路徑,不會有空段)。
nginx 的 merge_slashes 功能適合用於提供檔案系統服務;但作為反向代理時不一定合適,除非與來源端協議一致。
Go 語言的 net/http.ServeMux 與 path.Clean;注意 filepath.Clean 是用於檔案路徑的,而 path.Clean 廣泛用於 URL 處理相關程式碼中。(這也是 Lindenii 專案採用自訂 net/http 分支的原因。)
我也記得 Apache 在某些配置中有類似行為,但目前無法找到引用且無法自行驗證。