フロントエンド開発における大文字小文字の感度の問題#
大文字小文字の感度(case sensitivity)は、ソフトウェア開発の分野におけるテーマであり、同じ「単語」のスペルの大文字と小文字の違いが異なる効果を引き起こす可能性があるシナリオを指します。
次に、フロントエンド開発の分野における一般的な大文字小文字の感度 / 非感度のシナリオについて話しましょう:
- HTTP ヘッダー
- HTTP メソッド
- URL
- クッキー
- E メールアドレス
- HTML5 タグと属性名
- CSS プロパティ
- Git におけるファイル名
HTTP ヘッダーは大文字小文字を区別しますか?#
HTTP ヘッダーの名前フィールドは大文字小文字を区別しません。
RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1)#section-3.2 では次のように規定されています:
各ヘッダーフィールドは、大文字小文字を区別しないフィールド名とコロン(":")、オプションの先頭空白、フィールド値、およびオプションの末尾空白で構成されます。
ただし、HTTP/2 には追加の制限があり、ヘッダー圧縮が追加されたため、エンコード前に小文字に変換する必要があります。
RFC 7540 - Hypertext Transfer Protocol Version 2 (HTTP/2)#section-8.1.2 では次のように規定されています:
ただし、ヘッダーフィールド名は、HTTP/2 でのエンコード前に小文字に変換する必要があります。
現在では、ほとんどのクライアントと HTTP サーバーは、HTTP ヘッダーの名前フィールドを自動的に小文字に統一し、使用者が再度大文字小文字の変換処理を行う必要がないようにしています。
例えば、NodeJS の HTTP モジュールは自動的にヘッダーフィールドを小文字に変更します node/_http_outgoing.js at main · nodejs/node · GitHub:
// lib/_http_outgoing.js
ObjectDefineProperty(OutgoingMessage.prototype, '_headers', {
__proto__: null,
get: internalUtil.deprecate(function() {
return this.getHeaders();
}, 'OutgoingMessage.prototype._headers is deprecated', 'DEP0066'),
set: internalUtil.deprecate(function(val) {
if (val == null) {
this[kOutHeaders] = null;
} else if (typeof val === 'object') {
const headers = this[kOutHeaders] = ObjectCreate(null);
const keys = ObjectKeys(val);
// パフォーマンス上の理由から for(;;) ループを保持
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0; i < keys.length; ++i) {
const name = keys[i];
headers[StringPrototypeToLowerCase(name)] = [name, val[name]];
}
}
}, 'OutgoingMessage.prototype._headers is deprecated', 'DEP0066')
});
PS. この記事を書いているときに、NodeJS のこの部分のドキュメントに誤りがあることに気づき、サンプルのヘッダー取得に大文字が含まれていたため、PR を提案し、現在はマージされています。doc: fix typo in http.md by airingursb · Pull Request #43933 · nodejs/node · GitHub
さらに、Rust の HTTP モジュールもデフォルトでヘッダーのフィールド名を小文字に変更します。理由は、HeaderMap の処理がより迅速になるためです。
ドキュメントには http::header - Rust に次のように記載されています:
HeaderName
型は、標準のヘッダー名とカスタムのヘッダー名の両方を表します。この型は、ヘッダー名の大文字小文字を区別しない性質を処理し、HeaderMap
のキー部分として使用されます。ヘッダー名は小文字に正規化されます。言い換えれば、文字列でHeaderName
を作成する際に、大文字が含まれていても、HeaderName
の文字列表現を取得するとすべて小文字になります。これにより、HeaderMap
の比較操作がより迅速になります。
ソースコードは次のとおりです:src/headers/name.rs - http 0.1.3 - Docs.rs
フロントエンド開発者としては、Chromium と WebKit のこの部分の処理に特に注目します。
リクエストヘッダーについて、Chrome で実験を行います:
var ajax = new XMLHttpRequest();
ajax.open("GET", "https://y.qq.com/lib/interaction/h5/interaction-component-1.2.min.js");
ajax.setRequestHeader("X-test", "AA");
ajax.send();
HTTP2 のリクエストについて、パケットキャプチャを行うと、ここでの X-test
が小文字の x-test
に書き換えられていることがわかります:
厳密を期すために、Chrome に付属の netlog-viewer を使用してパケットキャプチャを行ったところ、リクエストヘッダーが確かに小文字に変換されていることが確認できました:
注:上記のテストは Safari でも同様の結果が得られますが、パケットキャプチャを行う必要があります。Safari のコンソールで見るだけでは、大文字小文字の表示に問題があり、Chrome の Devtools では問題ありません。これは Safari のバグではないかと推測しています。
そこで、Blink の XMLHTTPRequest のソースコードを読みましたが、どこで小文字に変換されているのかは見つかりませんでした。
また、XMLHTTPRequest の標準を調べたところ、XMLHttpRequest Standard ではヘッダーフィールド名を小文字に変更する必要があるとは記載されていませんでした。
XHR 標準の中には、XHR でヘッダーフィールド名を小文字に変換することを提案した人もいました(Should XHR store and send HTTP header names in lower case? · Issue #34 · whatwg/xhr · GitHub)が、却下されました。
規範に従えば、HTTP2 のヘッダー名はすべて小文字に変換する必要がありますが、HTTP1.1 のサイトでテストしてみました:
var ajax = new XMLHttpRequest();
ajax.open("get", "http://www.cn1t.com/airing.js");
ajax.setRequestHeader("X-test", "AA");
ajax.send();
ここではヘッダーフィールド名が小文字に変換されないことがわかります:
リクエストヘッダーのフィールド名の大文字小文字の変換は XMLHTTPRequest が行うのではなく、基盤となるネットワークライブラリのロジックであることがわかります。
PS. ここで長い間デバッグしましたが、どの部分で小文字に変換されているのかは見つけられませんでした。知っている方がいれば、ぜひコメントで教えていただけると幸いです。
一方、レスポンスヘッダーのフィールド名は異なります。XMLHTTPRequest の getAllResponseHeaders
などのメソッドを使用してレスポンスヘッダーを取得すると、Blink はヘッダーの名前フィールドを小文字に変換します:
String XMLHttpRequest::getAllResponseHeaders() const {
// ...
for (const auto& header : headers) {
string_builder.Append(header.first.LowerASCII());
string_builder.Append(':');
string_builder.Append(' ');
string_builder.Append(header.second);
string_builder.Append('\r');
string_builder.Append('\n');
}
// ...
}
さらに、Chromium はネットワークレスポンスパケットを解析する際、HTTP2 プロトコルを使用している場合、大文字のヘッダーフィールド名が見つかると、ネットワークパケットの解析に失敗します:
absl::optional<ParsedHeaders> ConvertCBORValueToHeaders(
const cbor::Value& headers_value) {
// |headers_value| のヘッダーはマップである必要があります。
if (!headers_value.is_map())
return absl::nullopt;
ParsedHeaders result;
for (const auto& item : headers_value.GetMap()) {
if (!item.first.is_bytestring() || !item.second.is_bytestring())
return absl::nullopt;
base::StringPiece name = item.first.GetBytestringAsString();
base::StringPiece value = item.second.GetBytestringAsString();
// 名前に大文字または非ASCII文字が含まれている場合、エラーを返します。
// これは [RFC7540] のセクション 8.1.2 の要件に一致します。
if (!base::IsStringASCII(name) ||
std::any_of(name.begin(), name.end(), base::IsAsciiUpper<char>))
return absl::nullopt;
// ...他
}
return result;
}
HTTP メソッドは大文字小文字を区別しますか?#
規範 RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1)#section-3.1.1 によれば:
メソッドトークンは、ターゲットリソースに対して実行されるリクエストメソッドを示します。リクエストメソッドは大文字小文字を区別します。
HTTP メソッドは大文字小文字を区別し、すべて大文字である必要があります。
ただし、XMLHttpRequest インスタンスに小文字のメソッドを渡しても問題ありません(例えば ajax.open("get", "https://ursb.me")
のように)。Blink などのブラウザエンジンは、それを大文字に正規化します Source/core/xmlhttprequest/XMLHttpRequest.cpp - chromium/blink - Git at Google:
void XMLHttpRequest::open(const AtomicString& method,
const KURL& url,
bool async,
ExceptionState& exception_state) {
//...
method_ = FetchUtils::NormalizeMethod(method);
// ...
}
AtomicString FetchUtils::NormalizeMethod(const AtomicString& method) {
// https://fetch.spec.whatwg.org/#concept-method-normalize
// GET と POST を最初に配置するのは、他のメソッドよりも一般的に使用されるためです。
const char* const kMethods[] = {
"GET", "POST", "DELETE", "HEAD", "OPTIONS", "PUT",
};
for (auto* const known : kMethods) {
if (EqualIgnoringASCIICase(method, known)) {
// すでにすべて大文字の場合は新しい文字列を割り当てる必要はありません。
return method == known ? method : known;
}
}
return method;
}
URL は大文字小文字を区別しますか?#
規範 RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1)#section-2.7.3 によれば:
スキームとホストは大文字小文字を区別せず、通常は小文字で提供されます。他のすべてのコンポーネントは、大文字小文字を区別して比較されます。
HTTP プロトコル(スキーム)とドメイン名(ホスト)は大文字小文字を区別しませんが、パス、クエリ、フラグメントは区別します。
Chromium では、URL を kUrl
クラスで管理し、その内部で protocol
と host
を正規化処理(canonicalization)します。このプロセスでは、protocol
と host
が小文字に変更されます。
さらに興味深いのは path
です。規範では path
は大文字小文字を区別するとされていますが、実際には Web サーバーの基盤となるファイルシステムによって異なるため、path
も大文字小文字を区別しない可能性があります。
例えば、IIS サーバーは大文字小文字を区別しません。これは Windows のファイルシステムに依存しており、Windows が使用する NTFS および FAT 系列のファイルシステムは、デフォルトで大文字小文字を区別しません(ただし、大文字小文字は保持されます)。同様に、Apache サーバーが大文字小文字を区別しない Mac(HFS)上にデプロイされている場合も同様です。
主流のオペレーティングシステムの基盤となるファイルシステムについての拡張情報:
- Windows が使用する NTFS および FAT 系列のファイルシステムは、デフォルトで大文字小文字を区別しませんが、大文字小文字は保持されます。
- macOS が使用する APFS および HFS+ ファイルシステムは、デフォルトで大文字小文字を区別しませんが、大文字小文字は保持されます。
- Linux が使用する ext3/ext4 ファイルシステムは、デフォルトで大文字小文字を区別します。
さらに、サーバーのポリシーにも関係があります。例えば、https://en.wikipedia.org/wiki/Case_sensitivity と https://en.wikipedia.org/wiki/case_sensitivity は同じ記事を指していますが、単純に大文字小文字を区別しないとは言えません。なぜなら、https://en.wikipedia.org/wiki/CASE_SENSITIVITY は直接 404 になっているからです。
クッキーは大文字小文字を区別しますか?#
結論から言うと、直感に合致して、クッキーの名前は大文字小文字を区別します。ただし、規範の進化はかなり困難でした。
初期の規範 RFC 2109 - HTTP State Management Mechanism では、クッキーの名前は大文字小文字を区別しませんでした:
属性(名前)(attr)は大文字小文字を区別しません。 トークン間には空白が許可されています。上記の構文説明では値がオプションとして示されていますが、ほとんどの属性には値が必要です。
しかし、RFC 2965 では RFC 2109 を廃止しました:
この文書は RFC 2109 の実装経験を反映しており、それを廃止します。
しかし、クッキーの最新規範 RFC 6265 では、クッキーが大文字小文字を区別するかどうかは明記されていないため、デフォルトで区別されると考えられます。主流のブラウザである Chrome と Firefox の実装もクッキーの大文字小文字を区別しています。
E メールアドレスは大文字小文字を区別しますか?#
規範 RFC 5321: Simple Mail Transfer Protocol#section-2.3.11 によれば:
標準のメールボックス命名規則は「local-part@domain」と定義されています。現代の使用法は、単純な「ユーザー名」よりもはるかに広範なアプリケーションを許可します。その結果、中間ホストが輸送を最適化するためにそれらを変更しようとした際の問題の長い歴史により、local-part はアドレスのドメイン部分で指定されたホストによってのみ解釈され、意味が割り当てられる必要があります。
ドメイン部分は RFC 1035: Domain names - implementation and specification#section3.1 に従います:
「ネームサーバーとリゾルバーは、[ドメイン] を大文字小文字を区別しない方法で比較する必要があります。」
以上から、メールアドレスのドメイン名部分は大文字小文字を区別せず、ユーザー名(local-part)が大文字小文字を区別するかどうかは、電子メールサービスプロバイダーに依存します。
HTML5 タグと属性名は大文字小文字を区別しますか?#
規範 HTML Live Standard#section-13.1 によれば、HTML タグと属性名は大文字小文字を区別しません:
HTML 構文の多くの文字列(例えば、要素の名前やその属性)は大文字小文字を区別しませんが、これは U+0041 から U+005A(ラテン大文字 A からラテン大文字 Z)および U+0061 から U+007A(ラテン小文字 a からラテン小文字 z)の範囲内の文字にのみ適用されます。このセクションでは、便宜上これを「大文字小文字を区別しない」と呼びます。
これは、文書タイプ <!DOCTYPE html>
を <!doctype html>
と書いても問題ないことを意味します。
ただし、データ属性は例外であり、小文字である必要があります。HTML Live Standard#section3.2.6.6 によれば:
カスタムデータ属性は、名前が文字列 "
data-
" で始まり、ハイフンの後に少なくとも 1 文字があり、XML 互換であり、ASCII 大文字アルファを含まない属性です。
Blink には訂正ロジックがあり、たとえ <div data-Name="airing"></div>
と書いても、最終的には <div data-Name="airing"></div>
に変換されます。しかし、互換性を考慮すると、ここでの属性名は小文字の data-name
である必要があります。
CSS は大文字小文字を区別しますか?#
CSS Selectors Level 3 によれば:
すべてのセレクター構文は、ASCII 範囲内では大文字小文字を区別しません(すなわち、[a-z] と [A-Z] は同等です)。ただし、セレクターの制御下にない部分は除きます。セレクター内の文書言語の要素名、属性名、および属性値の大文字小文字の感度は、文書言語に依存します。例えば、HTML では要素名は大文字小文字を区別しませんが、XML では大文字小文字を区別します。名前空間プレフィックスの大文字小文字の感度は [CSS3NAMESPACE] で定義されています。
CSS のセレクター構文は大文字小文字を区別しませんが、属性名と属性値が大文字小文字を区別するかどうかは、所在する文書言語に依存します。 例えば、XHTML DOCTYPE ではそれらは大文字小文字を区別しますが、HTML DOCTYPE では区別しません。
Git のファイル名は大文字小文字を区別しますか?#
Git はデフォルトでファイル名の大文字小文字を区別しません。
そのため、普段ファイルの大文字小文字に注意を払わないと、実際の使用中に次のような問題に直面する可能性があります:
- チームの中に Linux システムや大文字小文字を区別するファイルシステムを有効にした macOS または Windows で開発している人がいる場合、既存の
RankItem.tsx
ファイルを無視して新しいrankItem.tsx
ファイルを作成し、正常にコミットされます。 - この時点で、Git サーバー上には
RankItem.tsx
とrankItem.tsx
の両方のファイルが存在し、Windows または macOS(デフォルトのファイルシステム)で開発している人は、これらの 2 つのファイルを正常に取得できません。
ここでは、Git の大文字小文字無視機能を無効にすることをお勧めします:
git config --global core.ignorecase false
また、Windows または macOS で大文字小文字を変更する際には、git mv
を使用します:
git mv --force rankItem.jsx RankItem.jsx
無効にしている場合、例えば readme.md
を README.md
に変更すると、git status
では変更記録を検出できません:
git mv
を使用すれば、正常に検出されます:
そのため、開発時には Git の大文字小文字無視の設定を無効にし、git mv
を使用して名前変更操作を行うことを強くお勧めします。条件が許す場合は、macOS または Windows のデフォルトのファイルシステムを大文字小文字を区別するように変更することも検討できます。
以上がフロントエンド開発における大文字小文字の感度の問題に関する内容です。まとめると:
- HTTP ヘッダーのキーは大文字小文字を区別しませんが、ほとんどのフレームワークはレスポンスヘッダーのキーを小文字に変更します。[RFC 7230]
- HTTP2 ではヘッダー圧縮のため、必ず小文字にする必要があり、ブラウザは自動的に小文字に変更します。[RFC 7540]
- HTTP メソッドは大文字小文字を区別し、すべて大文字である必要があります。[RFC 7230]
- URL のプロトコルとホストは大文字小文字を区別せず、ブラウザは自動的に小文字に変更します。[RFC 7230]
- URL のパスは規範上大文字小文字を区別しますが、実際に大文字小文字を区別するかどうかは Web サーバーのファイルシステムとサーバー設定に依存します。[RFC 7230]
- URL のクエリとフラグメントは大文字小文字を区別します。[RFC 7230]
- クッキーのキーは大文字小文字を区別しますが、規範には明記されていません。[RFC 6265]
- E メールのドメイン名部分は大文字小文字を区別しません。[RFC 1035]
- E メールのユーザー名部分が大文字小文字を区別するかどうかは、メールサービスプロバイダーに依存します。[RFC 5321]
- HTML5 のタグ名と一般的な属性キーは大文字小文字を区別しません。[HTML Live Standard 13.1]
- HTML5 の data- 属性は大文字小文字を区別します。[HTML Live Standard 3.2.6.6]
- CSS のセレクター構文は大文字小文字を区別しませんが、属性名と属性値が大文字小文字を区別するかどうかは所在する文書言語に依存します。[CSS Selectors Level 3]
- Git はデフォルトでファイル名の大文字小文字を無視します。
- Windows が使用する NTFS および FAT 系列のファイルシステムは、デフォルトで大文字小文字を区別しませんが、大文字小文字は保持されます。
- macOS が使用する APFS および HFS+ ファイルシステムは、デフォルトで大文字小文字を区別しませんが、大文字小文字は保持されます。
- Linux が使用する ext3/ext4 ファイルシステムは、デフォルトで大文字小文字を区別します。