パーセントエンコーディングの処理においてスペースの置き換え先はプラス記号か %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 はパーセントエンコーディングについての[解説記事](https://dev.twitter.com/oauth/overview/percent-encoding-parameters)を公開しています。

## JavaScript

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

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

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

```js
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` に置き換えます。

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

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

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

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

```js
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` に置き換えます。

```js
const querystring = require('querystring');

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

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

```js
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` を導入します。

```js
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` はデフォルトでスペースを `+`
 に置き換えます。
 
```python
>>> from urllib.parse import urlencode
>>> urlencode({"msg": "hello world"})
'msg=hello+world'
```

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

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

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

```python
>>> 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` をスペースに置き換えます。
 
```go
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` は `+` をスペースに変換しません。

```go
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
}
```