Node与formidable异步上传文件:出现Can't set headers after they are sent

这几天在弄异步上传文件的事情,理想结果是前台选择文件ajax发送给node后台,选择了HTML5的FormData对象作为数据发送,然后后台返回它上传到服务器的绝对路径

问题

在发送了一次ajax请求后,后台正常响应,但是发送第二次时,后台出错了,json数据没有返回前台,但是文件却上传成功了

 **Can't set headers after they are sent**

希望各位指点一二,感谢。

前台代码:form1.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>粘贴上传图片</title>
</head>
<body>
<form action="/post" method="post" enctype="multipart/form-data" id="myForm">
    <input type='text' name="username">
    <input type="password" name="password">
    <input type="file" id="file" name="file" multiple="multiple">
    <input type="submit" value="提交">
    <a href="javascript:;" id="btn-ajax">点击异步提交</a>
</form>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>
<script>
    $(function() {
        $('#btn-ajax').on('click', function() {
            var formData = new FormData(document.getElementById('myForm'));
            var options={
                type:"post",
                url:'/post-ajax',
                data: formData,
                dataType:"json",
                /* 用h5的formdata时需要指定下面的两个为false */
                processData:false,
                //发送到服务器的数据。将自动转换为请求字符串格式。GET 请求中将附加在 URL 后。查看 processData 选项说明以禁止此自动转换。必须为 Key/Value 格式。如果为数组,jQuery 将自动为不同值对应同一个名称。如 {foo:["bar1", "bar2"]} 转换为 "&foo=bar1&foo=bar2"。
                contentType:false,
                // (默认: "application/x-www-form-urlencoded") 发送信息至服务器时内容编码类型。默认值适合大多数情况。如果你明确地传递了一个content-type给 $.ajax() 那么他必定会发送给服务器(即使没有数据要发送)
                success: function (data) {
    console.log(data);
                },
            };
            $.ajax(options);
        });

    });
</script>
</body>
</html>

后台代码:

var express = require('express');
var bodyParser = require('body-parser');

var app = express();
app.use(bodyParser.urlencoded({
    extended: true
}));

var fs = require('fs');
var formidable = require('formidable');
var form = new formidable.IncomingForm();
form.uploadDir = './tmp'; // 设置上传目录
form.multiples = true; // 设置为多文件上传
form.keepExtensions = true; // 保留文件后缀

var swig = require('swig');
app.engine('html', swig.renderFile);
app.set('views', './views');
app.set('view engine', 'html');
swig.setDefaults({
    cache: false,
    autoescape: false // 防止转义html代码
});

// 设置响应信息
var responseData = {
    message: '',
    paths: [] // 目的前台返回上传文件的路径
};

app.get('/', function(req, res) {
    res.render('ajax/form01');
});

app.post('/post-ajax', function(req, res) {

    form.parse(req, function(errs, fields, files) {
        if (errs) {
            console.log('出错了:' + errs.message);
            return;
        }

        // 其中file是前台上传文件的name值,为一个array
        for (var k = 0; k < files.file.length; k++) {
            try {
                fs.renameSync(files.file[k].path, 'tmp/' + 'test-' + files.file[k].name);
                responseData.paths.push(__dirname + '/tmp/' + 'test-' + files.file[k].name);
            } catch(e) {
                console.log('catch error:' + e.message);
            }
        } // endfor
        responseData.message = '成功';
        res.json(responseData);
        console.log('上传成功');
    })
});

app.listen(3000, function() {
    console.log('3000 开启成功');
});

在网络上查询到各种解决办法,但是尝试无果
如每次formidable监听end事件,返回request.end();

求解决方案,谢谢。

阅读 3.5k
3 个回答

express的每次请求命中路由调用必然是创建新的req、res对象啊,没道理会重复啊!带着疑惑查看了一下 formidable 的源码,发现了问题所在。

github incomming form parse方法源码

IncomingForm.prototype.parse = function(req, cb) {
  // ...
  // ...
  // ...

  // Setup callback first, so we don't miss anything from data events emitted
  // immediately.
  if (cb) {
    var fields = {}, files = {};
    this
      .on('field', function(name, value) {
        fields[name] = value;
      })
      .on('file', function(name, file) {
        // ... 
      })
      .on('error', function(err) {
        cb(err, fields, files);
      })
      .on('end', function() {
        cb(null, fields, files);
      });
  }

  //...
  //...
}

incomingForm 的 parse 方法会在注册一系列事件帮你暂存 fields 和 files 数据,最终在 error 或者 end 时调用你的 cb 方法并将数据给你,但是并没有卸载事件。当你第二次在同一个实例上调用 parse 方法时,第一次注册的这些事件回掉也会被执行,因此第二次 parse 结束时,第一次注册的 cb 函数也会被执行一次,这就导致了第一次的 res 上的 json 方法被再次执行!同理你的 rename 文件的操作爆出文件不存在也是因为这个原因。

按官方示例,每次 request 都需要重新 new IncomingForm 实例,或者你自己根据它的 parse 自己实现实例的复用,在 cb 执行前自动卸载事件。姨,等等,貌似不行哎,多个request 共享一个 IncommingForm 实例的话,如果存在大量并发,由于 parse 是异步的,在 parse 第一个 request 的 form 时,可能第二个 request 的 parse 方法也进入执行(两个ajax并发请求时),这就导致前一个 request 的 form 的 end 事件仍然会触发后一个 request 的 cb 的执行!

还是老老实实每个 request 都 new 一个自己用的 IncomingForm 吧。

Can't set headers after they are sent;
出现这个错误就表示。你在一个已经关闭的连接上又做一些写操作。
无论是res.json() 还是 res.send()
底层代码都是用res.end()结束。
也就是说如果你 res.json()后 又有出现了 res.xxxx()的操作
就会报上面的那个错误

clipboard.png

经过测试,你两次上传,使用的是同一个res对象,第一次上次的使用,已经调用了res.json方法,所以第二次上传,会报错.
同一个res对象无法返回两次数据给前端

第一次ajax提交时发送的头信息为:

clipboard.png

clipboard.png

后端返回的json数据为:

clipboard.png

第二次提交ajax请求时,出现:

clipboard.png

前台出现的错误是:

clipboard.png

后台出现的错误是:

clipboard.png

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题