URLSearchParamsはクエリを単一値のオブジェクトではなく、順序を持つname-valueの組として扱います。filterのように同じkeyを複数送るAPIでは、この違いでリクエストの意味が変わります。WHATWG URL StandardではgetAll()が一致する値をリスト順に返すと定義され、MDN APIガイドは各メソッドの使用例を示しています。

色の異なる鍵と図形のカードが中央の灰色の機械に入り、記号付きで出てくる図

鍵と図形を機械に通して記号が変わる比喩です。順序付きname=valueの組やURLSearchParamsの実際の直列化を示す図ではありません。

重複値を読む・追加する・置き換える

次のqueryにはtagが2つあります。get()は最初の値を返し、getAll()は順番どおりに両方を返します。append()は既存の組を残して末尾に追加し、set()は同名の組を一つの値に置き換えて残りを削除します。

const params = new URLSearchParams("tag=blue&tag=green");
params.get("tag");       // "blue"
params.getAll("tag");    // ["blue", "green"]
params.append("tag", "red");
params.set("tag", "gold"); // tag=goldだけが残る

複数選択filterを維持するならappend()、単一設定値を更新するならset()が使えます。ただし、重複fieldを配列・最初の値・最後の値のどれとして読むかはサーバー側の仕様です。ブラウザーAPIがサーバーの解釈を決めるわけではありません。

+が空白か文字かは入力方法で変わる

文字列constructorにq=a+bを渡すとフォーム形式として解析され、+は空白になり、値はa bです。一方、append("q", "a+b")の+は文字データで、直列化時には%2Bになります。フォーム形式の直列化では空白が+と表されることがあります。元の表記、解析後の値、再直列化したqueryを区別してください。

new URLSearchParams("q=a+b").get("q"); // "a b"
const p = new URLSearchParams();
p.append("q", "a+b");
p.toString(); // "q=a%2Bb"

constructorは完全なURL parserではありません。new URL(location.href).searchParamsやnew URLSearchParams(location.search)のようにquery部分を渡します。

ブラウザーだけでなくAPI境界を試す

ユーザーがtag=blueとtag=greenを選んだ例なら、送信queryに2組が残るか、サーバーが意図した順序の配列として読むか、空文字とkey欠落を区別できるかを統合テストで確かめます。期待が違う場合はAPI契約またはserver parserを調整します。

論理値が同じでも、encodeやURLの直列化後にraw query文字列が異なることがあります。署名・cache key・厳密なlog比較にraw bytesを使うなら、順序、重複、空値、canonical化規則を先に定めてテストしてください。URLSearchParamsはqueryを組み立てるAPIであり、入力のsanitizer、認証、認可ではありません。

参考資料