2016-09-12 21:28:13

Node.js: 让express 4.X 解析xml请求

使用expresss-generator创建出来的项目中, 默认使用的是bodyParser.json()这个中间件

app.use(bodyParser.json());

这将会将所有body为xml格式的请求全部解析为空对象{}

本文讨论如何拿到原始xml字符串数据.

express: req.rawBody

在早期的express 1.X中, req.rawBody默认存在,但是在1.5.1之后被取消.

经过查询express 4.X的API,并没有类似rawBody的属性与方法,因此, 考虑从中间件层面拿到原始数据

bodyParser.xml

既然默认使用bodyParser.json(),那么,有没有bodyParser.xml()呢?

查询body-parser源码, 发现内置四种解析方式, 并不包括xml, 代码如下:

/**
 * JSON parser.
 * @public
 */

Object.defineProperty(exports, "json", {
  configurable: true,
  enumerable: true,
  get: createParserGetter("json")
})

/**
 * Raw parser.
 * @public
 */

Object.defineProperty(exports, "raw", {
  configurable: true,
  enumerable: true,
  get: createParserGetter("raw")
})

/**
 * Text parser.
 * @public
 */

Object.defineProperty(exports, "text", {
  configurable: true,
  enumerable: true,
  get: createParserGetter("text")
})

/**
 * URL-encoded parser.
 * @public
 */

Object.defineProperty(exports, "urlencoded", {
  configurable: true,
  enumerable: true,
  get: createParserGetter("urlencoded")
})

分析源码可以知道, body-parser中间件利用Object.defineProperty设置了四种类型输出,那么,我们同样可以利用Object.defineProperty来新增一个xml格式的解析器

实际上,已经有一个body-parser-xml利用上面的思路来添加xml了

安装

npm install --save body-parser-xml

导入

var bodyParser = require("body-parser");
require("body-parser-xml")(bodyParser);

使用

app.use(bodyParser.xml({
  limit: "1MB",   // Reject payload bigger than 1 MB
  xmlParseOptions: {
    normalize: true,     // Trim whitespace inside text nodes
    normalizeTags: true, // Transform tags to lowercase
    explicitArray: false // Only put nodes in array if >1
  },
  verify: function(req, res, buf, encoding) {
    if(buf && buf.length) {
      // Store the raw XML
      req.rawBody = buf.toString(encoding || "utf8");
    }
  }
}));

app.use(bodyParser.json());

如上,我们就可以通过req.rawBody拿到原始xml数据了.

但是,还是有一些问题尚未解决,比如中间件bodyParser.xmlbodyParser.json的调用顺序是如何的, 比如verify函数是在哪里被调用的

body-parser-xml源码

  function xml(options) {
    options = options || {};

    options.type = options.type || DEFAULT_TYPES;
    if(!Array.isArray(options.type)) {
      options.type = [options.type];
    }

    var textParser = bodyParser.text(options);
    return function xmlParser(req, res, next) {
      // First, run the body through the text parser.
      textParser(req, res, function(err) {
        if(err) { return next(err); }
        if(typeof req.body !== "string") { return next(); }

        // Then, parse as XML.
        var parser = new xml2js.Parser(options.xmlParseOptions);
        parser.parseString(req.body, function(err, xml) {
          if(err) {
            err.status = 400;
            return next(err);
          }

          req.body = xml || req.body;
          next();
        });
      });
    };
  }

分析源码,可以看到, 这里通过闭包调用了bodyParser.text这个纯文本解析器,传入的参数对象中,如下xmlParseOptions对象纯粹被拿去调用xml2js.

  xmlParseOptions: {
    normalize: true,     // Trim whitespace inside text nodes
    normalizeTags: true, // Transform tags to lowercase
    explicitArray: false // Only put nodes in array if >1
  },

再通过搜索verify, 发现verifybody-parser中的read.js被调用,相关代码如下:

function read (req, res, next, parse, debug, options) {

  // read options
  var encoding = opts.encoding !== null
    ? opts.encoding || "utf-8"
    : null
  var verify = opts.verify

/******some code******/

    // verify
    if (verify) {
      try {
        debug("verify body")
        verify(req, res, body, encoding)
      } catch (err) {
        // default to 403
        setErrorStatus(err, 403)
        next(err)
        return
      }
    }

   }

body是调用raw-body时回调函数的第二个参数,即请求中的body字符串,至于回调函数的第一个参数,当然是error

因此,verify(req, res, body, encoding)调用了express的req,res对象,以及body和encoding(在此处为null),于是第三个参数就是我们要拿到的原始数据了

所以,如下语句能够成功拿到rawBody

  verify: function(req, res, buf, encoding) {
    if(buf && buf.length) {
      // Store the raw XML
      req.rawBody = buf.toString(encoding || "utf8");
    }
  }

中间件bodyParser.xmlbodyParser.json的调用顺序

express的中间件思想相当直接的, 为顺序执行,通过在中间件尾部调用next()来继续下一个中间件

因此, bodyParser.xml在本文中先于bodyParser.json执行, 只要执行无错误, bodyParser.json会继续执行

然而,bodyParser中间件内置了一种检查机制,只会执行bodyParser其中的一个中间件, 之后会全部跳过

中间件的优化点

关注如下代码

    // verify
    if (verify) {
      try {
        debug("verify body")
        verify(req, res, body, encoding)
      } catch (err) {
        // default to 403
        setErrorStatus(err, 403)
        next(err)
        return
      }
    }

    // parse
    var str
    try {
      debug("parse body")
      str = typeof body !== "string" && encoding !== null
        ? iconv.decode(body, encoding)
        : body
      req.body = parse(str)
    } catch (err) {
      err.body = str === undefined
        ? body
        : str

      // default to 400
      setErrorStatus(err, 400)

      next(err)
      return
    }

可以明显发现,verify验证后还继续执行了parse来进行文本解析, 如果是只要拿到原生的rawBody,那么这一步就是不需要的.另外,body-parser-xml还调用了xml2js,这也会对rawBody造成了一些性能损失.

但是,这并不是说我们可以轻易写出一个拿到rawBody的中间件来, 因为body可能涉及到deflate或者gzip压缩,以及stream的后续操作

本文链接:https://smallpath.me/post/express-body-parser-xml

-- EOF --