🌊

Fetch APIは「PATCH」だけ大文字と小文字の挙動が異なる

2023/10/06に公開

2022 年 6 月に Internet Explorer のサポートが終了してからというものの、ブラウザから利用できるウェブの API は日進月歩の勢いで進化しています。[1]

IE のサポート終了によって多くのブラウザで利用できるようになった機能の一つに fetch() のメソッド名でも知られる Fetch API があります。これは Promise ベースの単純な API を利用して、 JavaScript からネットワークリクエストを行うことができる機能です。

以下のコードで基本的な利用方法を示します。次のコードは、筆者が用意したデモ用のサーバに対して GETPOST を使ってリクエストを送信します。[2]

Fetch API の基本的な利用方法
/*----- 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 文字列をオブジェクトにパースできます。

上記のコードスニペットを実行した Chrome DevTools の Console タブのスクリーンショット。リクエストが正常に終了していることがわかる。
Fetch API の簡単な利用例

この API を利用することで、 AxiosjQuery.ajax などのサードパーティーライブラリを使わずに REST API などのバックエンドサーバーとの通信を行うことができます。

さて、上のコードスニペットでは HTTP リクエストメソッド を大文字で記述しましたが、実は小文字で書き換えても問題なく動作します。試しに「GET」「POST」をそれぞれ「get」「post」にしてみます。

上記のコードスニペットのうち、メソッド名を小文字にした Chrome DevTools の Console タブのスクリーンショット。リクエストが正常に終了していることがわかる。
Fetch API はメソッド名を小文字にしても動く

他のリクエストメソッド、すなわち GETPOSTDELETEPUTOPTIONSHEAD でも同様に大文字と小文字の区別は行われず、いずれの文字で書いてもリクエストは成功します。

Fetch API を利用したことがある方であれば、この挙動に遭遇したことがある方も少なくはないでしょう。文字の大小を区別しないでいてくれるのは、ケアレスミスを未然に防いでくれる便利な機能です。

なぜか仲間はずれにされる「PATCH」

しかし、リクエストメソッドとして「PATCH」を利用したときだけは大文字と小文字が区別されることはご存知でしょうか?

PATCH はリソースを部分的に更新するためのリクエストメソッドで、例えばブログの投稿やソーシャルメディアのプロフィールなどを更新する REST API を作るときによく用いられます。実はこの PATCH は前述の GETPOST と異なり、 Fetch API 内では特別に扱われているのです。

試しに次のコードスニペットを実行してみましょう。 Chrome を利用している場合は、 F12 を押して開発者ツールを開き、 Console タブに下記のコードを貼り付けてください。

「PATCH」と「patch」を用いたリクエスト
const url = "https://fetch-api-normalization.deno.dev";

await fetch(url, { method: "PATCH" });
await fetch(url, { method: "patch" });

実行すると、次のようなエラーを得るはずです。

Chrome DevTools の Console タブのスクリーンショット。「Apatch https://fetch-api-normalization.deno.dev/ net::ERR_ABORTED 405 (Method Not Allowed)」というエラーが赤い文字で表示されている
PATCH を小文字で書いた際のエラーの一例

さて、どのような条件でこのエラーが発生するのでしょうか?これが意図されたものなのだとしたら、 GETPOST は大文字・小文字を無視してよくて PATCH は無視できない理由がなにかあるのでしょうか?以下でその理由を探ってみましょう。

いつエラーが発生するか

このエラーは、 Fetch API を利用して外部の HTTP サーバーに対してリクエストを行う時に、 PATCH と書くべきところを patch と書いていると発生します。[3][4]

また、クロスオリジンでのリクエスト、すなわちリクエストを行う対象のサーバーとリクエストを行っているオリジン(ウェブサイトのスキームとホスト名の組)が異なっていて、サーバー側で CORS ヘッダー Access-Control-Allow-Methods を設定している場合は、ブラウザによってアクセスがブロックされ CORS エラーを得ます。

クロスオリジンでない場合は、リクエストは送信され得ますが、 405 Method Not Allowed など別種のエラーを得るでしょう。

しかし、前述の通り GETPOSTDELETEPUTOPTIONSHEAD では大文字・小文字や CORS ヘッダーの有無に関わらずリクエストは正常に終了します。

なぜエラーが発生するか

ここまでで「PATCH」と「patch」は区別されること、どういった条件でどのようなエラーが得られるかを確認しました。それでは、どうしてこのような挙動をするのかを確認していきましょう。

まず、これは意図された挙動なのでしょうか?もしかすると Chrome を開発している Google のエンジニアが大文字と小文字の変換処理で PATCH を列挙し忘れただけかもしれません。

ウェブで利用できる JavaScript、HTML、CSS などの API は W3CWHATWG といった標準化団体によって文書化され、公開されています。

ウェブブラウザーを実装する開発者は基本的にこの標準をもとに仕様に書かれているとおりに実装しているので、ウェブの機能について疑問に思ったことがあれば、その元となる仕様を当たれば解決することがあります。

WHATWGのメンバーを示す棒人間が仕様書を策定し、ブラウザベンダーを示す各ブラウザのロゴがそれを参照していることを示すイラスト。WHATWGは「よく読んでね」と、ブラウザは「はーい」とそれぞれ言っている。
各ブラウザは標準化された仕様をもとに開発を進めている

そこで、 Fetch API を定義している仕様を読み、 fetch() メソッドの第二引数に渡した method プロパティがどのような経緯を経てリクエストとして処理されるのかを見ていきましょう。

fetch() メソッドおよび Fetch API は WHATWG という標準化団体によって Fetch Standard として公開されており、細かい仕様や議論の経緯などは全てウェブ上で閲覧できるようになっています。

https://fetch.spec.whatwg.org

中でも、Fetch API のインターフェイスである fetch() メソッドは同仕様内の 5. Fetch API という章で言及されています。

この章では、 JavaScript から提供されている fetch() メソッドにおいて、各オプションをどのように解釈するべきなのかや、リクエストを表現する Request 、レスポンスを表現する Response 、 HTTP ヘッダーを簡単に処理するための Headers など、 Fetch API に付随するさまざまな API がそれぞれどのように振る舞いべきなのかが厳密に定義されています。

Fetch Standard の 5. Fetch API のスクリーンショット
5. Fetch API

さて、この記述によると fetch(input, init) の第二引数は RequestInit と呼ばれ、その振る舞いについては 5.4 Request class で詳述されていることがわかります。

そこで、5.4 Request class をさらに読み進めてみましょう。後半部分では、 fetch() のオプションとして受け取ったオブジェクトから Request クラスをインスタンス化する手順が詳細に書かれています。その手順は 42 段階にも及びます。

Fetch Standard の 5.4 Request class のスクリーンショット
5.4 Request class

このステップの 12 番目の記述によると、 Request コンストラクタは RequestInit で受け取った method を「method」として解釈し Request をインスタンス化することがわかります。ここで、「method」とは仕様の前半部分であらかじめ導入されている用語で、 2.1.1 Methods にその詳細があります。

Fetch Standard の 2.1.1. Methods のスクリーンショット
2.1.1 Methods

ここまで辿るとようやく fetch の第二引数に渡した method がどのように処理されるのかを知ることができます。ここでは、下記のように述べられています。

メソッドを正規化するには、それ(メソッド)がバイト大小無視(byte-case-insensitive)で DELETEGETHEADOPTIONSPOST または PUT に合致するときバイト大文字化(byte-uppercase)する。[5]

この記述を言い換えるとすれば 「大文字・小文字を無視して DELETEGETHEADOPTIONSPOSTPUT に合致する文字列が渡された場合は、すべて大文字にするよ」 と言えるでしょう。ですから、全て小文字の場合以外にも GetGeTgeT などはいずれも GET に正規化されることがわかります。

Fetch API のデータ加工処理を模したパイプに棒人間が「Request」と書かれたボールを入れている図。
Fetch API は内部でデータを変換する

いま注目したいのは、 PATCH が正規化の対象に含まれていないことです。正規化の対象に含まれていないということはすなわち、小文字で「patch」や「Patch」と書いた場合は、大文字に変換されることなくそのままサーバーに送信されることを意味しています。実際に、Chrome Devtools の Network パネルを開くと、小文字で 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 を小文字で利用してみましょう。

XHRを用いた「get」と「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 と全く同じ振る舞いです。

Chrome DevTools の Console パネルから XHR を使って小文字の patch を送信する画面のスクリーンショット。赤い文字で 405 Method Not Allowed のエラーが表示されている。
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-facebackground-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 年も後だったのです。

ウェブの歴史を表したタイムライン形式の図。左からHTTP/1.0の策定(1999年6月)、IE5リリース(2000年)、PATCH追加(2010年、IE5から10年後!!)
PATCH は IE5 より後に仕様に加わった

したがって、XHR が追加された IE5 がリリースされた時点では PATCH は存在せず、正規化する必要もなかったということです。この独自実装であった XHR はのちに W3C によって文書化されますが、その際も既存の実装に従った仕様が策定されました。[24]

なお、 Fetch Standard についての議論が行われている GitHub リポジトリでは、RFC 5789 に倣って PATCH も正規化の対象に含めるように提案する issue が作成されていますが、 van Kesteren 氏は「本来 HTTP メソッドには大文字と小文字の両方が利用できること」や「小文字の patch と大文字の PATCH を区別して利用していたウェブサイトがある可能性がゼロではないこと、したがって正規化が既存のウェブサイトの挙動を破壊しうること」などを理由にこれを却下しています。[25]

https://github.com/whatwg/fetch/issues/50

まとめ

この記事では、 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 が標準化されたことを確かめました。

参考

脚注
  1. https://learn.microsoft.com/en-us/lifecycle/products/internet-explorer-11 ↩︎

  2. ここでは説明のためにリクエストメソッド GET を明示したが、本来は省略できる。 ↩︎

  3. "Using patch is highly likely to result in a 405 Method Not Allowed. PATCH is much more likely to succeed." https://fetch.spec.whatwg.org/#methods ↩︎

  4. 言うまでもないが、もしリクエスト対象のサーバーが小文字の patch に対してリソースを提供している場合はリクエストが成功する。 ↩︎

  5. Fetch Standards 2.1.1 Methods より抄訳 https://fetch.spec.whatwg.org/#methods ↩︎

  6. "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 ↩︎

  7. "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 ↩︎

  8. "There are no restrictions on methods. CHICKEN is perfectly acceptable (and not a misspelling of CHECKIN)" https://fetch.spec.whatwg.org/#methods ↩︎

  9. Normalization is done for backwards compatibility and consistency across APIs as methods are actually "case-sensitive". https://fetch.spec.whatwg.org/#methods を抄訳 ↩︎

  10. "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 ↩︎

  11. https://web.archive.org/web/20141014195913/https://fetch.spec.whatwg.org/ ↩︎

  12. https://github.com/axios/axios/blob/a48a63ad823fc20e5a6a705f05f09842ca49f48c/lib/adapters/xhr.js#L77 ↩︎

  13. https://github.com/jquery/jquery/blob/ace646f6e83e653f666ba715c200739f1cbdba52/src/ajax/xhr.js#L6 ↩︎

  14. "The Fetch standard defines requests, responses, and the process that binds them: fetching." https://fetch.spec.whatwg.org/#:~:text=日本語-,Abstract,-The Fetch standard ↩︎

  15. https://github.com/whatwg/sg#steering-group-representatives ↩︎

  16. https://annevankesteren.nl/2013/05/fetching-urls より抄訳 ↩︎

  17. "Fetch the image: Fetch request." https://html.spec.whatwg.org/multipage/images.html#updating-the-image-data ↩︎

  18. "Fetch req, with processresponseconsumebody set to processResponse." https://www.w3.org/TR/css-values-4/#url-processing ↩︎

  19. "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 ↩︎

  20. "The XMLHttpRequest object is an API for fetching resources." https://xhr.spec.whatwg.org/#introduction ↩︎

  21. 自分のリサーチでは「XHR の側で正規化を定義して、fetch では一切の正規化をしない」というようにしなかった理由が「API 間の一貫性」以外に見つけられなかった。fetch 以前は CSP と CORS が別々の仕様で定義されたりしていて、ネットワークリクエストに関する記述が四分五裂していたことがあり、それを避けたかったのかと理解している。 ↩︎

  22. "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 ↩︎

  23. "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 ↩︎

  24. https://www.w3.org/TR/2006/WD-XMLHttpRequest-20060405/ ↩︎

  25. "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