1分で読了エンジニアリング

URLエンコーディングとは:エスケープすべき文字はどれか

URLエンコーディングは、文字をパーセント記号と2桁の16進数に置き換えることで、URLの構文として読まれないようにする仕組みです。どの文字が対象になるのか、そしてどこで壊れるのかを解説します。

Marius Voß
DevRel · edge infra
スペースとアンパサンドが1つのパラメータ値の中でパーセントシーケンスに変わる様子を、クエリ文字列で示したURLエンコーディングの図

URLエンコーディングは、文字をパーセント記号と2桁の16進数に置き換える仕組みです。スペースは%20に、アンパサンドは%26に、疑問符は%3Fになります。狙いは、データとして意図した文字がURLの構文として読まれてしまうのを防ぐことだけです。それ以上の意味はありません。

実際にはそれよりも難しく感じられる理由は、これにまつわるほぼすべての疑問が本当はスコープについての疑問だからです。どの文字を、URLのどの部分で、どの層がエスケープするのか。スコープを間違えると、2つの典型的な故障のどちらかが起こります。静かに途切れるトラッキングパラメータか、https%3A%2F%2Fexample.comとして届き404になる宛先です。この記事では、答えを決める2つの文字集合、ルールが変わる場所、そしてリンクが実際に何を運んでいるかを確認する方法を解説します。リダイレクトがこれらすべてに対して何を行うかという広い視点についてはリダイレクトの種類を参照してください。

すべてを決める2つの文字集合

RFC 3986のセクション2.3は、エンコードが決して必要ない非予約集合を定義しています。英字、数字、そしてちょうど4つの記号、ハイフン、ピリオド、アンダースコア、チルダです。値がこれらだけで構成されているなら、何もする必要はありません。

それ以外はすべて、2つのグループのどちらかに入ります。予約済み文字は構造的な意味を持ち、: / ? # [ ] @はURLの各部分を区切り、! $ & ' ( ) * + , ; =はそれらの部分の内部をさらに区切ります。セクション2.2に一覧があります。これらは構文としては合法ですが、データとして現れる場合はエンコードしなければなりません。残りはASCIIの範囲外にあるすべての文字で、UTF-8に変換された後、1バイトずつエンコードされます。そのため、アクセント付きの1文字が通常3文字ではなく6文字分のコストになります。

ここから、覚えておく価値のある唯一のルールが導かれます。文字がデータであり、そのままだと構文として読まれてしまう場合にエンコードする、です。2つのパラメータの間にあるアンパサンドは構文です。キャンペーン名の中にあるアンパサンドはデータであり、そのままにしておくとパラメータのリストはそこで終わってしまいます。

キャンペーンの値にスペースとアンパサンドが含まれるクエリ文字列を示す図。値の内部だけを正しくエンコードした場合と、URL全体を誤ってエンコードした場合を対比している

エンコードするのは値であり、URLではない

これは私が最もよく目にする間違いで、いつも同じ形をしています。誰かがURLを持っていて、エンコードが必要だと分かっているので、丸ごとエンコーダーに貼り付けてしまい、こうなります。

https%3A%2F%2Fexample.com%2Fspring%3Futm_campaign%3Dspring%20sale

この文字列はURLではありません。別のURLの中のにしかなり得ない、URLの形をしたテキストです。宛先をリダイレクターに渡すときにはまさにそこにあるべきものであり、それを直接開こうとするときにはまさにそこにあってはならないものです。

正しい処理は、それぞれの値を個別にエンコードすることです。

https://example.com/spring?utm_campaign=spring%20sale&utm_source=flyer

スキーム、ホスト、パスの区切り文字、そして?&は構文としてそのままにしておきます。変わったのは値だけです。どの言語もこの区別のために2つの関数を用意しており、どちらを選ぶかを間違えることが問題のもう半分です。MDNのencodeURIComponentのページは、encodeURIが意図的に予約文字をそのままにしておくのはURI全体を期待しているからであり、encodeURIComponentがそれらをエスケープするのはURIの断片を期待しているからだと明確に述べています。値にはencodeURIComponentを使うべきです。Pythonではurllib.parse.quote、Goではurl.QueryEscape、PHPではrawurlencodeがこれに当たります。

スペースは%20、ただしプラスになる場所もある

どちらも場所によっては正しく、これがこのテーマの中で最も混乱を招く点です。

