Node.jsでFetch APIで取得したShift_JISのテキストをTextDecoderではなくnpmのパッケージを使ってデコードする


#Overview

Node.jsでFetchでHTMLのデータを取得して`response.text()`でテキストを取得したら文字化けが:scream:
どうしてだろうと思ったらHTMLがShift-JIS、かつ`text()`はまさかのUTF-8専用とのこと。
https://developer.mozilla.org/ja/docs/Web/API/Body/text

> レスポンスは常に UTF-8 としてデコードします。

さすがモダンなAPIだな、この割り切り嫌いじゃない:wink:

それならばとブラウザ同様TextDecoderを使ってShift-JISに変換しようとしたらエラー[ERR_ENCODING_NOT_SUPPORTED]が:fearful:
Node.jsのドキュメントみてもShift-JISサポートあるやん！
https://nodejs.org/docs/latest/api/util.html#util_class_util_textdecoder

ググってみたらICUとかいうのが必要なようで…
マイナーなデータは別途提供するから自分で取り込んでねとごもっともな意見。
http://var.blog.jp/archives/80396639.html

私はCloud Build(CI/CD)やCloud Functions(FaaS)上でも動作するようなお手軽なのを求めているため、ICUは使わない方向で検討しました。

ちなみにブラウザの場合は、ブラウザ自体が多数の文字コードを扱っているため工夫いらずで変換できる模様。
@kerupani129さんが記事を書かれています。
https://qiita.com/kerupani129/items/6646eb920c23658bc525

#Target reader

- Shift_JISのテキストをnpmのパッケージだけでデコードしたい方

#Prerequisite

- バックエンドはCloud Build(CI/CD)やCloud Functions(FaaS)を利用する、つまりNodeの起動オプションを指定するようなことはできない。
- Node.jsのバージョンはCloud Functionに依存しているため、現時点ではV10系とする。
- ソースコードは`import/export`を使ってブラウザと記述を統一できるよう、esmというパッケージを利用している。


#Body

##2つの選択肢

npmだけで行こうとすると選択肢は二つある。

- `full-icu`をインストールしてTextDecoderを利用する。
- `iconv-lite`の類の独自の文字コード変換で処理する。

今回何より大事にしたいのはどこでもインストールエラー等なく動くことを最優先とする。
前者の`full-icu`はICUをダウンロードするようで、サイズの大きさの懸念と、何よりダウンロードがCloud Buildで利用できるかの懸念がある。
ローカルマシンと比較してフルマネージドサービスではいろいろと制約があるため、インストール時に権限等により失敗することが少なくない。
また、Cloud FunctionsではNode.jsの起動オプションは指定できず、環境変数についても動作するか不明。

ということで、後者のパッケージで文字コード変換を完結するものを利用する。

##iconv-lite

iconv-liteの週間ダウンロード数は2千万でversionが1.00に到達していないが十分すぎる人気。
https://www.npmjs.com/package/iconv-lite

Node.jsなのでパッケージサイズは気にならないが、念のため調べるとMINIFIED + GZIPPEDで150KBなので、フロントで利用するわけじゃないので合格。
package.json覗いてみたところ、依存関係も1つと素晴らしい。
https://bundlephobia.com/result?p=iconv-lite@latest

そして、私が作ったFetchAPIのソースコード。
Shift-JIS対策だけではなく、タイムアウトや認証エラーが含まれるため少し複雑になっている。
Shift-JISの変換は、`fetchText()`のoption.sjisの部分で、iconv-liteの`decode()`を利用しているだけ。
`decode()`にはBufferを渡す必要があるため、`Buffer.from()`でresponseを変換している。

responseをみて文字コード指定も考えたが、ヘッダーになかったり指定が間違っていることもありえるので、割り切ってShift-JISのURLの場合にはsjisオプションを付与してコールしている。

```javascript:utils/fetch.js

import iconv from 'iconv-lite';
import fetch from 'node-fetch';
import AbortController from 'abort-controller';
// for "reason: unable to verify the first certificate"
// see: https://github.com/node-fetch/node-fetch/issues/15#issuecomment-533869809
import https from "https";
const agent = new https.Agent({
    rejectUnauthorized: false
});

const fetchCore = async (url, option = {}) => {
    // set timeout after 15s
    const controller = new AbortController();
    const timeout = setTimeout(() => {
        controller.abort();
    }, option.timeout || 15000);

    try {
        const response = await fetch(url, {
            method: 'GET',
            mode: 'cors',
            cache: 'default',
            agent: option.resolveUnauthorized ? agent : undefined,
            signal: controller.signal, // for timeout
            ...option,
        });

        if (!response.ok) {
            const description = `status code:${response.status} , text:${response.statusText}`;
            throw new Error(description);
        }

        return response;

    } finally {
        clearTimeout(timeout);
    }
}

const fetchJson = async (url, option) => {
    const response = await fetchCore(url, option);
    return await response.json();
}

const fetchText = async (url, option) => {
    const response = await fetchCore(url, option);

    if (option.sjis) {
        return iconv.decode(Buffer.from(await response.arrayBuffer()), "shift_jis")
    }

    return await response.text();
}

export { fetchJson, fetchText };

```

#Conclusion

ブラウザを使っているともはや文字コードなんて気にすることもなかったが、改めブラウザ大変だなと実感。
Shift-JISはあと十年くらいは生存していそうなので、CI/CD環境下ではiconv-lite様様といったところ。


Have a great day!