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.
writePart()you're adding the Content-Type header which is already added for you when you specifycomposedOptions.headers['Content-Type'] = .... You also do not need line breaks around the final--${boundaryUuid}--.binaryencoding? That's an alias forlatin1. See ifbase64encoding everything makes any difference?