パスや一般的なURIの中では、スペースは%20です。HTMLフォームが組み立てる形式のクエリ文字列の中では、スペースは+になります。WHATWG URL標準application/x-www-form-urlencodedシリアライズ形式がそう規定しているからです。どちらの形式も、出会う可能性のあるサーバー側のクエリパーサーであればスペースとして読み取られます。

落とし穴は逆方向にあります。プラス記号がデータである場合、電話番号や検索語、spring+summerという名前のキャンペーンなどですが、%2Bと書かなければなりません。クエリ文字列の中でそのままにしておくとスペースになってしまい、CRMの中の番号がなぜ国番号を失ったのか、午後をまるごと使って悩むことになります。

CharacterEncodedWhy it matters
space%20または++はクエリ文字列の中だけ、%20はどこでも
&%26エンコードしないと、パラメータのリストはそこで終わる
?%3Fエンコードしないと、それ以降すべてがクエリになる
#%23エンコードしないと、残りの部分はサーバーに一切届かない
+%2Bクエリの中でエンコードしないと、スペースとして届く
%%25エンコードしないと、次の2文字が食べられてしまう

#の行には注記が必要です。これが最も分かりにくいバグ報告を生む原因だからです。フラグメントはサーバーに送信されることがありません。エンコードされていない#をリダイレクト先に入れると、サーバー側は途切れたURLを見ることになりますが、ブラウザのアドレスバーは正しく見えたままなので、報告してきた人はリンクは問題ないと言い張ります。

キャンペーンURLをたまに以上の頻度で手作業で組み立てているなら、そこでやめましょう。当社のUTMビルダーは入力しながら各値をエンコードしてくれますし、UTM命名規則ではそもそもエンコードが不要な値の選び方を解説しています。結果を自分のドメインで短縮すれば、エンコードされた見た目の悪い文字列を誰も見る必要がなくなります。

二重エンコーディングと、その見分け方

二重エンコーディングは、値が2つの層を通過し、それぞれがきちんと仕事をしてしまうことで起こります。パーセント記号自体もエスケープが必要な文字なので、%20%2520になり、%2520%252520になります。

症状は一度見れば見分けられるようになります。実際の訪問者にspring%20saleと表示されるページタイトル。エスケープシーケンスが見える状態でアナリティクスに届くパラメータ。最初のホップでは動くのに2番目のホップで失敗するリダイレクト。原因はほぼ常に、すでにエンコードされた状態で届いた値をさらにエンコード呼び出しで包んでしまうことであり、多くはエンコードされた形式のまま保存しているデータベースから出てきた値が原因です。

修正策は、どの層がエンコードを担当するかを決め、他の層には触らせないことです。値を読み取るときに1回だけデコードし、URLに書き込むときに1回だけエンコードし、同じ関数の中で両方を行うことは決してしないようにします。

値が2つのエンコーディング層を通過することで、スペースが%20になり、さらに%2520になる様子と、ブラウザ上に現れる症状を示す図

実際にどこで噛みつくのか

出会う可能性が高い順に、3つの場所を挙げます。

トラッキングパラメータ。エンコードされていないアンパサンドを含むキャンペーンの値はパラメータのリストを途中で切ってしまい、そのセッションはアナリティクス上でダイレクトトラフィックとして記録され、キャンペーンには成果がつきません。エラーは何も出ません。UTMパラメータがGA4に表示されないではレポート側からの診断方法を解説し、ブラウザがUTMパラメータを除去するではクリックからページの間でパラメータが消える別の理由を解説しています。

リダイレクト。サーバーのルールは一貫性なく再エンコードすることがあり、クエリ文字列がそもそも残るかどうかは使用したディレクティブに依存します。.htaccessでの301リダイレクトにApache用の完全な表があります。短く言えば、クエリ文字列を置き換えるルールは、あなたのクエリ文字列も静かに捨ててしまうということです。

QRコード。エンコーディングはペイロードの長さを膨らませ、ペイロードの長さは印刷されたコードの密度を決めます。スペース1つが1文字ではなく3文字分のコストになり、アクセント付きの1文字は6文字分になります。エンコードされたキャンペーン名を2つほど含むトラッキングURLは、コードのバージョンを1つか2つ押し上げることがあり、名刺サイズではそれが実際の差になります。まさにこの理由から、QRコードがスキャンできないではペイロードの長さを4つの原因の1つとして挙げています。フルURLの代わりに短縮リンクをエンコードすることが、利用できる中で最も安価な対策です。

リンクが実際に何を運んでいるかを確認する

2つのコマンドで、ほぼすべての議論に決着がつきます。1つ目は、リダイレクト後にサーバーが受け取る内容を表示します。

