为什么res.send被调用两次?

Jay*_*Jay 8 node.js express

我正在为快速路由器创建一个中间件,它将为每个请求和响应执行一些代码.拦截请求很容易,并且有很多例子,但我没有找到任何优雅的方法来拦截响应.经过一些研究,我提出的最好的方法是替换响应对象的send函数,如下面的代码片段所示:

const express = require('express');
const app = express();
var router = express.Router();
var port = process.env.PORT || 3000;

router.get('/test', function(req, res) {
    res.send({
        message: "testing"
    });
});

app.use(function(req, res, next){
    console.log("INTERCEPT-REQUEST");
    const orig_send = res.send;
    res.send = function(arg) {
        console.log("INTERCEPT-RESPONSE");
        orig_send.call(res, arg);
    };
    next();
});

app.use("/api", router);
app.listen(process.env.PORT || 3000) && console.log("Running");
Run Code Online (Sandbox Code Playgroud)

这种方法存在一个问题:出于某种原因,"INTERCEPT-RESPONSE"会在控制台中打印两次,这意味着res.send被调用两次......

我可以在第一次调用res.locals时设置一个标志,以避免两次处理响应,但我想知道为什么res.send被调用两次?

rsp*_*rsp 8

更好的例子

试试这段代码,看看传​​递给的参数是什么res.send:

const express = require('express');
const app = express();
var router = express.Router();
var port = process.env.PORT || 3000;

router.get('/test', function(req, res) {
    console.log('ACTUAL RESPONSE');
    res.send({
        message: "testing"
    });
});

app.use(function(req, res, next){
    console.log("INTERCEPT-REQUEST");
    const orig_send = res.send;
    res.send = function(arg) {
        console.log("INTERCEPT-RESPONSE", JSON.stringify(arguments));
        orig_send.call(res, arg);
    };
    next();
});

app.use("/api", router);
app.listen(process.env.PORT || 3000, function () {
    console.log("Running");
});
Run Code Online (Sandbox Code Playgroud)

(我还更改了"Running"的打印,以便在服务器实际监听时打印它 - 您的代码&&是在服务器监听之前打印的 - 但这并不重要.)

跑完后:

curl http://localhost:3000/api/test
Run Code Online (Sandbox Code Playgroud)

服务器控制台上的输出是:

Running
INTERCEPT-REQUEST
ACTUAL RESPONSE
INTERCEPT-RESPONSE {"0":{"message":"testing"}}
INTERCEPT-RESPONSE {"0":"{\"message\":\"testing\"}"}
Run Code Online (Sandbox Code Playgroud)

怎么了

正如您所看到的,您的处理程序实际上是由您的代码调用一次,而对象是第一个(也是唯一的)参数.但随后将一个序列化为JSON的对象再次调用.这是如何res.send内部工作原理-详情见下文.因为你把拦截函数放在实际的响应对象上,所以我猜它是用JSON参数调用自己,它甚至不知道它在此期间调用你的函数.

如何避免它

尝试使用自己序列化为JSON的对象:

const express = require('express');
const app = express();
var router = express.Router();
var port = process.env.PORT || 3000;

router.get('/test', function(req, res) {
    console.log('ACTUAL RESPONSE');
    res.send(JSON.stringify({
        message: "testing"
    }));
});

app.use(function(req, res, next){
    console.log("INTERCEPT-REQUEST");
    const orig_send = res.send;
    res.send = function(arg) {
        console.log("INTERCEPT-RESPONSE", JSON.stringify(arguments));
        orig_send.call(res, arg);
    };
    next();
});

app.use("/api", router);
app.listen(process.env.PORT || 3000, function () {
    console.log("Running");
});
Run Code Online (Sandbox Code Playgroud)

现在它打印:

Running
INTERCEPT-REQUEST
ACTUAL RESPONSE
INTERCEPT-RESPONSE {"0":"{\"message\":\"testing\"}"}
Run Code Online (Sandbox Code Playgroud)

res.send只召唤一次.

说明

现在,这是处理对象参数的代码res.json:

  if (chunk === null) {
    chunk = '';
  } else if (Buffer.isBuffer(chunk)) {
    if (!this.get('Content-Type')) {
      this.type('bin');
    }
  } else {
    return this.json(chunk);
  }
Run Code Online (Sandbox Code Playgroud)

请参阅:https://github.com/expressjs/express/blob/master/lib/response.js#L144-L154

你得到了else分支,它用你的参数调用this.json()(这是res.json()真的).

但是猜猜是什么 - 在这一行中res.json()打电话res.send():

return this.send(body);
Run Code Online (Sandbox Code Playgroud)

请参阅:https://github.com/expressjs/express/blob/master/lib/response.js#L250

在运行真实之前,它会调用您的拦截功能(第二次)res.send().

所以,神秘解决了.:)

  • 感谢@rsp的精彩解释,现在它完全有道理! (2认同)