FlowingDev

パーセントエンコーディング解説:%20や%3Fなど、URLのナゾ文字を解き明かす

なぜURLには特定の文字が使えないのか?そして、パーセントエンコーディングがどのようにして安全でない文字を、ウェブで汎用的に使える安全な形式に変換するのかを解説します。

ツールを試す: URL エンコーダー

一文で言うと

URLエンコーディング(正式にはパーセントエンコーディング)とは、URL内で特別な意味を持つ文字や使えない文字を、混乱なく送信できるよう、安全で世界共通の形式に変換する処理のことです。

こいつが解決する問題

ウェブ黎明期のカオスの中では、物事はもっとシンプルでした。URL、もっと広く言えばURI(Uniform Resource Identifier)は、リソースの場所をクリーンで予測可能な方法で示すために設計されました。ティム・バーナーズ=リー卿をはじめとする設計者たちは、ASCIIという限られた文字セットを土台にこのシステムを構築しました。

http://example.com/reports/April.htmlみたいなのを指し示すだけなら、これで十分でした。でも、もっと複雑な話になるとどうでしょう?

URLの構造を考えてみてください。URLにはパーツがあります。スキーム(http:)、ホスト(example.com)、パス(/search)、そしてクエリ文字列(?q=dogs&cats)などです。特定の文字は、この劇の構成監督のような役割を果たします。コロン(:)はスキームを区切ります。スラッシュ(/)はパスのセグメントを区切ります。クエスチョンマーク(?)はクエリパラメータの始まりを告げます。アンパサンド(&)はパラメータとパラメータを区切ります。

ここからが問題の始まりです。もし、"C++ & C#"という文字列そのものを検索したいとしたら?これをそのままURLに放り込むと、.../search?q=C++ & C#のようになります。これを見たウェブサーバーは、もうお手上げです。サーバーは、クエリが"C++ "だと解釈し、その後のアンパサンドを見て次のキーと値のペアを期待しますが、そこにあるのは孤独な" C#"だけ。大混乱です。意図した意味は失われてしまいました。

さらに、そもそも許されていない文字もあります。スペース(空白)は古典的なトラブルメーカーです。いつがファイル名の一部としてのスペースで、いつがブラウザが無視すべきただのタイポなのか?それに、基本的な英字アルファベット以外の文字はどうでしょう?ウェブはグローバルです!ASCII用に設計されたURLに、Résumé.pdfや你好.htmlをどうやって入れればいいんでしょうか?

パーセントエンコーディングは、この種の問題をすべて解決します。これは、いわば緊急脱出口。「おい、ウェブサーバー君。次の文字は構造的な意味はないから。解釈しないでくれ。これはただのデータなんだ」と伝えるための方法です。ブラジルのブラウザでも、ベルリンのサーバーでも、URLが同じ意味を持つことを保証する、まさに万能翻訳機なのです。

裏側の仕組み

パーセントエンコーディングの裏側にある「魔法」は、驚くほど単純です。これは手品というより、誰もが使うことに同意した単純な換字式暗号のようなものです。

登場人物:予約文字 vs 非予約文字

まず、どの文字がイケてて、どの文字が問題児なのかを知る必要があります。これらはいくつかのグループに分けられます。

文字の種類 文字 エンコードするケース
非予約文字 (Unreserved) A-Z a-z 0-9 - _ . ~ 絶対にしない。 これらはURL界のVIPです。常に安全。
予約文字 (Reserved) : / ? # [ ] @ ! $ & ' ( ) * + , ; = 時々する。 これらは特別な構造的意味を持ちます。パス区切りの/のように、その意味で使いたい場合はエンコードしません。検索クエリ内の&のように、単なるデータとして使いたい場合は、必ずエンコードします。
その他(安全でない文字) (スペース)、`< > " % { } \ ^` およびすべての非ASCII文字

重要なのは文脈(コンテキスト)です。?という文字は、パスとクエリ文字列を分ける唯一の?として使うなら問題ありません。しかし、クエリパラメータの値の中に文字通りのクエスチョンマークが必要な場合は、エンコードしなければなりません。

魔法のトリック:パーセント + 16進数

