【问题标题】:How to cancel HTTP upload from data events?如何从数据事件中取消 HTTP 上传?
【发布时间】:2016-05-13 14:25:38
【问题描述】:

鉴于这个简单的网络服务器代码:

console.log('starting');
var server = require('http').createServer();

server.on('connection',function(socket){console.log('*server/connection');});
server.on(
    'request',
    function(request, response){
        console.log('*server/request');
        request.on(
            'data',
            function(chunk){
                console.log('*request/data');
                // <!> How do I abort next data calls from here?
            }
        );
        request.on(
            'readable',
            function(chunk){
                console.log('*request/readable');
                // <!> How do I abort next readable calls from here?
            }
        );
        request.on(
            'end',
            function(){
                console.log('*request/end');
                response.writeHead(200,"OK");
                response.write('Hello');
                response.end();
            }
        );
        request.on('close',function(){ console.log('*request/close'); } );
        request.on('error',function(){ console.log('*request/error'); } );
    }
);
server.on('close',function(){console.log('server/close');});
server.on('checkContinue',function(request, response){console.log('*server/checkContinue');});
server.on('connect',function(request, socket, head){console.log('*server/connect');});
server.on('upgrade',function(request, socket, head){console.log('*server/upgrade');});
server.on('clientError',function(exception, socket){console.log('*server/clientError');});

server.listen(8080);
console.log('started');

当提交 POST 或 FILE 时,我的 on data 功能会被触发一次或多次或多次。有时(比如发送了一个巨大的大文件)我想在数据事件上取消它并触发用户的 on end 功能(稍后我会显示“你的帖子/文件太大”) .我该怎么做?

