在Express / Node.js中调试“无法在发送后设置标头”错误的最佳方法是什么?

11
我将尝试调试 node.js Express 应用程序中可怕的“Can't set headers after they are sent”错误。具体来说,我正在使用 knox 库与 s3 进行通信。
大致上,我有这个 Express 处理程序,使用 knox s3client 的全局实例:
function foo(req, res) {
    //Region A
    var s3req = global.s3client.get('foo').on('response', function(s3res){
        //Region B
        res.set('content-length', s3res.headers['content-length']); //This will fail
        s3res.on('data', function(chunk){
            res.write(chunk);
        });
    });
    //Region C
    s3req.end();
}

如果我在区域A设置任何标题或状态代码在res上,一切正常。如果我在区域B尝试任何这样的操作,我会得到“无法在发送后设置标头”的错误。请注意,我想在res而不是s3res上设置标题。
可能在knox s3client的响应回调之前某处调用了response.writeHead。是否有一些调试标志或其他方法可以让Node输出writeHead何时/在哪里被调用?Node 0.10添加了response.headersSent,但是将我的代码和所有第三方库与此标志的检查填满以找出调用位置将是难以实现的。还是有其他方法可以解决这个问题吗?

我之前遇到过这个问题。我建议先尝试不同版本的Express,也就是升级或降级你的express.js。 - wayne
我已经尝试过Express 3.1和3.2.1。我还尝试过node 0.10.3和node 0.10.5。 - Anton I. Sipos
2个回答

12

您可以尝试包含一个简单的中间件,当调用 writeHead 时将堆栈跟踪转储到标准输出:

app.use(function(req, res, next) {
  res.on('header', function() {
    console.trace('HEADERS GOING TO BE WRITTEN');
  });
  next();
});

在可能触发问题的路由/中间件之前插入该代码。

顺便说一句,我猜测问题是由区域C中发生的某些事情(在那里调用了res.sendres.endres.jsonres.render)引起的:

function foo(req, res) {
    //Region A
    var s3req = global.s3client.get('foo').on('response', function(s3res){
        //Region B
        res.set('content-length', s3res.headers['content-length']);
    }); 
    //Region C
}

编辑 如果 s3res 是一个适当的流,您可以尝试这样做:

function foo(req, res) {
  global.s3client.get('foo').on('response', function(s3res) {
    s3res.pipe(res);
  }).end();
}

但请注意,S3请求返回的所有标题将传递给Express响应。


感谢您的回复。我已经编辑了更多细节的问题。实际上,我在区域C中调用了s3req.end()(请参见更新的问题)。但是,如果我将其删除,则会出现“错误:套接字挂起”。如果我只想将s3res流回foo的res,并设置内容类型以及内容长度标头,那么最好的解决方案是什么? - Anton I. Sipos
s3req.end() 不应该是问题,因为它不是 Express 的一部分。我猜这取决于你在什么上下文中调用 foo(),虽然(正如 @wayne 已经建议的那样)我也记得 Node 存在一些问题,会出现这种毫无明显原因的问题。 - robertklep
foo只是我的API方法之一,注册为express get处理程序。该方法的高级目的是返回私有s3存储桶的内容(顺便说一句,我意识到我可以使用过期的签名S3 URL来实现此目的。但是现在我需要保留向后兼容性,以前我使用s3)。是否有更简单的方法将s3res代理/附加回res(API方法的调用者看到的响应)?像我现在这样手动操作是容易出错的,正如你所看到的。 - Anton I. Sipos
看看我的修改。不确定是否有效,但值得一试 :) - robertklep
标记为已接受。你的所有建议都是正确的。你建议记录writehead的“中间件”起作用了,并指向我的代码中在一个假错误情况下调用流的end()的位置。正如你所怀疑的那样,它“实际上”处于C区域(尽管实际上比我简化的代码更复杂)。你的pipe建议也很有价值,我甚至能够在调用pipe将s3res传输到res之前设置自己的头部,这些头部出现在实际响应中。所以一切都好了 - 非常感谢! - Anton I. Sipos

4

我不确定这是否适用于此处,但当我看到错误时,我立即想到的是“模型层”中某个地方的“回调逻辑”中出现了一个非常简单的错误。在两个不同的实例中,我都在苦思冥想,最终发现我调用了回调(即最终会调用响应写入回调的回调)两次。这只会在我的模型层出现错误时发生,因为代码如下:

if (err) cb(err) // note the missing "return" here
cb(null,result) // alternatively, also no "else"

如果我现在遇到这样的错误,我首先要做的事情就是检查是否有任何操作导致响应写回调函数被调用两次。只需要在区域A(Knox请求启动的范围)和区域C(响应写回调函数)中各添加一条console.log语句即可,以消除这种可能性。
原则上,您的res对象应该非常受保护。只有连接/表达式中间件和特定的请求处理程序才能访问它。因此,只有有限的几个地方可以编写标头。

1
我刚遇到了同样的问题,显然比那更复杂,但基本上就是这样发生的! - A.M.K
真的,后续版本的Express似乎对这种错误的容忍度更低了。 - Robert Moskal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接