kdnakt blog

hello there.

axiosを使っていて配列のはずのレスポンスがstringになった

今週の困ったことメモ。あまり解決してないけど書いておく。

[画面にデータが表示されない問題]

開発中のアプリケーションで、あるユーザでログインしたときだけ画面にデータが表示されないという問題が発生した。
開発者ツールを開くと、コンソールに次のようなエラーが記録されていた。

Uncaught (in promise) TypeError: e.reduce is not a function
  at a.mapData (xxx.js:4415:30) 

コードを読むと、あるAPIのレスポンスが配列であることを想定して次のようなコードが書かれていた。

mounted() {
  Promise.all([
    callApi("foo").then(data => this.foo = this.mapData(data, "hoge")),
    callApi("bar").then(data => this.bar = this.mapData(data, "fuga")),
    callApi("baz").then(data => this.baz = this.mapData(data, "piyo")),
  ]);
},
methods: {
  mapData(data, key) {
    data.reduce((map, obj) => (map[obj[key]] = obj.name, map), {})
  }
}

問題があったのはこのうち、bazのAPIを呼んだ部分で、開発者ツールでmapData()に渡されたdataの型を見ると、配列ではなく文字列になっていた。
reduce()関数はArray.prototype.reduce()で配列に生えているので、文字列型で利用できないのは仕方ない。

[axiosのissue]

callApi()の内部ではHTTPクライアントとしてaxiosというライブラリを利用して、サーバからデータを取得している。
ちょっと古いコードだったので、利用していたバージョンは2018年の0.18.0だった。

開発者ツールでサーバからのレスポンスを確認したが、レスポンス上は問題なく配列になっているように見えた。何も問題はなさそうに見える。ふと気になってレスポンスをPretty Printボタンで整形しようとしたところ、結果の表示にやたらと時間がかかる。よくよく確認してみると、レスポンスのサイズが5Mバイトとなっていた。

サイズがデカすぎるとレスポンスをうまくパースできないとかあるのか...?と思い、ググってみるとそれっぽいissueがあった。
JSON.parse()の例外を握りつぶしてしまっているというわけだ。

github.com

暫定的なものも含めて、解決策として以下のものが話し合われていた。

  • APIの返すデータが不正なJSON文字列だったのを直した
  • axiosもXMLHttpRequestもダメだけどfetchを使ったら問題なかった
  • データがデカすぎると発生するのでページングを実装しろ

データ量の問題か?と思ったが、なんとか開発者ツールでHTTPレスポンスを取得して、直接JSON.parse()メソッドに渡してみると、SyntaxError: Unexpected tokenのエラーが出ており、バックエンドのAPIが悪さをしているらしいことが分かった。
それはそれで別途直さないと...。

ちなみにこのissueは2018年8月にオープンされて、1年後の2019年8月に重複と言われてクローズされている。重複しているissueは以下の2015年のもの。

github.com

この修正は2021年にバージョン0.21.2でようやく対応され、オプションを設定すればJSONのパースエラーをaxios利用者がハンドリングできるようになった。

github.com

以下のように、axiosのインスタンスsilentJSONParsingオプションを渡すと例外をハンドリングできるようになるので、これでエラーの発生をUIで表現できるようになる。

await axios.get("https://run.mocky.io/v3/95e6ffc7-ac0c-46c2-aded-7149eeb2ef2d", {
  responseType: "json", 
  transitional: {
    silentJSONParsing: false
  }
});

SyntaxError: Unexpected token b in JSON at position 1
    at JSON.parse (<anonymous>)
    at Object.transformResponse (axios2.js:1023)
    at transform (axios2.js:493)
    at Object.forEach (axios2.js:257)
    at Object.transformData (axios2.js:492)
    at onAdapterResolution (axios2.js:1120)

[まとめ]

  • axiosはデフォルトではJSONのパースに失敗した場合例外を投げない
  • 0.21.2以降であればsilentJSONParsingオプションをfalseにすると、例外を投げるように変更できる
  • そもそもバックエンドのAPIで変なJSONを作らないようにすべき