【问题标题】:org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request...Stream ended unexpectedlyorg.springframework.web.multipart.MultipartException:无法解析多部分 servlet 请求...流意外结束
【发布时间】:2016-08-26 20:06:47
【问题描述】:

情况:

从 Node.js(通过节点核心 HTTPS 模块)向 spring-boot Java API 提交多部分表单请求。 API 需要两个表单数据元素:

“路线”
“文件”

完全错误: Exception processed - Main Exception: org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadException: Stream ended unexpectedly

请求标头:

{"Accept":"*/*",
  "cache-control":"no-cache",
  "Content-Type":"multipart/form-data; boundary=2baac014-7974-49dd-ae87-7ce56c36c9e7",
  "Content-Length":7621}

FORM-DATA BEING WRITTEN(全部以二进制形式写入):

Content-Type: multipart/form-data; boundary=2baac014-7974-49dd-ae87-7ce56c36c9e7

--2baac014-7974-49dd-ae87-7ce56c36c9e7

Content-Disposition:form-data; name="route"

...our route object

--2baac014-7974-49dd-ae87-7ce56c36c9e7
Content-Disposition:form-data; name="files"; filename="somefile.xlsx"
Content-Type:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

...excel file contents

--2baac014-7974-49dd-ae87-7ce56c36c9e7--

节点代码:

let mdtHttpMultipart = (options, data = reqParam('data'), cb) => {
  const boundaryUuid = getUuid()
    , baseHeaders = {
        'Accept': '*/*',
        'cache-control': 'no-cache'
      }
    , composedHeaders = Object.assign({}, baseHeaders, options.headers)
    ;

  options.path = checkPath(options.path);

  let composedOptions = Object.assign({}, {
    'host': getEdiHost(),
    'path': buildPathFromObject(options.path, options.urlParams),
    'method': options.method || 'GET',
    'headers': composedHeaders,
    'rejectUnauthorized': false
  });


  composedOptions.headers['Content-Type'] = `multipart/form-data; boundary=${boundaryUuid}`;

  let multipartChunks = [];
  let dumbTotal = 0;

  let writePart = (_, encType = 'binary', skip = false) => {
    if (!_) { return; }

    let buf = Buffer.from(_, encType);
    if (!skip) {dumbTotal += Buffer.byteLength(buf, encType);}
    multipartChunks.push(buf);
  };

  writePart(`Content-Type: multipart/form-data; boundary=${boundaryUuid}\r\n\r\n`, 'binary', true)
  writePart(`--${boundaryUuid}\r\n`)
  writePart(`Content-Disposition:form-data; name="route"\r\n`)
  writePart(JSON.stringify(data[0]) + '\r\n')
  writePart(`--${boundaryUuid}\r\n`)
  writePart(`Content-Disposition:form-data; name="files"; filename="${data[1].name}"\r\n`)
  writePart(`Content-Type:${data[1].contentType}\r\n`)
  writePart(data[1].contents + '\r\n')
  writePart(`\r\n--${boundaryUuid}--\r\n`);

  let multipartBuffer = Buffer.concat(multipartChunks);

  composedOptions.headers['Content-Length'] = dumbTotal;
  let request = https.request(composedOptions);

  // on nextTick write multipart to request
  process.nextTick(() => {
    request.write(multipartBuffer, 'binary');
    request.end();
  });

  // handle response
  request.on('response', (httpRequestResponse) => {
    let chunks = []
      , errObject = handleHttpStatusCodes(httpRequestResponse);
    ;

    if (errObject !== null) {
      return cb(errObject, null);
    }

    httpRequestResponse.on('data', (chunk) => { chunks.push(chunk); });
    httpRequestResponse.on('end', () => {
      let responseString = Buffer.concat(chunks).toString()
        ;

      return cb(null, JSON.parse(responseString));
    });

  });

  request.on('error', (err) => cb(err));
};

根据规范,我们看不出有任何理由抛出 500。在这里对格式进行了大量的修改,但我们还没有正确地实现结果。

旁注:它适用于我们使用 POSTMAN,但无法使用我们自己的应用程序服务器(我们实际构建 excel 文件的地方)。

