Collin Estes Collin Estes - 3 months ago 58
Node.js Question

org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request...Stream ended unexpectedly

SITUATION:

Submitting a multipart form request from Node.js (via node core HTTPS module) to a spring-boot Java API. The API requires two form-data elements:

"route"

"files"

FULL ERROR:


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


REQUEST HEADERS:

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


FORM-DATA BEING WRITTEN (all written as binary):

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--


NODE CODE:

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));
};


We cannot see any reason for the 500 to be thrown based on the spec. Tons of tinkering around with the format here but we have yet to achieve the result correctly.

SIDE NOTE: It works for us using POSTMAN, just can't get it to work using our our own application server (where we actually build the excel file).

Any help would be appreciated even if just ideas to try.

Answer

Try this:

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);
};
Comments