express使うとqs周りで苦労しません？

express使ってる人たちから直近相談をうけて、「なるほどこりゃ難儀だな〜」となったことの紹介です。

## qsのデフォルトの挙動

今回の焦点はクエリパーサーの振る舞いにあるのですが、express generatorで適当に雛形をつけてアプリケーションをつくって以下のようなリクエストなげてみます。

クエリがどのようにパースされるのかを見てみましょう。

```
/?q1[21]=21&q2[1]=1
```

req.queryの中身
```javascript
{ q1: { '21': '21' }, q2: [ '1' ] }
```

一方はハッシュで、一方は配列として得られました。
境界値を探すとちょうど20が配列になるかハッシュになるかの境目のようです。

同じように数値インデックスを指定された配列型式のクエリなわけですがなぜこんな解析の差がうまれるのでしょう。


ソースを追っていくとexpressがquery parserとしてデフォルトで採用してるqsのソースに到達します。

https://github.com/ljharb/qs/blob/main/lib/parse.js#L12

```javascript
var defaults = {
    ...
    arrayLimit: 20,
    ...
}
...
} else if (
    !isNaN(index)
    && root !== cleanRoot
    && String(index) === cleanRoot
    && index >= 0
    && (options.parseArrays && index <= options.arrayLimit)
) {
    obj = [];
    obj[index] = leaf;
} else if (cleanRoot !== '__proto__') {
...
```

コードを要約すると配列型式のクエリでインデックスが指定されてる場合、`arrayLimit`までなら配列としてパースするがそれを超えるとハッシュとして扱うみたいな処理になっています。

うけるパラメータによっては、`obj[99999999] = 99999999`という処理が途中はさまることになるので配列が大きくなりすぎることをさけようとしてる感じですかね。

この辺の配慮が影響してこの解析結果の差がうまれているようです。


## arrayLimitは上書きできる？

expressはミドルウェアを定義できる機構があってそこでうまくやれそうな気がしますが、query stringのパースに関してはexpressの内部に記述されており、アクセスはできないんですね。

https://github.com/expressjs/express/blob/135a05c524796329a97f4bdb69a00f765dd80393/lib/application.js#L144

なのでオプションを渡すことは難しいのでapp.setでquery parser自体を差し替えるなどにしないと対応が厳しいことがわかります。

```javascript
const qs = require('qs');
app.set('query parser', str => qs.parse(str, {arrayLimit: 0}))
```

このような対応で一律`xxx[index]`をハッシュとして扱えるようになるかと思います。

## body-parserのurlencodedメソッドも同じ悩みを抱えている

これでquery stringをパースするときの不安定な挙動に対応できるようになりましたが、qsはbody-parser(urlencoded)の時にも同様に利用されています。

expressのgeneratorとかを使ってコード生成するとこんな感じの雛形ができると思います。

```javascript
var app = express();
...
app.use(express.json());
app.use(express.urlencoded({ extended: false })); // [1]
app.use(cookieParser());
...
```

上記のコードの[1]のところにurlencoded形式のreq.bodyをパースするためのmiddlewareがセットされています。

`extended:false`の場合、標準のquerystringが利用され、ネスト構造のパラメータが展開されないので、多くの人がquery側と挙動をあわせようと`extended: true`として利用してるのではないでしょうか

このexpress.urlencodedもまた、`extended:true`の設定の場合、qsに処理が移譲されてurlencodedなbodyをパースします。

https://github.com/expressjs/body-parser/blob/1.19.2/lib/types/urlencoded.js#L136

が、ここではパラメータの数を`arrayLimit`に指定する処理が記述されており、やはりここでも`arrayLimit`にハマることになります。

ここでは最小コストでちょちょいとarrayLimitを0にもってくことが難しいのでquery側と同様の振る舞いに統一したい場合、ミドルウェアを差し替える必要があります。

結構大変ですよね。

めぼしいミドルウェアもあったりなかったりなので自作みたいな道になるかもしれません。

## 標準のquerystringに置き換えて安定させることも一つの手段

expressはデフォルトでqsライブラリにクエリパースを移譲するわけですが、設定でnode標準のquerystringにかえることもできます。

```javascript
app.set('query parser', 'simple');
..
app.use(express.urlencoded({extended: false}))
```
これでarrayLimitから解放され挙動は安定しますが、ネストパラメータが展開されずにそのまま文字列として扱う煩わしさがあったりします。

理想郷に到達するのちょっと大変ですよね...


## body-parserはなんでqsにパラメータをそのままバイパスしないの？

qsはもともとtj製のライブラリですが、本人がnode開発からgoでのバックエンド開発に仕事がかわるからという理由でnode界隈から去りました。

その後しばらくして彼らのライブラリは別の開発者に引き継がれてメンテされるようになりました。

qsも当然同じような道を辿りました。

body-parserはちょうどこの引継ぎの混沌の最中でqsへのパラメータバイパス案が提案され、qsの先行き不透明ということで却下されたという歴史があるようです。

https://github.com/expressjs/body-parser/pull/98

> Because the qs module is officially been abandoned, we are going to switch to a different library, so we don't want to expose any options to qs through our API, otherwise we cannot easily switch to an alternative.

> qsモジュールは公式に放棄され、我々は別のライブラリに切り替える予定なので、我々のAPIを通してqsのオプションを公開したくない。
さもなければ我々は簡単に代替に切り替えることができない。


事情がかわってもう一回考えなそうぜという提案もあるし、それならgenericなパーサを渡せるようにしようみたいな提案もあって現在はそのどちらの提案もマージには至っていません。

https://github.com/expressjs/body-parser/issues/453

https://github.com/expressjs/body-parser/pull/282

## qsのarrayLimitはどうなるの？

どうやら今後arrayLimit=0に寄せていこうという動きがあるみたいです。

https://github.com/ljharb/qs/issues/107

> Alright it doesn't seem like there will be a nice way to handle this so I guess the changes which will set the default arrayLimit to 0 in the future version are more than good enough :) thanks for the discussion either way

> さて、これを処理する良い方法はなさそうなので、将来のバージョンでデフォルトのarrayLimitを0にする変更は十分すぎるほど良いと思います :) いずれにせよ、議論に感謝します。

expressもv5のベータ版が動き出したのでそのタイミングにうまく拾われてくれるといいですね。