文字コードを指定してURLエンコードを行う（Node.js）

文字コードを指定してURLエンコードしたい。日本語（が含まれるん）だもの。
そういう話です。

# encodeURI()はUTF-8を表すエスケープシーケンスで置換される
Node.jsを用いて、あるAPIをGETメソッドでリクエストする処理を実装していました。

そのAPIはクエリストリングを
`APIの名前?key1=value1&key2=value2&signature=認証用の値`
にしてリクエスト送信してくださいね、ということで下記のようなソースでクエリストリングを作成。

```js:makeQueryString.js
// 例えばユーザーの情報を取得するAPI
// API名称
const apiName = 'getUser';

// リクエスト内容
const params = {
    name : '則巻アラレ',
    age  : 13
}

/**
 * クエリストリングを作成する
 * @param {String} apiName API名称
 * @param {Object} params  リクエスト内容
 * @returns クエリストリング
 */
function makeQueryString(apiName, params) {
    let queryString = `${apiName}?`;
    // リクエスト内容分ループ
    for (const key in params) {
        const value = params[key]; // 値
        queryString += `${key}=${value}&`;
    }
    // 末尾に認証用の値を付与
    queryString += `signature=xxx`;
    // URLエンコード
    return encodeURI(queryString);
}

// クエリストリングの作成
const queryString = makeQueryString(apiName, params);
```

しかし、`encodeURI()`[^1]を用いてURLエンコードを行ったクエリストリングを使って、GETリクエストを送信しても、**該当ユーザーがいない**、というレスポンスばかりが返ってきていました:fearful:


認証エラーが返ってきていないので、認証は通っている...。
何がダメなんだ？とそのAPIの開発者向けドキュメントを再読すると、以下の記述。

「**Windows-31J**でURLエンコードを行ってください」

**文字コード CP932 / Windows-31J**

