我們知道,Django 提供了專屬的 HttpRequest 類別,把從前端(主要為瀏覽器)傳過來的 HTTP 請求重新封裝成 OOP 物件,方便操作、使用。

而這個HttpRequest物件,也就是我們開發時,前端傳給 view 函式的第一個位置參數——request

我們經常使用它,在 view 函式(或類別)內部獲得本次 HTTP 請求的相關資訊,比如所使用的 HTTP 方法,或下面提到的 header 相關資訊等等。

1
2
3
4
if request.method == 'GET':
do_something()
elif request.method == 'POST':
do_something_else()

場景需求:取得特定 reqeust header 內容

在開發內部 API 相關功能時,一個很常見的需求,就是取得本次請求所帶的「驗證 token」 內容,加以重新封裝成一個新的請求,並轉發給內部 API。

通常 token 會附在請求的 header 中,以下列鍵值對形式存在:

1
2
3
4
5
6
...
accept-encoding: gzip, deflate, br
accept-language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6
cache-control: max-age=0
...
Authorization: Bearer <token_string>

其中Authorization,就是我們要的,有關驗證 token 的 header 欄位。

所以我們的需求是:原封不動地取得它的值——「'Bearer <token_string>'」,並複製到我們重新封裝的 HTTP 請求的 header 中,以維持認證的有效性。

以往做法:使用 reqeust.META

過去撰寫內部 API 的相關請求,驗證 token 部分通常是透過上述「reqeust.META」取得 request header 裡的 token 值。

申言之,HttpRequest有一個物件屬性叫META,其值為一個 Python 字典,並以字典中的每一個鍵值對代表 HTTP header 相關資訊。

典型的 HTTP header 欄位名稱看起來像這樣:

  • Accept-Language – List of acceptable human languages for response.
  • Authorization – Authentication credentials for HTTP authentication.

但實際上,META字典中的鍵名,並非上述格式,因為 request.META 在封裝 HTTP 請求的 header 時,會對「欄位名稱」進行格式轉換(mapping),規則如下:

  1. 加上HTTP_前綴
  2. 轉為全大寫
  3. -替換_(底線)。

So, for example, a header called X-Bender would be mapped to the META key HTTP_X_BENDER.

因此,程式中取值的寫法差不多都是這樣,以獲得驗證 token 為例(注意名稱):

1
headers = {'Authorization': request.META.get('HTTP_AUTHORIZATION')}

request.META 缺點

我覺得大致有下:

  1. Header 欄位名稱被強制轉換,與原生的 HTTP header 並不相同。
    1. 比如Accept-LanguageHTTP_ACCEPT_LANGUAGE
    2. 這樣的轉換雖然遵循一定規律,但畢竟和原來不同,所以並不直觀
  2. META這個詞,太籠統太抽象:request.META究竟代表什麼?說真的第一時間你不太能和 HTTP header 聯想,多少會影響程式的可讀性

所以我一直不太喜歡「request.META」這個寫法——不易理解且存在較多出錯可能。

更好的做法:使用 reqeust.headers

其實仔細想想就會覺得:使用META來取 header 資訊的做法實在很不直覺、不自然,對於一個成熟的框架,理論上應該要再給出「語法糖」加以簡化。

Django 2.2 更新

這個需求終於在 Django 2.2 獲得實現:加入了 request.headers。Django 文件也建議你,如果只是想單純取得 header 資訊,可以使用更簡潔的headers

HttpRequest.headers is a simpler way to access all HTTP-prefixed headers

HttpRequest.headers特色

  1. 依舊為 Python 字典格式。
  2. Header 鍵名維持原來的名稱,不再進行轉換。
  3. 使用鍵名取值時,無須區分大小寫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> request.headers
{'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6',..}

>>> 'User-Agent' in request.headers
True
>>> 'user-agent' in request.headers
True

>>> request.headers['User-Agent']
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)
>>> request.headers['user-agent']
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)

>>> request.headers.get('User-Agent')
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)
>>> request.headers.get('user-agent')
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)

這個「不區分大小寫」的設計,主要是考量 header 欄位名稱的大小寫格式十分多樣,尤其是自訂的 header 名稱,根本沒有一定的標準。

使用 reqeust.headers 後的改善

可以看看原來程式碼的寫法變化:

Before

1
headers = {'Authorization': request.META.get('HTTP_AUTHORIZATION')}

After

1
headers = {'Authorization': request.headers.get('Authorization')}

改變雖小,卻直觀了很多!

換句話說,其實就是改善META既有的缺點:

  1. HTTP header 鍵名不必強制前綴HTTP_全大寫了!只要使用原來的名稱即可,這才是符合人類直覺得設計!至於不區分大小寫我覺得還好,畢竟最好還是使用一致的命名,如上述程式碼,比較不易混淆。
  2. headers這個屬性名稱比META更加直觀、可讀,容易聯想到 HTTP headers。

小結:可讀性為王

簡潔、直觀、易讀,都是 Clean Code 的重要元素。

因此,哪怕只是很小的改動,我仍然強烈建議你:在不影響原有需求的前提下,盡可能使用request.headers取代request.META

無時無刻遵守好習慣,你的隊友——包括未來的自己——會感謝現在的你。