エンコードのプロセスは、簡単な3ステップのダンスです。

  1. エンコードしたい文字を選ぶ。ここではアンパサンド&を使ってみましょう。
  2. 標準的な文字セットを使って、そのバイト値を見つける。ウェブの場合、この標準はUTF-8です。UTF-8(とその前身であるASCII)では、&という文字は10進数の38で表現されます。
  3. その数値を2桁の16進数に変換し、先頭にパーセント記号(%)を付けます。10進数の38は、16進数では26です。

というわけで、&は%26になります。

もう少し試してみましょう:

  • スペースは10進数で32、16進数で20。エンコードすると%20。
  • クエスチョンマーク(?)は10進数で63、16進数で3F。エンコードすると%3F。
  • パーセント記号(%)自体は10進数で37、16進数で25。なので、%自体をエンコードするには%25と書きます。

このシステムが素晴らしいのは、パーセント記号自体が非予約文字ではないため、パーサーは%を見つけたら、その後に16進数2桁が続くと判断できる点です。

英語以外の文字はどうなるの?

ここでUTF-8が極めて重要になります。Aのような単純なASCII文字は1バイトです。しかし、フランス語のéや中国語の好のような文字は、UTF-8では複数のバイトで表現されます。エンコードのプロセスは同じですが、各バイトに対して繰り返されます。

éを例にとってみましょう:

  1. UTF-8では、éはC3とA9(16進数)という2つのバイトで表現されます。
  2. 各バイトを個別にエンコードします:
    • C3 は %C3 になります。
    • A9 は %A9 になります。
  3. それらを結合します。éは%C3%A9になります。

デコードのプロセスは、このまったくの逆です。ブラウザやサーバーは%C3%A9を見て、C3とA9という2つのバイトを取得し、UTF-8デコーダにかけると、美しいéの文字が元通りになります。

実録・現場の物語

理論は素晴らしいですが、実際に現場でどう使われているか見てみましょう。

消えた検索クエリ事件

若手開発者のマヤは、技術ドキュメントサイトの検索機能を構築していました。ユーザーは「C++」や「promises & async/await」などを検索できます。彼女は単純な文字列連結で検索URLを組み立てました:site.com/search?q= + userInput。

事態はめちゃくちゃになりました。「promises & async/await」を検索すると、.../search?q=promises & async/awaitというURLが生成されました。しかし、サーバーが報告してきた検索語は"promises "だけでした。&が新しいパラメータの区切り文字として解釈され、キーがなかったasync/awaitは捨てられてしまったのです。彼女の検索結果は、まったくの間違いでした。

教訓: マヤはウェブ開発の鉄則を学びました。URLのコンポーネントに動的なデータを入れるときは、必ずパーセントエンコードする。 ユーザー入力をエンコードし始めると、URLは正しく.../search?q=promises%20%26%20async%2Fawaitとなり、サーバーは完全で正しい文字列を受け取り、検索は完璧に機能するようになりました。

国際問題

あるオンラインストアが、ドイツのパートナーからの新製品「Fußball」をフィーチャーすることにしました。マーケティングチームは、store.com/products/Fußballという親しみやすいURLを作成しました。オフィスのモダンなブラウザでは、すべてが順調に見えました。

しかし、発売日は大混乱。カスタマーサポートにはチケットが殺到しました。一部のユーザーは「404 Not Found」エラーに遭遇。他のユーザーのブラウザバーには.../products/Fu%C3%9Fballと表示されたり、.../products/FuÃballと表示されたり。システムは新旧コンポーネントの寄せ集めで、非ASCII文字であるß(エスツェット)を一貫して処理できていなかったのです。ある部分はエンコードせず、ある部分はUTF-8を想定してエンコードし、またあるレガシーシステムは別の文字セットを想定してデコードしたため、文字化け(mojibake)が発生しました。

教訓: URL内の非ASCII文字をブラウザやサーバーが「よしなにやってくれる」のを期待するのは、非一貫性を生むだけです。UTF-8標準を使い、すべての非予約文字を積極的かつ一貫してパーセントエンコードすることで、URLが堅牢になり、新旧問わずウェブエコシステム全体で予測どおりに機能することが保証されます。

二重エンコードの大失敗

あるチームがシングルサインオン(SSO)システムを構築していました。フローはこんな感じです:service-a.comがユーザーをsso.com/loginにリダイレクトし、ログイン後にユーザーを戻せるように、自身のURLをパラメータとして渡します。リダイレクトURLは次のようになります:sso.com/login?redirect_uri=https://service-a.com/dashboard?param=1。

