express使ってる人たちから直近相談をうけて、「なるほどこりゃ難儀だな〜」となったことの紹介です。
qsのデフォルトの挙動
今回の焦点はクエリパーサーの振る舞いにあるのですが、express generatorで適当に雛形をつけてアプリケーションをつくって以下のようなリクエストなげてみます。
クエリがどのようにパースされるのかを見てみましょう。
/?q1[21]=21&q2[1]=1
req.queryの中身
{ q1: { '21': '21' }, q2: [ '1' ] }
一方はハッシュで、一方は配列として得られました。
境界値を探すとちょうど20が配列になるかハッシュになるかの境目のようです。
同じように数値インデックスを指定された配列型式のクエリなわけですがなぜこんな解析の差がうまれるのでしょう。
ソースを追っていくとexpressがquery parserとしてデフォルトで採用してるqsのソースに到達します。
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の内部に記述されており、アクセスはできないんですね。
なのでオプションを渡すことは難しいのでapp.setでquery parser自体を差し替えるなどにしないと対応が厳しいことがわかります。
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とかを使ってコード生成するとこんな感じの雛形ができると思います。
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をパースします。
が、ここではパラメータの数をarrayLimit
に指定する処理が記述されており、やはりここでもarrayLimit
にハマることになります。
ここでは最小コストでちょちょいとarrayLimitを0にもってくことが難しいのでquery側と同様の振る舞いに統一したい場合、ミドルウェアを差し替える必要があります。
結構大変ですよね。
めぼしいミドルウェアもあったりなかったりなので自作みたいな道になるかもしれません。
標準のquerystringに置き換えて安定させることも一つの手段
expressはデフォルトでqsライブラリにクエリパースを移譲するわけですが、設定でnode標準のquerystringにかえることもできます。
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の先行き不透明ということで却下されたという歴史があるようです。
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なパーサを渡せるようにしようみたいな提案もあって現在はそのどちらの提案もマージには至っていません。
qsのarrayLimitはどうなるの?
どうやら今後arrayLimit=0に寄せていこうという動きがあるみたいです。
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のベータ版が動き出したのでそのタイミングにうまく拾われてくれるといいですね。