任何帮助都将不胜感激,即使只是尝试的想法。

【问题讨论】:

  • 可能不是问题,但是当您第一次调用 writePart() 时,您添加的是在指定 composedOptions.headers['Content-Type'] = ... 时已经为您添加的 Content-Type 标头。您也不需要在最后的 --${boundaryUuid}-- 周围换行。
  • 继续进行这些更改,结果仍与预期相同。
  • 您确定binary 编码?这是latin1 的别名。看看base64 对所有内容进行编码是否有什么不同?
  • 所以需要二进制的原因是由于 excel 文件的内容。我们实际上是从一个单独的微服务接收该文件,该微服务将其构建在内存中并将其发布到一个单独的 API,在那里它以二进制形式传输它。使用该文件(作为二进制文件)执行单个多部分表单请求对我们有用,但添加额外的 JSON 表单数据对象(在本例中为“路由”)会使同一服务器返回 500 个。

标签: node.js spring http multipartform-data


【解决方案1】:

试试这个:

let mdtHttpMultipart = (options, data = reqParam('data'), cb) => {
  const boundaryUuid = getUuid()
    , baseHeaders = {
        'Accept': '*/*',
        'cache-control': 'no-cache'
      }
    , composedHeaders = Object.assign({}, baseHeaders, options.headers)
    ;

  let file = data[1]
  let xlsx = file.contents

  options.path = checkPath(options.path);

  let composedOptions = Object.assign({}, {
    'host': getEdiHost(),
    'path': buildPathFromObject(options.path, options.urlParams),
    'method': options.method || 'GET',
    'headers': composedHeaders,
    'rejectUnauthorized': false
  });

  let header = Buffer.from(`--${boundaryUuid}
    Content-Disposition: form-data; name="route"

    ${JSON.stringify(data[0])})
    --${boundaryUuid}
    Content-Disposition: form-data; name="files"; filename="${file.name}"
    Content-Type: ${file.contentType}

  `.replace(/\r?\n */gm, '\r\n'))
  let footer = Buffer.from(`\r\n--${boundaryUuid}--`)
  let length = header.length + xlsx.length + footer.length
  let body = Buffer.concat([header, xlsx, footer], length)

  composedOptions.headers['Content-Length'] = length;
  composedOptions.headers['Content-Type'] = `multipart/form-data; boundary=${boundaryUuid}`;

  let request = https.request(composedOptions);

  // handle response
  request.on('response', (httpRequestResponse) => {
    let chunks = []
      , errObject = handleHttpStatusCodes(httpRequestResponse);
    ;

    if (errObject !== null) {
      return cb(errObject, null);
    }

    httpRequestResponse.on('data', (chunk) => { chunks.push(chunk); });
    httpRequestResponse.on('end', () => {
      let responseString = Buffer.concat(chunks).toString()
        ;

      return cb(null, JSON.parse(responseString));
    });

  });

  request.on('error', (err) => cb(err));

  // write multipart to request
  request.end(body);
};

【讨论】:

    【解决方案2】:

    是不是你没有在任何地方给request.end()打电话?

    发送带有正文的请求的(非常通用的)形式是https.request(opts).end(body)

    此外,您可以在每次想要发送数据时调用 request.write(buf),而不是累积到一个巨大的缓冲区中(并且您不需要在 nextTick 上执行此操作)(编辑:正如 OP 在cmets,这将阻止设置Content-Length,所以可能保持原样)

    【讨论】:

    • 我在写完buf之后添加了request.end()。我们以前让代码一次编写一个部分,我们只是将其更改为在发送请求之前尝试计算内容长度,因为我们认为这可能是问题,但似乎不是。添加 request.end() 后仍然出现同样的错误。我将修改上面的代码示例。不过还是谢谢..
    猜你喜欢
    • 1970-01-01
    • 2011-03-01
    • 1970-01-01
    • 2020-05-25
    • 2014-10-10
    • 2022-07-12
    • 2016-08-16
    • 2019-06-10
    • 2019-04-04
    相关资源
    最近更新 更多