curl -sI 'https://example.com/spring?utm_campaign=spring%20sale' | grep -i '^location'

2つ目は、自分の手を信用する代わりにエンコーディングを組み立ててくれるもので、1つの値の中に複数の問題文字が同時に含まれているときに役立ちます。

curl -G --data-urlencode 'utm_campaign=spring & summer sale' \
  --data-urlencode 'utm_source=flyer' \
  -o /dev/null -w '%{url_effective}\n' https://example.com/spring

出力は装飾ではなくデータとして読みます。%2520が見えれば二重エンコーディングの問題があり、値が途中で終わっていればエンコードされていない区切り文字があり、先頭に%3A%2F%2Fが見えればURL全体をエンコードしてしまっています。ターミナルを開きたくない場合は、当社のリンクチェッカーがリダイレクトの部分をブラウザ上で行ってくれます。

身につける価値のある習慣は、キャンペーンを配信する前に、最終的なURLを一度目で確認することです。エンコーディングの不具合はブラウザ上では見えず、ターミナルでは一目瞭然です。それが原因で失われるのはアップタイムではなくアトリビューションであり、だからこそ長く生き残ってしまうのです。

コーナーストーンシリーズを読む

この記事はエンジニアリングクラスターに属しています。リダイレクトの側面については、リダイレクトの種類がすべてのステータスコードとクライアント側の手法を解説し、URL短縮サービスはどう動くのかがクリックからページの間で何が起きているかを解説しています。

ブログの関連記事

よくある質問

URLエンコーディングとは何ですか。

文字をパーセント記号とその文字のバイト値を16進数で表したものに置き換えることで、その文字がURLの構文と誤認されないようにする仕組みです。スペースは%20に、アンパサンドは%26に、疑問符は%3Fになります。この仕組みはRFC 3986で定義されており、パーセントエンコーディングとも呼ばれます。

どの文字をURLエンコードする必要がありますか。

非予約文字集合の外にあるものすべてです。RFC 3986はこの集合を、英字、数字、そしてハイフン、ピリオド、アンダースコア、チルダの4つの記号と定義しています。それ以外はすべて、構造的な意味を持つ予約済みの記号か、ASCIIの範囲外のバイトのいずれかであり、構文としてではなく値の中に現れる場合はどちらもパーセントエンコードする必要があります。

URL全体をエンコードすべきですか、それとも一部だけですか。

一部だけです。URL全体をエンコーダーに通すと、https://example.comはhttps%3A%2F%2Fexample.comになってしまい、もはやURLではなくなります。クエリパラメータの値とパスセグメントをそれぞれ個別にエンコードし、スキーム、ホスト、区切り文字はそのままにしておきます。

スペースは%20ですか、それともプラス記号ですか。

場所によって両方です。パスや一般的なURIの中では、スペースは%20です。HTMLフォームが組み立てる形式のクエリ文字列の中では、スペースはプラス記号になります。application/x-www-form-urlencodedのシリアライズ形式がそう規定しているからです。したがって、クエリの値の中にあるリテラルなプラス記号は%2Bと書かなければならず、そうしなければスペースとして読まれてしまいます。

二重エンコーディングとは何ですか。

すでにエンコードされているものを、さらにエンコードしてしまうことです。パーセント記号自体が%25にエスケープされるため、%20は%2520になります。症状としては、ページのテキストに%20がそのまま表示されたり、パラメータがエスケープシーケンスの見える状態で届いたりします。ほとんどの場合、2つの層それぞれが親切にエンコードしてしまった値が原因です。

エンコードされた文字がQRコードのスキャンを難しくするのはなぜですか。

1文字ごとに、本来の1文字ではなく3文字分のコストがかかるからです。スペースは意図としては1文字ですが、ペイロードとしては3文字になり、それがいくつか重なるだけでコードのバージョンが1つか2つ上がり、同じ印刷面積の中により多くのモジュールが詰め込まれることになります。長いトラッキングURLをQRにエンコードすることは、近距離でしかスキャンできないコードを作ってしまう最も手軽な原因の一つです。

Elidoを試す

URLを貼り付けて短縮リンクを取得

登録不要。リンクは30日間有効。永久に保存するには登録してください。

Free、登録不要 · 1日あたり2件

Elidoを試す

EUホスティングのURL短縮サービス。カスタムドメイン、詳細な分析、オープンAPI付き。無料プラン - クレジットカード不要。

タグ
url encoding
percent encoding
encodeuricomponent
query string
utm parameters
url shortener

続きを読む