URLにエンコードが必要な理由
URLは文字で構成されていますが、すべての文字が「安全に」直接使用できるわけではありません。URLはインターネット上のさまざまなシステムを経由して送信され、それらのシステムが特定の文字を特別な意味を持つものとして解釈することがあります。たとえば、? はクエリ文字列の開始、& はパラメータの区切り、# はフラグメント識別子として使われます。また、中国語やアラビア語などの非ASCII文字も存在し、古いHTTPインフラによって文字化けが発生する可能性もあります。
URLエンコード(公式名称:パーセントエンコーディング)は、安全でない文字を % とその文字のバイト値を2桁の16進数で表したものに置き換えることで、この問題を解決します。
スペース (0x20) → %20
アットマーク (0x40) → %40
スラッシュ (0x2F) → %2F
RFC標準
URLエンコードはRFC 3986(Uniform Resource Identifiers)で定義されています。この標準によれば:
非予約文字 — エンコードせずにURLのどこでも安全に使用できる文字:
- A–Z、a–z、0–9
-、_、.、~
予約文字 — URIにおいて特別な意味を持ち、データとして使用する場合はパーセントエンコードしなければならない文字:
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
それ以外のすべての文字——スペース、非ASCII文字、"、<、>、{、} など——はパーセントエンコードしなければなりません。
パーセントエンコードの実践
スペースのエンコード
スペース文字(U+0020)は %20 にエンコードされます。クエリ文字列では %20 の代わりに + が使われることもあります。これはHTMLフォームのURLエンコード(application/x-www-form-urlencoded)標準に由来するもので、スペースを + でエンコードします。この2つは異なります:
%20— RFC 3986パーセントエンコード(パスやヘッダーのスペース)+— HTMLフォームエンコード(クエリ文字列のスペース)
デコードする際は、どちらの規約が適用されるかを必ず把握してバグを防ぎましょう。
非ASCII文字
非ASCII文字はまずUTF-8でエンコードされ、その後各バイトがパーセントエンコードされます:
漢字「中」(U+4E2D)
UTF-8バイト列:0xE4 0xB8 0xAD
パーセントエンコード:%E4%B8%AD
したがって、日本語の「こんにちは」のような文字も同様の方式でエンコードされます。
JavaScriptのエンコード関数
JavaScriptにはURLエンコード用のビルトイン関数がいくつか用意されています:
encodeURI()
完全なURIをエンコードします——URL全体をエンコードしながらその構造を保持するように設計されています。URI構文の一部である文字(; , / ? : @ & = + $ #)はエンコードしません。
encodeURI("https://example.com/search?q=hello world&lang=日本語")
// "https://example.com/search?q=hello%20world&lang=%E6%97%A5%E6%9C%AC%E8%AA%9E"
encodeURIComponent()
URIコンポーネントをエンコードします——クエリパラメータの値など、個々の値をエンコードするために設計されています。A–Z a–z 0–9 - _ . ! ~ * ' ( ) 以外のすべてをエンコードします。
encodeURIComponent("hello world & more")
// "hello%20world%20%26%20more"
encodeURIComponent("https://example.com")
// "https%3A%2F%2Fexample.com"
どちらを使うべきか
| シナリオ | 関数 |
|---|---|
| URL全体をエンコード | encodeURI() |
| クエリパラメータの値をエンコード | encodeURIComponent() |
| パスセグメントをエンコード | encodeURIComponent() |
| 別のURLに埋め込むURLをエンコード | encodeURIComponent() |
対応するデコード関数
decodeURI(encodedURI)
decodeURIComponent(encodedComponent)
非推奨の escape() と unescape() 関数は絶対に使わないでください——非ASCII文字の扱いが異なり、誤った結果を生成します。
よくあるハマりどころ
二重エンコード
すでにエンコードされた文字列を再度エンコードしてしまうのはよくあるバグです:
encodeURIComponent(encodeURIComponent("hello world"))
// "hello%2520world"
// %25 は % のエンコードなので、%20 が %2520 になってしまう
エンコードする前に、その値がすでにエンコードされていないかを必ず確認してください。
+ と %20 の罠
+ でスペースを表すクエリ文字列を decodeURIComponent でデコードしても、+ はスペースにデコードされません——事前に + を %20 に置換するか、URLSearchParams APIを使用する必要があります:
new URLSearchParams("q=hello+world").get("q")
// "hello world" ✓
decodeURIComponent("hello+world")
// "hello+world" ✗ — リテラルのプラス記号のまま
フラグメント識別子
URLの # 文字はフラグメント識別子(ページ内アンカー)の始まりを示します。データに # が含まれる場合は %23 にエンコードしなければなりません。そうしないと、ブラウザはその後のすべてをフラグメントとして扱います。
国際化ドメイン名(IDN)
非ASCII文字を含むドメイン名(bücher.de など)はパーセントエンコードではなく、Punycodeエンコードを使用します。ブラウザはIDNを内部的にPunycodeに変換します:bücher.de → xn--bcher-kva.de。
URL構造リファレンス
RFC 3986によれば、URLは以下のコンポーネントで構成されます:
scheme://userinfo@host:port/path?query#fragment
| コンポーネント | 例 | エンコードルール |
|---|---|---|
| Scheme | https | 英字、数字、+、-、. |
| Host | example.com | ドメインラベル + ドット |
| Port | 8080 | 数字のみ |
| Path | /search/results | %XXでエンコード(非予約文字 + :@!$&'()*+,;= を除く) |
| Query | q=hello+world | フォームデータではスペースに +、通常は %20 |
| Fragment | #section-2 | サーバーには送信されない;ブラウザのみ使用 |
サーバーサイドの考慮事項
URLの正規化
サーバーは処理前にURLを正規化する必要があります——たとえば、%41(Aにデコードされる)と A を同じものとして扱います。ただし、一部の文字はエンコードされた状態とエンコードされていない状態で異なる意味を持ちます:パスの / と %2F ——セキュリティ上の理由(パストラバーサル対策)から、多くのWebサーバーはこれらを異なるものとして扱います。
URLパラメータを通じたSQLインジェクション
URLデコード後であっても、URLパラメータをデータベースクエリに使用する前には必ずサニタイズと検証を行ってください。URLエンコードはセキュリティの境界ではありません。
ツールとAPI
ブラウザのURL API
最新のブラウザとNode.jsは、URLを構造化された方法で操作するためのURL APIを提供しています:
const url = new URL("https://example.com/search?q=hello world&page=1");
console.log(url.searchParams.get("q")); // "hello world"(自動デコード)
url.searchParams.set("q", "new value & special");
console.log(url.href);
// https://example.com/search?q=new+value+%26+special&page=1
URL APIはエンコード/デコードを透過的に処理するため、一般的には encodeURIComponent を手動で呼び出すよりも推奨されるアプローチです。
まとめ
URLエンコードは、すべてのWeb開発者が毎日遭遇する基本的な概念で、多くの場合気づかないうちに使っています。覚えておくべき重要なポイント:
- パーセントエンコード(
%XX)はURIで安全でない文字をエンコードするための標準メカニズムです。 - 個々の値には
encodeURIComponent()を、URL全体にはencodeURI()を使用してください。 - クエリ文字列における
+と%20の違いに注意してください。 - 二重エンコードを避けてください——エンコードする前に文字列がすでにエンコードされていないか確認してください。
- プログラムでURLを操作する際は、最新の
URLおよびURLSearchParamsAPIを優先して使用してください。