0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

URLエンコードのencodeURIComponentとencodeURIを使い分ける — 残る記号・空白と+の罠・UTF-8パーセント

0
Posted at

3行まとめ

  • URLをパーセントエンコード/デコードするブラウザ完結ツールを作った。中身は標準の encodeURIComponent / encodeURI そのままだが、この2つの使い分けが本題
  • encodeURIComponent はクエリの「値」用、encodeURI はURL「全体」用。前者は / ? & = まで変換し、後者は残す。ただし encodeURIComponent でも ! ~ * ' ( ) は変換されない
  • 空白は %20 になり + にはならない。フォーム(application/x-www-form-urlencoded)で + が要るなら自前で置換する。逆に値の中の +%2B になる

APIのクエリに日本語や特殊文字を載せる、アクセスログのパーセントエンコードを読み解く——URLエンコードは頻出だが、encodeURIComponentencodeURI のどちらを使うか、空白が + にならない、といった所で毎回迷う。

ぱんだツールズのURLエンコード/デコードツールは、テキストをパーセントエンコード・デコードするツール。方式(encodeURIComponent / encodeURI)を選べる。処理はブラウザ内で完結し、入力はサーバーに送信されない。

実装は標準APIを呼ぶだけなので、この記事は「その標準APIをどう使い分けるか」に振り切って、encodeURIComponentencodeURI の違い、変換されずに残る記号、空白と + の罠、UTF-8パーセントエンコードの仕組み、デコードの例外を、実装ベースで解説する。

実装は標準API — だから使い分けが全て

エンコード・デコードは方式に応じて標準関数を呼ぶだけ。

const result = mode === 'component' ? encodeURIComponent(input) : encodeURI(input)
// デコード
const result = mode === 'component' ? decodeURIComponent(input) : decodeURI(input)

車輪の再発明はしない。パーセントエンコーディングは仕様(RFC 3986)が確立していて、ブラウザ標準の実装が正しく速い。自前で書くとUTF-8のバイト分解やサロゲートペアで事故る。問題は「どっちの関数を選ぶか」で、ここを外すとURLが壊れる。

encodeURIComponentencodeURI の境界

2つの関数の違いは「何を変換せずに残すか」に尽きる。どちらも英数字と - _ . ! ~ * ' ( ) は変換しない。差はそれ以外の記号の扱い。

記号 encodeURIComponent encodeURI
; , / ? : @ & = + $ # 変換する(%2F など) 変換しない(残す)
日本語・空白など 変換する 変換する
  • encodeURIComponent/ ? & = # といったURLの区切り記号まで容赦なく変換する。だから「URLの部品(クエリの値ひとつ)」を安全に包める。値に &= が入っていても区切りと誤認されない
  • encodeURI:URLの区切り記号は構造として残す。だから「URL全体」を渡して、日本語やスペースだけをエンコードしたいときに使う

使い分けの原則はシンプル。クエリパラメータの「値」を作るなら encodeURIComponent、既にできあがったURL全体をまとめてエンコードするなら encodeURI。迷ったら encodeURIComponent——値を1つずつ包んで組み立てる方が事故りにくい。

// 値を個別に包んで組み立てる(推奨)
const url = `https://api.example.com/search?q=${encodeURIComponent(keyword)}&tag=${encodeURIComponent(tag)}`

逆に、encodeURI に「値だけ」を渡すと、値の中の &= が残ってしまい、クエリの区切りと衝突して壊れる。encodeURI は「もう構造が確定したURL」専用と覚えておくといい。

encodeURIComponent でも残る記号 — ! ~ * ' ( )

ここが盲点。encodeURIComponent は「ほぼ全部エンコードする」イメージだが、! ~ * ' ( ) の6文字は変換されない。RFC 3986 が予約する記号の一部で、これらはそのまま通る。

このうち ~(チルダ)は RFC 3986 の「非予約文字(unreserved)」なので、エンコードせず残しても仕様上問題ない。厄介なのは残り5つの ! ' ( ) *(サブデリミタ)で、厳密なエンコードを要求する相手——OAuth 1.0 の署名や、( ) ' を特別扱いするサーバー——では、これらが素通りすることで署名不一致や解釈のズレが起きる。その場合は encodeURIComponent の後段で、この5文字だけ手当てする。

