Connect ソースコードリーディング(4) - bodyParser
bodyParser middleware
第4回目は Connect の組込み middleware である bodyParser を読んでいきたいと思います。
Node の http.ServerRequest は Java の HttpServletRequest などよりもかなりローレベルです。特に POST データはそのまま送信されてくるだけなので、プログラムで利用するためにはパースしてオブジェクト化する必要があります。そのパース処理をして、req.body プロパティに結果を格納してくれるのが、bodyParser です。Express でも bodyParser の利用を前提に設計されています。
connect/lib/middleware/bodyParser.js at 1.x · senchalabs/connect · GitHub
bodyParser が依存しているモジュール
bodyParser は以下の npm モジュールに依存しています。
Node には標準で querystring というモジュールがありますが、v0.3.x から以下のような nest 記法をサポートしなくなったので、connect では qs を利用している模様。
require('qs').parse('user[name][first]=tj&user[email]=tj'); // => { user: { name: { first: 'tj' }, email: 'tj' } }
formidable は multipart/form-madata (要するにファイルアップロード)の解析に利用されています。
bodyParser での処理の流れ
- req.body が既に設定済み(=パース済)であるかどうかチェック
- req.body が既に設定済みの場合は何もせずに後続の処理へ
- req.method が GET or HEAD でないかチェック
- GET or HEAD の場合は何もせずに後続の処理へ
- リクエストヘッダーの Content-Type から mime type を取得し、対応するパーサーを探す
- 対応するパーサがみつからない場合は何もせずに後続の処理へ
- 対応したパーサで POST データを解析して、後続の処理へ
ソースとしては以下の部分に当たります。
bodyParser.js 69行目〜
exports = module.exports = function bodyParser(options){ options = options || {}; return function bodyParser(req, res, next) { if (req.body) return next(); req.body = {}; if ('GET' == req.method || 'HEAD' == req.method) return next(); var parser = exports.parse[mime(req)]; if (parser) { parser(req, options, next); } else { next(); } } };
なお、コメントには書いてありますが、connect.bodyParser.parser.parse[contentType] = customParserObject とやると、任意の contentType に対するカスタムパーサを bodyParser の処理フローに混ぜ込むことができます。
対応している mime type
bodyParser.parse オブジェクトにはデフォルトで以下の3つの mime type に対応するパーサが定義されています。
- application/x-www-form-urlencoded
- application/json
- multipart/form-data
application/json に対応しているのが嬉しいですね。なお、mime タイプの取得には以下の mime メソッドが利用されています。
function mime(req) { var str = req.headers['content-type'] || ''; return str.split(';')[0]; }
ここで 'content-type' が小文字になっているのは、Node 本体で ServerRequest を生成する時に、リクエストヘッダーは全て小文字に変換されているからです。この処理は、Node 本体の http.js 83行目、parser.onHeadersComplete メソッド内に実装されています。リクエストヘッダーを操作する何かを作る場合は注意しましょう。
Node.js lib/http.js
parser.onHeadersComplete = function(info) { var headers = info.headers; var url = info.url; if (!headers) { headers = parser._headers; parser._headers = []; } // ... 省略 for (var i = 0, n = headers.length; i < n; i += 2) { var k = headers[i]; var v = headers[i + 1]; parser.incoming._addHeaderLine(k.toLowerCase(), v); // <= ここ } // ... 省略 }
bodyParser.parse['application/x-www-form-urlencoded']
通常の HTML form に対応するパーサです。リクエストデータを全て読み込んだ後、qs モジュールでパースした結果のオブジェクトを req.body プロパティに設定しています。なお、特に問題になることはないと思いますが、UTF-8 以外のエンコーディングには対応していません。
exports.parse['application/x-www-form-urlencoded'] = function(req, options, fn){ var buf = ''; req.setEncoding('utf8'); req.on('data', function(chunk){ buf += chunk }); req.on('end', function(){ try { req.body = qs.parse(buf); fn(); } catch (err){ fn(err); } }); };
bodyParser.parse['application/json']
application/x-www-form-urlencoded とほぼ同じですが、POST データのパースに JSON.parse メソッドを利用している点が異なります。
exports.parse['application/json'] = function(req, options, fn){ var buf = ''; req.setEncoding('utf8'); req.on('data', function(chunk){ buf += chunk }); req.on('end', function(){ try { req.body = JSON.parse(buf); fn(); } catch (err){ fn(err); } });
};
bodyParser.parse['multipart/form-data']
ファイルアップロードに対応するパーサです。処理としては、まず formidable モジュールで HTML input と file のそれぞれ解析した結果を、qs モジュールでパースして、req.body プロパティに格納しています。これにより、HTML input が通常の application/x-www-form-urlencoded と同じ形式で取得できるようになっています。
exports.parse['multipart/form-data'] = function(req, options, fn){ var form = new formidable.IncomingForm , data = {} , done; Object.keys(options).forEach(function(key){ form[key] = options[key]; }); function ondata(name, val){ if (Array.isArray(data[name])) { data[name].push(val); } else if (data[name]) { data[name] = [data[name], val]; } else { data[name] = val; } } form.on('field', ondata); form.on('file', ondata); form.on('error', function(err){ fn(err); done = true; }); form.on('end', function(){ if (done) return; try { req.body = qs.parse(data); fn(); } catch (err) { fn(err); } }); form.parse(req); };
実際に以下の HTML で試してみると、
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> </head> <body> <form action="/upload" method="POST" enctype="multipart/form-data"> <input type="text" name="title" value=""/> <input type="file" name="file"/> <input type="submit"/> </form> </body> </html>
テストコード
var connect = require('connect'), util = require('util'); var bodyDump = function(req, res, next) { console.log(util.inspect(req.body, true, 2)); next(); }; var server = connect.createServer( connect.bodyParser(), bodyDump, connect.static(__dirname + '/public') ); server.listen(3000);
title="test", file="calc.js" を指定した場合の結果は以下のようになります。
{ title: 'test', file: { size: 389, filename: [Getter], _writeStream: { bytesWritten: 389, busy: false, encoding: 'binary', fd: 8, flags: 'w', mode: 438, path: '/tmp/7ed91b5c2bcbe9da0cc213d50e8c52e1', drainable: true, writable: false, _queue: [Object] }, lastModifiedDate: Sun, 27 Nov 2011 07:50:59 GMT, length: [Getter], mime: [Getter], name: 'calc.js', path: '/tmp/7ed91b5c2bcbe9da0cc213d50e8c52e1', type: 'text/javascript' } }
次回予告
Session middleware の解説か、3rd Parser middleware の紹介の予定。