Fetch APIは「PATCH」だけ大文字と小文字の挙動が異なる
2022 年 6 月に Internet Explorer のサポートが終了してからというものの、ブラウザから利用できるウェブの API は日進月歩の勢いで進化しています。[1]
IE のサポート終了によって多くのブラウザで利用できるようになった機能の一つに fetch()
のメソッド名でも知られる Fetch API があります。これは Promise ベースの単純な API を利用して、 JavaScript からネットワークリクエストを行うことができる機能です。
以下のコードで基本的な利用方法を示します。次のコードは、筆者が用意したデモ用のサーバに対して GET
と POST
を使ってリクエストを送信します。[2]
/*----- GET リクエスト -----*/
let r1 = await fetch("https://fetch-api-normalization.deno.dev", {
method: "GET",
});
/*----- POST リクエスト -----*/
let r2 = await fetch("https://fetch-api-normalization.deno.dev", {
method: "POST",
headers: new Headers({
"Content-Type": "application/json",
}),
body: JSON.stringify({ message: "hello" }),
});
このコードを実行すると次のような結果を得られるでしょう。リクエストは正常に終了し、レスポンスボディの JSON 文字列をオブジェクトにパースできます。
Fetch API の簡単な利用例
この API を利用することで、 Axios や jQuery.ajax などのサードパーティーライブラリを使わずに REST API などのバックエンドサーバーとの通信を行うことができます。
さて、上のコードスニペットでは HTTP リクエストメソッド を大文字で記述しましたが、実は小文字で書き換えても問題なく動作します。試しに「GET」「POST」をそれぞれ「get」「post」にしてみます。
Fetch API はメソッド名を小文字にしても動く
他のリクエストメソッド、すなわち GET
・POST
・DELETE
・PUT
・OPTIONS
・HEAD
でも同様に大文字と小文字の区別は行われず、いずれの文字で書いてもリクエストは成功します。
Fetch API を利用したことがある方であれば、この挙動に遭遇したことがある方も少なくはないでしょう。文字の大小を区別しないでいてくれるのは、ケアレスミスを未然に防いでくれる便利な機能です。
なぜか仲間はずれにされる「PATCH」
しかし、リクエストメソッドとして「PATCH」を利用したときだけは大文字と小文字が区別されることはご存知でしょうか?
PATCH
はリソースを部分的に更新するためのリクエストメソッドで、例えばブログの投稿やソーシャルメディアのプロフィールなどを更新する REST API を作るときによく用いられます。実はこの PATCH
は前述の GET
や POST
と異なり、 Fetch API 内では特別に扱われているのです。
試しに次のコードスニペットを実行してみましょう。 Chrome を利用している場合は、 F12 を押して開発者ツールを開き、 Console タブに下記のコードを貼り付けてください。
const url = "https://fetch-api-normalization.deno.dev";
await fetch(url, { method: "PATCH" });
await fetch(url, { method: "patch" });
実行すると、次のようなエラーを得るはずです。
PATCH を小文字で書いた際のエラーの一例
さて、どのような条件でこのエラーが発生するのでしょうか?これが意図されたものなのだとしたら、 GET
や POST
は大文字・小文字を無視してよくて PATCH
は無視できない理由がなにかあるのでしょうか?以下でその理由を探ってみましょう。
いつエラーが発生するか
このエラーは、 Fetch API を利用して外部の HTTP サーバーに対してリクエストを行う時に、 PATCH
と書くべきところを patch
と書いていると発生します。[3][4]
また、クロスオリジンでのリクエスト、すなわちリクエストを行う対象のサーバーとリクエストを行っているオリジン(ウェブサイトのスキームとホスト名の組)が異なっていて、サーバー側で CORS ヘッダー Access-Control-Allow-Methods
を設定している場合は、ブラウザによってアクセスがブロックされ CORS エラーを得ます。
クロスオリジンでない場合は、リクエストは送信され得ますが、 405 Method Not Allowed など別種のエラーを得るでしょう。
しかし、前述の通り GET
・POST
・DELETE
・PUT
・OPTIONS
・HEAD
では大文字・小文字や CORS ヘッダーの有無に関わらずリクエストは正常に終了します。
なぜエラーが発生するか
ここまでで「PATCH」と「patch」は区別されること、どういった条件でどのようなエラーが得られるかを確認しました。それでは、どうしてこのような挙動をするのかを確認していきましょう。
まず、これは意図された挙動なのでしょうか?もしかすると Chrome を開発している Google のエンジニアが大文字と小文字の変換処理で PATCH
を列挙し忘れただけかもしれません。
ウェブで利用できる JavaScript、HTML、CSS などの API は W3C や WHATWG といった標準化団体によって文書化され、公開されています。
ウェブブラウザーを実装する開発者は基本的にこの標準をもとに仕様に書かれているとおりに実装しているので、ウェブの機能について疑問に思ったことがあれば、その元となる仕様を当たれば解決することがあります。
各ブラウザは標準化された仕様をもとに開発を進めている
そこで、 Fetch API を定義している仕様を読み、 fetch()
メソッドの第二引数に渡した method
プロパティがどのような経緯を経てリクエストとして処理されるのかを見ていきましょう。
fetch()
メソッドおよび Fetch API は WHATWG という標準化団体によって Fetch Standard として公開されており、細かい仕様や議論の経緯などは全てウェブ上で閲覧できるようになっています。
中でも、Fetch API のインターフェイスである fetch()
メソッドは同仕様内の 5. Fetch API という章で言及されています。
この章では、 JavaScript から提供されている fetch()
メソッドにおいて、各オプションをどのように解釈するべきなのかや、リクエストを表現する Request 、レスポンスを表現する Response 、 HTTP ヘッダーを簡単に処理するための Headers など、 Fetch API に付随するさまざまな API がそれぞれどのように振る舞いべきなのかが厳密に定義されています。
5. Fetch API
さて、この記述によると fetch(input, init)
の第二引数は RequestInit
と呼ばれ、その振る舞いについては 5.4 Request class で詳述されていることがわかります。
そこで、5.4 Request class をさらに読み進めてみましょう。後半部分では、 fetch()
のオプションとして受け取ったオブジェクトから Request
クラスをインスタンス化する手順が詳細に書かれています。その手順は 42 段階にも及びます。
5.4 Request class
このステップの 12 番目の記述によると、 Request
コンストラクタは RequestInit
で受け取った method
を「method」として解釈し Request
をインスタンス化することがわかります。ここで、「method」とは仕様の前半部分であらかじめ導入されている用語で、 2.1.1 Methods にその詳細があります。
2.1.1 Methods
ここまで辿るとようやく fetch
の第二引数に渡した method
がどのように処理されるのかを知ることができます。ここでは、下記のように述べられています。
メソッドを正規化するには、それ(メソッド)がバイト大小無視(byte-case-insensitive)で
DELETE
、GET
、HEAD
、OPTIONS
、POST
またはPUT
に合致するときバイト大文字化(byte-uppercase)する。[5]
この記述を言い換えるとすれば 「大文字・小文字を無視して DELETE
・GET
・HEAD
・OPTIONS
・POST
・PUT
に合致する文字列が渡された場合は、すべて大文字にするよ」 と言えるでしょう。ですから、全て小文字の場合以外にも Get
・GeT
・geT
などはいずれも GET
に正規化されることがわかります。
Fetch API は内部でデータを変換する
いま注目したいのは、 PATCH
が正規化の対象に含まれていないことです。正規化の対象に含まれていないということはすなわち、小文字で「patch」や「Patch」と書いた場合は、大文字に変換されることなくそのままサーバーに送信されることを意味しています。実際に、Chrome Devtools の Network パネルを開くと、小文字で patch
と指定した時は小文字のままリクエストが行われているのを確認できます。
小文字の「patch」でリクエストを行った場合は小文字のまま送信されている
なお 2.1.1 Methods でも言及されていますが、小文字のリクエストメソッドが HTTP として誤っているというわけではありません。 HTTP の最新の仕様である RFC 9110 では「全てのメソッドは大文字・小文字を区別する」と述べられており、これは最初期のバージョンから変更されていない記述のうちの一つです。[6][7]
ただし、HTTP ではメソッド名として任意の文字列を使うことができるため、小文字で patch
と書いた場合は HTTP の仕様で定義されている標準のメソッドにはならず、いわば「カスタムメソッド」として扱われてしまいます。この場合、仮にサーバーがリソースを提供していたとしても PATCH に期待するものと同じであることは保証できないでしょう。[8]
なぜこんな仕様なのか
さて、ここまでで「PATCH」と「patch」で振る舞いがことなることはブラウザのバグではなく、れっきとした仕様の一部であることを見てきました。しかし依然として、「PATCH」を正規化の対象に含めていない理由について疑問が残ります。
この点について、前述した Fetch Standard で正規化について定義している箇所に補足があります。
実際はメソッドの大文字・小文字は区別されますが、後方互換性と API 間の一貫性 のために正規化(normalization)を行っています[9]
ここで「後方互換性」とは、古い仕様に基づいて作られたウェブサイトが新しい仕様の策定後も問題なく利用できることです。1990 年代にホームページビルダーを用いて制作された 阿部寛さんのホームページ は私たちが使っている最新版のウェブブラウザーでも問題なく利用できますが、これは HTML が互換性の維持を重視し、古いウェブサイトが動かなくなるような変更を避け続けてきたからに他なりません。[10]
ですが、 Fetch Standard の策定が始まったのは 2014 年で、同年 10 月の時点で既に正規化についての記述があります。つまり、 Fetch API は WHATWG が新しく作成した API だったのにも関わらず、初期の時点から後方互換性の維持のために正規化を定義していたことになります。[11]
新しく作られた API なのに、どうして最初から互換性のことを考えているのでしょうか? これを理解するためには、 Fetch Standard が策定された経緯と、その役割を正しく理解する必要があります。
Fetch API 以前のリクエスト
Fetch API が最初から互換性を気にしていたということは、きっとそれ以前のウェブの歴史に何か原因があるに違いありません。そこで、ウェブの歴史を遡って Fetch 以前にどのようにリクエストが行われてきたかを見ていきます。
Fetch API が登場する以前は、ブラウザから JavaScript を使ってリクエストを送信するのには XMLHttpRequest (略称:XHR)という API が用いられていました。かつてブラウザからリクエストを行うライブラリとして覇権を握っていた Axios や jQuery.ajax などのサードパーティのライブラリも、内部では XMLHttpRequest を使って実装されています。[12][13]
XMLHttpRequest は Promise ベースでないことや、レスポンスをストリームできないこと、同期的にリクエストを送れることなどを除くと概ね Fetch API と同様の機能を提供しています。
ここで気になるのは、 XHR において PATCH の正規化はどのようにハンドリングされていたのかということです。 Fetch API と同じように PATCH を除いて全て大文字に変換していたのでしょうか?
XHR での PATCH の振る舞い
次のコードを実行して、実際に XHR で PATCH を小文字で利用してみましょう。
const xhr1 = new XMLHttpRequest();
xhr1.open("get", "https://fetch-api-normalization.deno.dev");
xhr1.send();
const xhr2 = new XMLHttpRequest();
xhr2.open("patch", "https://fetch-api-normalization.deno.dev");
xhr2.send();
結果は次のようになります。小文字の「get」は成功しましたが、小文字の「patch」は失敗してしまいました。これは Fetch API と全く同じ振る舞いです。
XHR を使って小文字の「get」「patch」でリクエストする
もちろん XMLHttpRequest と Fetch API は全く別の API であり、新しい機能として提供された Fetch API が XHR と同じ振る舞いをする必要は必ずしもありません。しかし、 Fetch API の振る舞いは先に提供されていた XHR を真似したように見えます。そこで、次に XHR と Fetch Standard の関係性を見ていきます。
Fetch Standard の真の目的
ここまで何度も参照してきた Fetch Standard ですが、実はメソッドfetch()
について定義しているのは第 1 章から第 6 章まであるうちの第 5 章の部分だけです。
他の部分では何が書かれているのでしょうか?それは、リファラーをどう処理するか、クロスオリジンだったときはどうするか、 Content Security Policy をどのように処理するのか、といったウェブにおける普遍的なリクエスト全般についてです。[14]
Fetch Standard がこのような構成になっている経緯について、当時 WHATWG の Mozilla の代表であり、 Fetch Standard の策定にも関わった Anne van Kesteren 氏は自身のブログで次のように述べています。[15]
ウェブプラットフォームには URL を利用する機能がたくさんあります。しかし、その URL からリソースを取得する際のセマンティクスはあまりよく定義されていません。
このような機能の相互依存性を減らし、新しい機能を定義する際に簡素化するために、私は Fetch Standard を策定しました。 [16]
Fetch Standard が策定される以前はウェブ上でリソースを取得する際の振る舞いは各機能ごとに定義されており、その定義も曖昧でした。これを一元化することで、ウェブ標準を簡素化し振る舞いを明確にすることを目的として策定されました。
Fetch Standard と他のウェブテクノロジー
このように、 Fetch Standard は実は fetch()
メソッドを提供するのみではなく、ウェブ上でリソースを取得すること全般(fetching)について述べた仕様だったのです。実際に、現在の <img>
要素、@font-face
や background-image
などの CSS プロパティの仕様を読むと、 Fetch Standard を参照していることがわかります。[17][18]
ウェブのリクエストを処理する仕様は Fetch Standard を参照している
fetch()
はその仕組みを JavaScript からでも呼び出せるようにした、いわば「おまけ」であり、その振る舞いは他のウェブ機能がリクエストを処理するときに利用するものを再利用しています。[19]
Fetch Standard と XMLHttpRequest
さて、ここで XHR の話に戻ります。van Kesteren 氏が先に述べたように、 Fetch Standard は全ての「リクエスト」に関わる仕様から共通して参照される仕様になりました。それは XHR も含み、最新の XHR の仕様は Fetch Standard を参照するようになっています。[20]
後発の仕様である Fetch Standard によって、昔からあった XMLHttpRequest が定義し直され、その基底にあるテクノロジーは画像の取得やスクリプトの読み込み、そして fetch()
と同じものに共通化されたと言えるでしょう。
その過程で Fetch Standard の側で XHR の「PATCH を除いたメソッド名を大文字に変換する」という癖を吸収しなければならなかったというわけです。これが先に述べた「後方互換性」と「API 間の一貫性」の正体でした。[21]
なぜ XHR は PATCH を正規化しなかったのか
待ってください。 Fetch API が GET や POST を正規化している理由が XHR にあることはわかりましたが、じゃあその XHR はどうして PATCH を正規化しなかったんでしょうか?
これには XHR が開発された時期が関係しています。 XHR は Microsoft がかつて開発していた Internet Explorer 5.0 で独自に実装された機能でした。かつてウェブは今ほど標準化、文書化が進んでおらず、各ブラウザベンダーが独自に新機能を実装しては、他のブラウザがそれに追従するといったことが日常茶飯事でした。
そんな Internet Explorer 5.0 に XHR が実装されたバージョンがリリースされたのは 2000 年 でした。[22] ここで、当時の最新だった HTTP の仕様は RFC 2616 です。これは HTTP/1.1 とも呼ばれているバージョンで、初めて Host
ヘッダーが追加され、同じ IP アドレスから複数のドメインをホストできるようになったバージョンでもありました。[23]
そんな HTTP/1.1 で定義されていた初期の HTTP リクエストメソッドは GET
, POST
, HEAD
, PUT
, DELETE
, OPTIONS
, TRACE
, CONNECT
の 8 種類で、その中に PATCH
は含まれていませんでした。実は PATCH
が正式に追加されたのは 2010 年に標準化された RFC 5789 でのことで、これは XHR が実装された IE5 がリリースされた時から 10 年も後だったのです。
PATCH は IE5 より後に仕様に加わった
したがって、XHR が追加された IE5 がリリースされた時点では PATCH
は存在せず、正規化する必要もなかったということです。この独自実装であった XHR はのちに W3C によって文書化されますが、その際も既存の実装に従った仕様が策定されました。[24]
なお、 Fetch Standard についての議論が行われている GitHub リポジトリでは、RFC 5789 に倣って PATCH も正規化の対象に含めるように提案する issue が作成されていますが、 van Kesteren 氏は「本来 HTTP メソッドには大文字と小文字の両方が利用できること」や「小文字の patch と大文字の PATCH を区別して利用していたウェブサイトがある可能性がゼロではないこと、したがって正規化が既存のウェブサイトの挙動を破壊しうること」などを理由にこれを却下しています。[25]
まとめ
この記事では、 Fetch API で「PATCH」と「patch」の挙動が異なるという事象から、ウェブ標準の読み方、 Fetch Standard が策定された経緯、 HTTP およびウェブの歴史について見てきました。
まず「PATCH」と「patch」が別の文字列として扱われるのは、 WHATWG が策定したウェブ標準の1つである Fetch Standard という仕様の正規化の定義によるものだとわかりました。また、本来 HTTP においてメソッド名の大文字と小文字は区別されるものであり、小文字の「patch」をそのまま送信した場合にはいわば「カスタムメソッド」として扱われることを確かめました。
次に、このような仕様になっている歴史的な経緯を確認していきました。 Fetch Standard はウェブにおけるリソースの取得について普遍的に定義したものであり、 fetch()
メソッドで知られる Fetch API はその仕様の一部であること、 <img>
要素や background-image
などの URL を扱う他の機能が Fetch Standard を参照していることを学び、かつて Fetch API と同等の機能を提供していた XMLHttpRequest という API の挙動に揃えるために PATCH 以外の HTTP リクエストメソッドの正規化が導入されたことを確かめました。
最後に、どうして XMLHttpRequest が PATCH の正規化を行わなかったかを学びました。 XMLHttpRequest は Microsoft が Internet Explorer 5.0 で独自に実装した機能が元になっており、開発時点で最新版の HTTP の仕様では、標準メソッドとして PATCH が定義されていなかったこと、のちの 2010 年に PATCH が標準化されたことを確かめました。
参考
- 入門 HTML5 - Mark Pilgrim 著、矢倉 眞隆 監訳、水原 文 訳
https://www.oreilly.co.jp/books/9784873114828 - Fetch Standard(日本語訳)
https://triple-underscore.github.io/Fetch-ja.html - XMLHttpRequest とはなんだったのか - blog.jxck.io
https://blog.jxck.io/entries/2022-09-30/XMLHttpRequest.html - The Case of The Somewhat Case-Sensitive HTTP Method - brendanforster.com
https://brendanforster.com/2013/10/the-case-of-the-somewhat-case-sensitive-http-method
-
https://learn.microsoft.com/en-us/lifecycle/products/internet-explorer-11 ↩︎
-
ここでは説明のためにリクエストメソッド GET を明示したが、本来は省略できる。 ↩︎
-
"Using
patch
is highly likely to result in a405 Method Not Allowed
.PATCH
is much more likely to succeed." https://fetch.spec.whatwg.org/#methods ↩︎ -
言うまでもないが、もしリクエスト対象のサーバーが小文字の patch に対してリソースを提供している場合はリクエストが成功する。 ↩︎
-
Fetch Standards 2.1.1 Methods より抄訳 https://fetch.spec.whatwg.org/#methods ↩︎
-
"The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names." https://datatracker.ietf.org/doc/html/rfc9110#section-9.1 ↩︎
-
"The Method token indicates the method to be performed on the resource identified by the Request-URI. The method is case-sensitive." https://datatracker.ietf.org/doc/html/rfc1945#section-5.1.1 ↩︎
-
"There are no restrictions on methods.
CHICKEN
is perfectly acceptable (and not a misspelling ofCHECKIN
)" https://fetch.spec.whatwg.org/#methods ↩︎ -
Normalization is done for backwards compatibility and consistency across APIs as methods are actually "case-sensitive". https://fetch.spec.whatwg.org/#methods を抄訳 ↩︎
-
"In particular, it should be possible to process existing HTML documents as HTML 5 and get results that are compatible with the existing expectations of users and authors, based on the behavior of existing browsers." https://www.w3.org/TR/html-design-principles/#compatibility ↩︎
-
https://web.archive.org/web/20141014195913/https://fetch.spec.whatwg.org/ ↩︎
-
https://github.com/axios/axios/blob/a48a63ad823fc20e5a6a705f05f09842ca49f48c/lib/adapters/xhr.js#L77 ↩︎
-
https://github.com/jquery/jquery/blob/ace646f6e83e653f666ba715c200739f1cbdba52/src/ajax/xhr.js#L6 ↩︎
-
"The Fetch standard defines requests, responses, and the process that binds them: fetching." https://fetch.spec.whatwg.org/#:~:text=日本語-,Abstract,-The Fetch standard ↩︎
-
https://github.com/whatwg/sg#steering-group-representatives ↩︎
-
"Fetch the image: Fetch request." https://html.spec.whatwg.org/multipage/images.html#updating-the-image-data ↩︎
-
"Fetch req, with processresponseconsumebody set to processResponse." https://www.w3.org/TR/css-values-4/#url-processing ↩︎
-
"The Fetch Standard also defines the fetch() JavaScript API, which exposes most of the networking functionality at a fairly low level of abstraction." https://fetch.spec.whatwg.org/#infrastructure ↩︎
-
"The XMLHttpRequest object is an API for fetching resources." https://xhr.spec.whatwg.org/#introduction ↩︎
-
自分のリサーチでは「XHR の側で正規化を定義して、fetch では一切の正規化をしない」というようにしなかった理由が「API 間の一貫性」以外に見つけられなかった。fetch 以前は CSP と CORS が別々の仕様で定義されたりしていて、ネットワークリクエストに関する記述が四分五裂していたことがあり、それを避けたかったのかと理解している。 ↩︎
-
"The reality is that the client architecture of GMail appears to follow the rough design of the Exchange 2000 implementation of Outlook Web Access for IE5 and later which shipped way back in 2000." https://web.archive.org/web/20090130092236/http://www.alexhopmann.com/xmlhttp.htm ↩︎
-
"Thanks to the Host header, the ability to host different domains from the same IP address allowed server collocation." https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP#http1.1_–_the_standardized_protocol ↩︎
-
"HTTP verbs are case-sensitive. That we normalize a couple of them is already, strictly speaking, against the rules, but we have to do so for compatibility." https://github.com/whatwg/fetch/issues/50#issuecomment-188241506 ↩︎
Discussion