service-a.com側の開発者は賢く、redirect_uriの値をエンコードしました。結果はこうです:sso.com/login?redirect_uri=https%3A%2F%2Fservice-a.com%2Fdashboard%3Fparam%3D1。

しかし、彼らが使っていたウェブフレームワークには、「セキュリティのため」と称して、送信するすべてのクエリパラメータを自動的にURLエンコードするミドルウェア層がありました。このミドルウェアは、すでにエンコードされた文字列を見て、それを再度エンコードしてしまったのです。%3Aの%が%25に変換され、%3Aは%253Aになりました。最終的なURLは、二重エンコードされためちゃくちゃな代物になってしまいました。ユーザーがsso.comにアクセスしたとき、URLは一度しかデコードされず、単一エンコードされた文字列が返ってきました。これではリダイレクト先として使えず、ログインフローは完全に壊れてしまいました。

教訓: 自分のツールチェーン全体を把握しておくこと。データは生成時点でエンコードし、後続のシステムが再エンコードしないようにする。 二重エンコードは、有効なURLを役立たずのガラクタに変えてしまう、頭を抱えるようなよくあるバグです。

よくある間違いと落とし穴

  • URL全体をエンコードする。 これは絶対にやってはいけません。https://example.comをパーセントエンコードすると、https%3A%2F%2Fexample.comのようになります。これはもはや有効なURLではありません。スキームとホストの部分が、意味のないごちゃ混ぜの文字列になってしまいます。クエリパラメータの値や特定のパスセグメントなど、必要な部分だけをエンコードしなければなりません。
  • まったくエンコードしない。 最もよくある罪です。生のユーザー入力や特殊文字を含むデータをURL文字列に直接突っ込むのは、セキュリティホール(クロスサイトスクリプティングなど)や機能不全を招く行為です。
  • コンテキストを忘れる。 &という文字はURLのパス部分では問題ありませんが、クエリ文字列では予約された区切り文字です。/も同様です。予約文字をその特別な目的で使用している場合は、エンコードする必要はありません。
  • +と%20を混同する。 HTMLフォームで使われるapplication/x-www-form-urlencodedというコンテンツタイプでは、スペースはクエリ文字列内で+記号にエンコードされることがよくあります。多くのサーバーはこれを理解しますが、スペースの公式なパーセントエンコーディングは%20です。%20を使えば曖昧さがなく、クエリ文字列だけでなくURLのすべての部分で正しく機能します。迷ったら%20を使いましょう。
  • 古い文字セットを使う。 ウェブはUTF-8で動いています。もしデータを別の文字セット(ISO-8859-1など)でエンコードすると、UTF-8を期待しているサーバーはバイトを誤って解釈し、データを破壊してしまいます。常にUTF-8を指定し、使用してください。

なぜ知っておくべきか

URLに触れるコードを書くなら、パーセントエンコーディングの理解は必須です。オプションではありません。次のようなときは、常にこのことを考えるべきです:

  • 変数やユーザー入力からURLを組み立てるとき。
  • URLにパラメータを含めてAPIリクエストを行うとき。
  • ファイル名、ユーザープロファイル、またはURLに表示される可能性のあるコンテンツで、国際文字を扱うとき。
  • サーバーサイドでURLをパースしてデータを抽出するとき。
  • リダイレクトを記述したり、他のサービスにパラメータとしてURLを渡したりするとき。

要するに、パーセントエンコーディングはウェブの配管の基本部品のようなものです。これを無視すると、バグが多く、安全でなく、信頼性の低いソフトウェアにつながります。その仕組みを知っていることは、プロのウェブ開発者の証です。

さらに深く

  • RFC 3986: URI (Uniform Resource Identifier) の正式な仕様書。セクション2で文字セットとパーセントエンコーディングのルールが定義されています。これこそが究極の真実の源です。
  • MDN Web Docs: encodeURIComponent(): JavaScript開発者向けの実践的なガイド。どの関数をなぜ使うべきかを説明しています。「関連情報」セクションには、他の関連エンコード関数へのリンクもあります。
  • Wikipedia: Percent-encoding: この概念、その歴史、そして様々なニュアンスについての、包括的で非常に読みやすい概要。
  • W3C: Character encodings: なぜウェブで文字エンコーディングが重要なのかについてのハイレベルな入門書。物語の主役はUTF-8です。

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: URL エンコーダー