Python
JavaScript
Go

パーセントエンコーディングの処理においてスペースの置き換え先はプラス記号か %20 か?

概要

いくつかの言語でパーセントエンコーディングの処理においてスペースの置き換え先がプラス記号か %20 であるかどうかを調べました。

URL のクエリ文字列や application/x-www-form-urlencoded のボディを求めるためとき、HTML5 や URL Standard の仕様ではスペースはプラス記号 (+) に変換されることが求められます。

一方でパーセントエンコーディングの明確な仕様がなかったことから、HTTP サーバーは +%20 の両方をスペースとして扱われます。サーバーサイドの言語では application/x-www-form-urlencoded のためのエンコーディングメソッドもしくはオプションが提供されず、RFC 3986 の仕様にもとづいたエンコーディングメソッドで代用することがあります。

多くの場合、スペースの置き換え先が (+) かパーセントエンコーディング (%20) か意識する必要はありませんが、OAuth の場合、問題になります。

OAuth 1.0 の場合、スペースに対してパーセントエンコーディング (RFC 3986) が要求されるために、プラス記号にエンコーディングする方法を採用したライブラリではエラーになります。Twitter はパーセントエンコーディングについての解説記事を公開しています。

JavaScript

URLSearchParams は URL Standard にもとづきます。スペースはプラス記号に置き換わります。

const params = new URLSearchParams();
params.append("msg", "hello world");
console.log("msg=hello+world" === params.toString());

URLSearchParams%20 もスペースとして扱います。解析した結果は次のとおりです。

const params2 = new URLSearchParams("msg=hello+world");
console.log("hello world" === params2.get("msg"));

const params3 = new URLSearchParams("msg=hello%20world");
console.log("hello world" === params3.get("msg"));

encodeURIComponent の場合、スペースを %20 に置き換えます。

console.log("hello%20world" === encodeURIComponent("hello world"));

スペースをプラス記号に置き換えたいのであれば、replace の呼び出しを追加します。

const ret = encodeURIComponent("hello world").replace(/%20/g, '+')
console.log("hello+world" === ret);

decodeURIComponent%20 をスペースに置き換えますが、プラス記号は置き換えません。

console.log("hello world" === decodeURIComponent("hello%20world"));
console.log("hello+world" === decodeURIComponent("hello+world"));

Node.js

Node.js v7.0 からサポートされるようになった URLSearchParams を優先的に使うほうがよいでしょう。v7.0 以前の以前の方法を示します。

標準の querystring.stringify モジュールはスペースを %20 に置き換えます。

const querystring = require('querystring');

const ret = querystring.stringify({ msg: "hello world" });
console.log("msg=hello%20world" === ret);

querystring.parse+%20 の両方をスペースに置き換えます。

const querystring = require("querystring");

console.log("hello world" === querystring.parse("msg=hello+world")["msg"]);
console.log("hello world" === querystring.parse("msg=hello%20world")["msg"]);

RFC 3986 の仕様にもとづいたエンコーディングが必要であれば、ljharb/qs を導入します。

const qs = require("qs");

console.log("msg=hello%20world" === qs.stringify({ msg: "hello world" }));
console.log("hello world" === qs.parse("msg=hello+world")["msg"]);
console.log("hello world" === qs.parse("msg=hello%20world")["msg"]);

Python 3

urllib を使います。urlencode はデフォルトでスペースを +
に置き換えます。

>>> from urllib.parse import urlencode
>>> urlencode({"msg": "hello world"})
'msg=hello+world'

quote_via=quote を指定すれば、スペースは %20 に置き換わります。

>>> from urllib.parse import urlencode, quote
>>> urlencode({"msg": "hello world"}, quote_via=quote)
'msg=hello%20world'

parse_qsparse_qsl の両方とも +%20 をスペースに置き換えます。

>>> from urllib.parse import parse_qs
>>> parse_qs("msg=hello+world")
{'msg': ['hello world']}
>>> parse_qs("msg=hello%20world")
{'msg': ['hello world']}

Go

net/url モジュールを使います。url.Values 型の Encode はスペースを + に置き換えます。ParseQuery+ と %20 をスペースに置き換えます。

package main

import (
    "fmt"
    "net/url"
)

func main() {
    v := url.Values{}
    v.Set("msg", "hello world")
    fmt.Println(v.Encode())
  // msg=hello+world


  m, _ := url.ParseQuery("msg=hello+world")
  fmt.Println(m["msg"][0])
  // hello world

  m2, _ := url.ParseQuery("msg=hello%20world")
  fmt.Println(m2["msg"][0])
}

QueryEscape がスペースを + に変換するのに対して PathEscape%20 に変換します。さらに PathUnescape+ をスペースに変換しません。

func main() {
  fmt.Println("QueryEscape")
  fmt.Println(url.QueryEscape("hello world"))
  // hello+world
  fmt.Println(url.QueryUnescape("hello+world"))
  // hello world
  fmt.Println(url.QueryUnescape("hello%20world"))
  // hello world

  fmt.Println()
  fmt.Println("PathEscape")
  fmt.Println(url.PathEscape("hello world"))
  // hello%20world
  fmt.Println(url.PathUnescape("hello+world"))
  // hello+world
  fmt.Println(url.PathUnescape("hello%20world"))
  // hello world
}