【问题讨论】:

    标签: node.js


    【解决方案1】:

    这里要做的正确的、符合规范的事情就是尽早发送一个 HTTP 413 响应——也就是说,只要你检测到客户端发送的字节数超过了你想要处理的字节数。发送错误响应后是否终止套接字取决于您。这符合 RFC 2616:(已添加重点)

    413 请求实体太大

    服务器拒绝处理请求,因为请求实体比服务器愿意或能够处理的要大。 服务器可以关闭连接以阻止客户端继续请求。

    接下来发生的事情并不理想。

    • 如果您让套接字保持打开状态,所有浏览器(Chrome 30、IE 10、Firefox 21)将继续发送数据,直到上传整个文件。然后,只有这样,浏览器才会显示您的错误消息。这真的很糟糕,因为用户必须等待整个文件完成上传,才发现服务器拒绝了它。它还会浪费您的带宽。

      浏览器当前的行为违反了 RFC 2616 § 8.2.2:

      发送消息体的 HTTP/1.1(或更高版本)客户端应该在传输请求时监控网络连接的错误状态。如果客户端看到错误状态,它应该立即停止传输正文。如果使用“分块”编码(第 3.6 节)发送正文,则可以使用零长度的块和空的尾部来提前标记消息的结尾。如果正文前面有 Content-Length 标头,则客户端必须关闭连接。

      存在open ChromeFirefox 问题,但不要指望很快会得到修复。

    • 如果在发送 HTTP 413 响应后立即关闭套接字,所有浏览器显然会立即停止上传,但 它们 大多数当前显示“连接重置”错误(或类似的),而不是您可能在响应中发送的任何 HTML。

      同样,这可能违反了规范(允许服务器提前发送响应并关闭连接),但我也不希望浏览器很快修复。

      更新:自 4 月 15 日起,Chrome 可能在您提前关闭连接时显示您的 413 HTML。这仅在浏览器在 Linux 和 Mac OS X 上运行时有效。在 Windows 上,Chrome 仍显示ERR_CONNECTION_RESET 网络错误,而不是您发送的 HTML。 (IE 11 和 Firefox 37 在所有平台上继续只显示网络错误。)

    因此,您对传统纯 HTTP 上传的选择是:

    • 显示友好的错误消息,但仅在上传运行完成之后。这会浪费时间和带宽。

    • 快速失败,但让用户对神秘的浏览器错误屏幕感到困惑。

    您最好的选择可能是使用 AJAX 上传器,您可以更好地控制用户体验。您仍然应该提供一个传统的上传表单作为后备,我会使用“快速失败”选项(关闭套接字)来防止浪费时间和带宽。

    下面是一些示例代码,如果请求接收到超过 1 kB,则它会终止该请求。我正在使用 Express,但同样适用于 node 的 vanilla HTTP 库。

    注意:实际上,您应该使用 formidable multiparty 来处理您的上传(这是 Connect/Express 使用的),它有其 @987654327 @监控上传数据。

    var express = require("express")
        , app = express();
    
    app.get('/', function(req, res) {
        res.send('Uploads &gt; 1 kB rejected<form action="/upload" method="post" enctype="multipart/form-data"><input type="file" name="file"><input type="submit"></form>');
    });
    
    app.post('/upload', function(req, res) {
        var size = 0;
    
        var gotData = function(d) {
            size += d.length; // add this chunk's size to the total number of bytes received thus far
            console.log('upload chunk', size);
            if (size > 1024) {
                console.log('aborting request');
                req.removeListener('data', gotData); // we need to remove the event listeners so that we don't end up here more than once
                req.removeListener('end', reqEnd);
                res.header('Connection', 'close'); // with the Connection: close header set, node will automatically close the socket...
                res.send(413, 'Upload too large'); // ... after sending a response
            }
        };
    
        var reqEnd = function() {
           res.send('ok, got ' + size + ' bytes');
        }
    
        req.on('data', gotData);
    
        req.on('end', reqEnd);
    });
    
    app.listen(3003);
    

    【讨论】:

    • 非常感谢您,我的主人。无法期待更好的回应。我遇到了您提到的大多数问题,例如连接重置神秘错误消息;只是认为可能有一个“正确”的解决方案(并且您确认这是浏览器问题,而不是 Node 的问题)。似乎最好的解决方法是使用 AJAX 上传。仅作记录,我正在使用 vanilla NodeJS,我希望在使用框架之前学习该语言的基础知识。
    • 为了未来围观者的利益,express/connect 现在似乎正在使用多方。此外,有必要添加一个标头以便提前断开连接,因为来自浏览器的连接通常使用保持活动连接。 res.header('连接', '关闭'); res.send(413, '上传太多!');
    • 非常感谢。上周我遇到了类似的问题。我正在实现一个允许管理员升级服务器的 HTTP API。该请求包含升级包和管理员密码。服务器将确保包具有正确的格式。我决定让服务器在发现格式不正确时停止接收数据并发送 400 BAD REQUEST。该 API 在 Postman 中完美运行,但是当我在 Chrome 和 FF 中调用它时,我不断收到 CONNECTION_ABORT/CONNECTION_RESET 错误。我被困了将近一个星期,这让我发疯了。
    【解决方案2】:

    请求参数是一个http.IncomingMessage 类,不允许停止流。

    但是你可以访问底层的socket,你可以中止它:

    request.socket.end('too big !');
    

    但我不确定浏览器是否会喜欢它...他可能会抱怨并指出连接被不正确地关闭。

    【讨论】:

    • 是的,它结束了在浏览器中显示“连接已重置”的连接。我以前见过这样的解决方案。我需要一个更漂亮的解决方案。还是谢谢!
    【解决方案3】:

    这是我的解决方案:

    var maxSize = 30 * 1024 * 1024;    //30MB
    app.post('/upload', function(req, res) {
    
        var size = req.headers['content-length'];
        if (size <= maxSize) {
            form.parse(req, function(err, fields, files) {
                console.log("File uploading");
                if (files && files.upload) {
                    res.status(200).json({fields: fields, files: files});
                    fs.renameSync(files.upload[0].path, uploadDir + files.upload[0].originalFilename);
                }
                else {
                  res.send("Not uploading");
                }
            });
        }
        else {
            res.send(413, "File to large");
        }
    

    如果在得到响应之前浪费了客户端的上传时间,请在客户端javascript中进行控制。

    if (fileElement.files[0].size > maxSize) {
        ....
    }
    

    【讨论】:

      【解决方案4】:

      我是这样使用 Formidable 的:

      var formidable = require('formidable'),
          http = require('http'),
          util = require('util');
      
      var MaxFieldSize = 1000 * 1000,
          MaxFields = 100,
          MaxUploadSize = 8 * 1000 * 1000;
      
      
      http.createServer (function(req, res) {
      
          console.log (req.url);
          console.log (req.headers ["content-type"]);
      
          if (req.url == '/upload')
          {
              var form = new formidable.IncomingForm();
      
              form.maxFieldsSize = MaxFieldSize;
              form.maxFields = MaxFields;
      
              form.on ('progress', function (bytesReceived, bytesExpected) {
                  //console.log (bytesReceived, bytesExpected);
                  if (bytesReceived > MaxUploadSize)
                  {
                      console.log ('*** TOO BIG');
      
                      // ***HACK*** see Formidable lib/incoming_form.js
                      // forces close files then triggers error in form.parse below
                      // bonus: removes temporary files
                      // --> use throttling in Chrome while opening /tmp in nautilus
                      //     and watch the files disappear
                      form.__2big__ = true;
                      form._error (new Error ('too big'));
      
                      //req.connection.destroy (); --- moved to form.parse
                  }
              });
      
              form.parse (req, function (err, fields, files) {
                  if (err)
                  {
                      console.log ('*** A', err);
                      try // just in case something is wrong with the connection, e.g. closed
                      {
                          // might not get through?
                          if (form.__2big__)
                          {
                              res.writeHead (413, {"connection": 'close', "content-type": 'text/plain'});
                              res.end ('upload to big');
                          }
                          else
                          {
                              res.writeHead (500, {"connection": 'close', "content-type": 'text/plain'});
                              res.end ('something wrong');
                          }
                          req.connection.destroy ();
                      }
                      catch (err)
                      {
                          console.log ('*** B', err);
                      }
                  }
                  else
                  {
                      res.writeHead (200, {"content-type": 'text/plain'});
                      res.write ('received upload:\n\n');
                      //for (let f in files)
                      //    console.log (f, files [f]);
                      res.end (util.inspect ({fields: fields, files: files}));
                  }
              });
          }
          else
          {
              res.writeHead (200, {"content-type": 'text/html'});
              res.end (
                  '<html>\
                      <head>\
                          <meta charset="UTF-8">\
                          <title>Test Formidable</title>\
                      </head>\
                      <body>\
                          <form action="/upload" method="POST" enctype="multipart/form-data">\
                              <input type="hidden" name="foo" value="1">\
                              <input type="text" name="fooh" value="2"><br>\
                              <input type="file" name="bar"><br>\
                              <input type="file" name="baz"><br>\
                              <input type="file" name="boo"><br>\
                              <button type="submit">Submit</submit>\
                          </form>\
                      </body>\
                  </html>'
              );
          }
      }).listen(8080);
      
      
      /* terminal:
          #> node upload
          /upload
          multipart/form-data; boundary=----WebKitFormBoundaryvqt1lXRmxeHLZtYi
          *** TOO BIG
          *** A Error: too big
              at IncomingForm.<anonymous> (/home/marc/Project/node/upload.js:33:18)
              at emitTwo (events.js:106:13)
              at IncomingForm.emit (events.js:191:7)
              at IncomingForm.write (/home/marc/Project/node/node_modules/formidable/lib/incoming_form.js:155:8)
              at IncomingMessage.<anonymous> (/home/marc/Project/node/node_modules/formidable/lib/incoming_form.js:123:12)
              at emitOne (events.js:96:13)
              at IncomingMessage.emit (events.js:188:7)
              at IncomingMessage.Readable.read (_stream_readable.js:387:10)
              at flow (_stream_readable.js:764:26)
              at resume_ (_stream_readable.js:744:3)
      */

      【讨论】:

        猜你喜欢
        • 2021-06-09
        • 2021-03-20
        • 2014-01-21
        • 1970-01-01
        • 1970-01-01
        • 2021-07-13
        • 2011-12-10
        • 2013-09-27
        • 2018-09-26
        相关资源
        最近更新 更多