記事一覧へ戻る
読了 約 5

URL エンコードの仕様と「日本語が文字化けする」を本気で直すガイド

#development

執筆: デジタル道具屋 編集部 / Web 標準担当

検索フォーム・OAuth コールバック URL・Open Graph 等、URL を真面目に扱う実装を多数経験してきた編集チームです。

本記事の執筆方針と編集ポリシーについてはAbout ページをご覧ください。

導入:「URL に日本語を入れたら全部 % になった」

男の子
男の子
「博士、Slack にシェアした URL が https://example.com/?q=%E3%81%82%E3%81%84 って『%』だらけになっちゃって意味不明なんだぜ! これって壊れてるのかよ?」
博士
博士
「壊れておらんよ。あれは『percent-encoding(パーセントエンコード)』と言って、ASCII以外の文字を URL で安全に運ぶための正しい姿じゃ。%E3%81%82 は『あ』を UTF-8 で 3バイト表現した結果じゃよ。」
女の子
女の子
「URL の仕様(RFC 3986)では、ASCIIの一部の文字以外は percent-encoding しないと使えないって決まっているの。だから日本語を URL に含めるときは必ずこの変換が走るのよ。」

1. percent-encoding の仕組み

URL では 「予約文字(reserved characters)」「非予約文字(unreserved characters)」が区別されます。 非予約文字(英数字とごく一部の記号)以外を URL の意味のある場所に入れたい場合、各バイトを %XX という 16進数表記に変換します。

'あ'  → UTF-8: 0xE3 0x81 0x82  → "%E3%81%82"
'A'   → ASCII: 0x41             → "A"(変換不要)
' '   → ASCII: 0x20             → "%20" もしくは "+"
'?'   → ASCII: 0x3F             → "%3F"(クエリ区切りと混同しないため)

2. encodeURIencodeURIComponent の違い

男の子
男の子
「JavaScript には encodeURIencodeURIComponent 両方あるけど、どっち使うんだぜ?」

ざっくり言うと、パスやクエリの「区切り記号」を残すかどうかの違いです。

関数対象?/
encodeURIURL 全体そのまま残す
encodeURIComponentクエリの値、フラグメント等の「断片」エンコードする

実務では クエリパラメータの値はほぼ常に encodeURIComponentを使います。encodeURI は『URL 全体をまとめて変換したい』という用途のとき使いますが、ベストは URL オブジェクトと URLSearchParams を活用することです:

// 推奨:URLSearchParams を使う
const params = new URLSearchParams({ q: '東京 ラーメン', page: '2' });
const url = `https://example.com/search?${params.toString()}`;
// → https://example.com/search?q=%E6%9D%B1%E4%BA%AC+%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3&page=2
女の子
女の子
URLSearchParams なら percent-encoding を全部勝手にやってくれるから、文字列を自分で連結するより安全よ。」

3. 「+ はスペース」問題

URL のクエリ部分では、スペースが %20 ではなく + で表されることがあります。 これは application/x-www-form-urlencoded(HTML フォームの送信形式)の歴史的経緯です。パス部分では %20、クエリ部分では + または %20、というのが現在の事実上のルール。

デコード側でも注意が必要:

decodeURIComponent('東京+ラーメン')      // → "東京+ラーメン"(+ は変換されない)
new URLSearchParams('q=東京+ラーメン').get('q') // → "東京 ラーメン"(+ がスペースに)

4. 日本語ファイル名のダウンロード

Content-Disposition: attachment; filename="日本語.pdf" のような書き方は、 ASCII 以外の扱いが歴史的に混乱しています。 現在は RFC 5987 に従い、以下のように書くのが標準です:

Content-Disposition: attachment;
  filename="report.pdf";
  filename*=UTF-8''%E6%97%A5%E6%9C%AC%E8%AA%9E.pdf

filename(旧)に ASCII フォールバック、filename*(新)に RFC 5987 形式で日本語を入れます。 モダンブラウザは filename* を優先するため、日本語ファイル名で正しくダウンロードされます。

5. SNS シェア用 URL の組み立て

X(Twitter)や LINE のシェア URL を生成するとき、ユーザー入力をそのまま連結すると壊れます:

// 失敗:& や # が混入すると壊れる
const url = `https://twitter.com/intent/tweet?text=${title}&url=${pageUrl}`;

// 正しい
const url = new URL('https://twitter.com/intent/tweet');
url.searchParams.set('text', title);
url.searchParams.set('url', pageUrl);
window.open(url.toString());

6. 試して確かめる

URL に含まれる謎の %XX をデコードしたり、自分の文字列を percent-encoding したりするには、URL エンコード・デコードツールが便利です。 クエリパラメータのデバッグや、SNS シェア URL の構築にもどうぞ。

関連: HTML エンティティ変換(HTML 上で < を表示する用)、Base64 変換(バイナリを URL に乗せる別手段)。

🚨 現場の失敗あるある

OAuth の stateredirect_uri をエンコードし忘れて、+= を含む値が壊れて認証フローが止まる事故をしばしば見ます。 URL を組み立てるときは、String 連結ではなく URL オブジェクトとURLSearchParams を使う癖を付けると、ほとんどの落とし穴を回避できます。

参考にした一次情報

本記事の内容は、以下の公式仕様や一次情報を参照して執筆しています。