> CP932とは、日本語の文字などを収録した文字コード規格の一つで、Shift JIS規格を元にマイクロソフト（Microsoft）社が独自に拡張したもの。微妙に異なる複数の仕様がある。
〔......〕同社（注: Microsoft社）はCP932のインターネット上での識別名としてIANAに「Windows-31J」を登録し〔......〕
[CP932とは - IT用語辞典](https://e-words.jp/w/CP932.html)

https://e-words.jp/w/CP932.html

ぶっちゃけこの文字コード初めて聞いたな...と思いつつ、
`encodeURI()`について確認すると、確かに**representing the UTF-8 encoding of the character**[^2]と記載があるので、UTF-8の文字を表すエスケープシーケンスで置換されるようです。

先ほどのコードだと、以下クエリストリングは

```bash
getUser?name=則巻アラレ&age=13&signature=xxx
```

`encodeURI()`によって、以下に変換されます。
name部分は、**UTF-8**の文字を表す（らしい）エスケープシーケンスで置換されています。

```bash
getUser?name=%E5%89%87%E5%B7%BB%E3%82%A2%E3%83%A9%E3%83%AC&age=13&signature=xxx
```


今回は、URLエンコードが行われた文字列を、Windows-31Jの文字列として表す必要があるので、標準の関数は使えないですね:pensive:

# 文字コードを指定してURLエンコードができるiconv-urlencode

やっと本題。
UTF-8以外の文字コードを指定してURLエンコードを行う必要があるなら、
`iconv-urlencode`というパッケージを用いることで、それが可能です。

https://www.npmjs.com/package/iconv-urlencode

上記サイトの説明によると、`iconv-lite`パッケージで指定可能な文字コード[^3]であれば、URLエンコード/デコード可能なようです。日本語ではShift_JIS, Windows-31j等が使用可能です。

以下の実行環境で実施していきます。

```bash
$ node --version
v14.17.4

$ npm --version
6.14.14
```

インストール。

```bash
npm install iconv-urlencode
```

あとは先ほどのソースコードに、モジュールの読み込みを追加し、
関数内で作成したクエリストリングをURLエンコードして返すようにします。

```js:makeQueryString.js
const  conv = require('iconv-urlencode');
const encoding = 'Windows-31j'; // 文字コード

// ......
// 略
// ......

function makeQueryString(apiName, params) {
    let queryString = `${apiName}?`;
    // リクエスト内容分ループ
    for (const key in params) {
        const value = params[key]; // 値
        queryString += `${key}=${value}&`;
    }
    // 末尾に認証用の値を付与
    queryString += `signature=xxx`;
    // 文字コードを指定し、URLエンコード
    return conv.encode(queryString, encoding); // ★iconv-urlencodeを利用
}
```
...と上記ソースコードだと実行結果がこうなります。

```bash
# getUser?name=則巻アラレ&age=13&signature=xxx のURLエンコード結果
getUser%3Fname%3D%91%A5%8A%AA%83A%83%89%83%8C%26age%3D13%26signature%3Dxxx
```

今回は`?`, `=`, `&`は変換されてほしくないので、修正。
値のみURLエンコードするようにします。

```js:makeQueryString.js
function makeQueryString(apiName, params) {
    let queryString = `${apiName}?`;
    // リクエスト内容分ループ
    for (const key in params) {
        // ★値のみURLエンコードを行う（文字コードを指定）
        const value = conv.encode(params[key], encoding);
        queryString += `${key}=${value}&`;
    }
    // 末尾に認証用の値を付与
    queryString += `signature=xxx`;
    return queryString;
}

```

結果は以下のようになりました。

```bash
# getUser?name=則巻アラレ&age=13&signature=xxx のURLエンコード結果(Windows-31J)
getUser?name=%91%A5%8A%AA%83A%83%89%83%8C&age=13&signature=xxx
```

これで無事に文字コードWindows-31Jの文字列として、サーバ側でデコードができて、
指定ユーザーの情報がAPIのレスポンスで返ってくるようになりましたとさ、めでたしめでたs

......残念ですが、このままではある条件下でエラーになる場合があります。

## 値だけURLエンコードをしていると、数字の0が消える

それは、以下のユーザーで検索を行ったときに起きました...。

```js
// リクエスト内容
const params = {
    name : '則巻ターボ',
    age  : 0
}
```

```bash
# 作成されたクエリストリング
getUser?name=%91%A5%8A%AA%83%5E%81%5B%83%7B&age=&signature=xxx
```

おわかりいただけただろうか...。
そう、ageの値が欠けているのだ...。

ageの値を文字列の`'0'`とした場合はクエリストリングに`age=0&...`となりますが、
数字の`0`であった場合は、`age=&...`として値が欠けてしまいます。


今回は値が数字の0であった場合は、URLエンコードしないようにしました。

```js
const value = params[key] === 0 ? params[key] : conv.encode(params[key], encoding);
```

```bash
# getUser?name=則巻ターボ&age=0&signature=xxx のURLエンコード結果(Windows-31J)
getUser?name=%91%A5%8A%AA%83%5E%81%5B%83%7B&age=0&signature=xxx
```

ひとまず、（今回は）ヨシ！

最終的に、クエリストリング作成関数は以下のようになりました。

```js:makeQueryString.js
function makeQueryString(apiName, params) {
    let queryString = `${apiName}?`;
    // リクエスト内容分ループ
    for (const key in params) {
        // 値のみURLエンコードを行う（文字コードを指定）
        // 値が数字の0の時はURLエンコードを行わない
        const value = params[key] === 0 ? params[key] : conv.encode(params[key], encoding);
        queryString += `${key}=${value}&`;
    }
    // 末尾に認証用の値を付与
    queryString += `signature=xxx`;
    return queryString;
}

```


# URLエンコードのまとめ
- JavaScriptの`encodeURI()`/`encodeURIComponent()`関数は**UTF-8の文字を表すエスケープシーケンス**で置換される
- UTF-8以外の文字コードを指定する場合は、`iconv-urlencode`パッケージを使うと可能
    - 数字の`0`や`false`はURLエンコード時に消えてしまうので、それらはエンコードしないようにする

---

というわけで、文字コードを指定して、URLエンコードを行う方法でした。


（今回は本筋から逸れるので記載していませんが、実際は入力値のバリデーションチェックもしています）

[^1]: 利用するAPIの仕様から「=」と「&」をエンコードさせないために`encodeURI()`を使用しています。`encodeURIComponent()`との違いは[MDN Web Docs](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/encodeURI#examples)に例として記載があります
[^2]: 翻訳が若干分かりにくかったので、[英語版](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI)から引用。ちなみに`encodeURIComponent()`も同様にUTF-8の文字列として表される。
[^3]: [iconv-liteのgithub内のwiki](https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings)にサポートしている文字コードの一覧があります。Windows-31jが！指定！できる！