function strictEncode(s: string): string {
  // ~ は unreserved なので触らず、! ' ( ) * の5文字だけ追加エンコード
  return encodeURIComponent(s).replace(
    /[!'()*]/g,
    (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase(),
  )
}

encodeURIComponent を「完全なエンコード」と思い込まず、残る記号があると知っておくのが肝心。このツールはブラウザ標準の挙動をそのまま見せる方針なので、これらの記号は残った状態で出力される。

空白は %20+ にはならない

一番よく踏む罠。encodeURIComponent(' ') の結果は %20 であって + ではない。

encodeURIComponent('a b')   // "a%20b"(+ ではない)
encodeURIComponent('a+b')   // "a%2Bb"(+ は %2B にエンコードされる)

一方、HTMLフォームの送信形式 application/x-www-form-urlencoded では、歴史的経緯から空白を + で表す。つまりフォーム相当のエンコードが欲しいなら、%20+ に置換する一手間が要る。

// フォーム(application/x-www-form-urlencoded)相当にする
const formEncoded = encodeURIComponent(value).replace(/%20/g, '+')

逆方向の非対称も要注意で、値の中のリテラルの +encodeURIComponent%2B になる。「+=空白」と決めつけている受け側に生の + を送ると空白と解釈されるので、%2B へ確実に包めるのは実はありがたい。「+ は空白なのか、プラス記号なのか」は文脈(クエリ文字列かフォーム本体か)次第で割れる古典的な曖昧さで、%20%2B を意識的に使い分けるのが安全策になる。

日本語はUTF-8のバイトごとにパーセント化

日本語などのマルチバイト文字は、UTF-8にエンコードしたバイト列を1バイトずつ %XX にする。

encodeURIComponent('')   // "%E6%9D%B1"

「東」はUTF-8で E6 9D B1 の3バイトなので、%E6%9D%B1 になる。encodeURIComponent / encodeURI はどちらもUTF-8基準なので、現代のUTF-8なサーバーとはそのまま噛み合う。1文字が複数の %XX になるのはこのため。絵文字のようなサロゲートペアの文字も、標準関数がUTF-8の4バイトへ正しく分解してくれるので、自前でコードポイントを触る必要はない。

デコードは壊れた入力で例外を投げる

デコード側 decodeURIComponent / decodeURI は、不正なパーセント列に対して URIError を投げる% の後に16進2桁が続かない、UTF-8として不完全なバイト列、といった入力で例外になる。

decodeURIComponent('%E6%9D')   // URIError: URI malformed(3バイト目が欠けている)
decodeURIComponent('%ZZ')      // URIError: URI malformed

なので、外部から来た文字列をデコードするときは try/catch が要る。このツールも try/catch で囲み、失敗時は「不正なエンコード文字列の可能性があります」と伝えるようにしている。エンコードは(サロゲート単独でない限り)まず失敗しないが、デコードはユーザーが貼り付けた壊れた文字列で普通に例外になるので、そこだけ守りを入れる。

まとめ

  • URLエンコードは標準の encodeURIComponent / encodeURI に任せる。自前実装はUTF-8分解で事故るだけ。本質は使い分け
  • クエリの「値」は encodeURIComponent/ ? & = まで変換)、URL「全体」は encodeURI(区切りは残す)。迷ったら値を個別に包む前者
  • encodeURIComponent でも ! ~ * ' ( ) は残る。OAuth など厳密な相手には後段で追加エンコードする
  • 空白は %20+ にならない。フォーム相当が要るなら %20+ に置換。値の中の +%2B になる
  • 日本語はUTF-8バイトごとに %XX。デコードは壊れた入力で URIError を投げるので try/catch が要る

APIのクエリ組み立てやログの解読にどうぞ。入力はブラウザの外に出ない。

ぱんだツールズ では他にも Base64変換・JSON整形・文字コード変換・ハッシュ生成など、Web開発の細かい変換作業に効くブラウザ完結ツールを多数公開中。全部無料・登録不要・ファイルはサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?