Skip to content

Commit bad8960

Browse files
author
Mike Kistler
authored
fix: Fix handling of array form parameters. (#43)
1 parent ae3cc69 commit bad8960

File tree

4 files changed

+59
-47
lines changed

4 files changed

+59
-47
lines changed

lib/base_service.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,20 @@ export class BaseService {
338338
* are being used to authenticate the request. If so, the token is
339339
* retrieved by the token manager.
340340
*
341-
* @param {Object} parameters - service request options passed in by user
341+
* @param {Object} parameters - service request options passed in by user.
342+
* @param {string} parameters.options.method - the http method.
343+
* @param {string} parameters.options.url - the URL of the service.
344+
* @param {string} parameters.options.path - the path to be appended to the service URL.
345+
* @param {string} parameters.options.qs - the querystring to be included in the URL.
346+
* @param {string} parameters.options.body - the data to be sent as the request body.
347+
* @param {Object} parameters.options.form - an object containing the key/value pairs for a www-form-urlencoded request.
348+
* @param {Object} parameters.options.formData - an object containing the contents for a multipart/form-data request.
349+
* The following processing is performed on formData values:
350+
* - string: no special processing -- the value is sent as is
351+
* - object: the value is converted to a JSON string before insertion into the form body
352+
* - NodeJS.ReadableStream|FileObject|Buffer|FileParamAttributes: sent as a file, with any associated metadata
353+
* - array: each element of the array is sent as a separate form part using any special processing as described above
354+
* @param {HeaderOptions} parameters.options.headers - additional headers to be passed on the request.
342355
* @param {Function} callback - callback function to pass the response back to
343356
* @returns {ReadableStream|undefined}
344357
*/

lib/helper.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,31 @@ export interface FileStream extends NodeJS.ReadableStream {
4343
}
4444

4545
// custom type guards
46-
function isFileObject(obj: any): obj is FileObject {
46+
export function isFileObject(obj: any): obj is FileObject {
4747
return Boolean(obj && obj.value);
4848
}
4949

5050
function isFileStream(obj: any): obj is FileStream {
5151
return obj && isReadable(obj) && obj.path;
5252
}
5353

54+
export function isFileParamAttributes(obj: any): obj is FileParamAttributes {
55+
return obj && obj.data &&
56+
(
57+
isReadable(obj.data) ||
58+
Buffer.isBuffer(obj.data) ||
59+
isFileObject(obj.data)
60+
);
61+
}
62+
5463
export function isFileParam(obj: any): boolean {
5564
return Boolean(
5665
obj &&
5766
(
5867
isReadable(obj) ||
5968
Buffer.isBuffer(obj) ||
6069
isFileObject(obj) ||
61-
(obj.data && isFileParam(obj.data))
70+
isFileParamAttributes(obj)
6271
)
6372
);
6473
}
@@ -164,6 +173,7 @@ export function getFormat(
164173
* this function builds a `form-data` object for each file parameter
165174
* @param {FileParamAttributes} fileParams - the file parameter attributes
166175
* @param {NodeJS.ReadableStream|Buffer|FileObject} fileParams.data - the data content of the file
176+
* @param (string) fileParams.filename - the filename of the file
167177
* @param {string} fileParams.contentType - the content type of the file
168178
* @returns {FileObject}
169179
*/

lib/requestwrapper.ts

+24-42
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import FormData = require('form-data');
2020
import https = require('https');
2121
import querystring = require('querystring');
2222
import { PassThrough as readableStream } from 'stream';
23-
import { buildRequestFileObject, getMissingParams, isEmptyObject, isFileParam } from './helper';
23+
import { buildRequestFileObject, getMissingParams, isEmptyObject, isFileObject, isFileParam, isFileParamAttributes } from './helper';
2424

2525
const isBrowser = typeof window === 'object';
2626
const globalTransactionId = 'x-global-transaction-id';
@@ -127,48 +127,30 @@ export class RequestWrapper {
127127

128128
// Form params
129129
if (formData) {
130-
// Remove keys with undefined/null values
131-
// Remove empty objects
132-
// Remove non-valid inputs for buildRequestFileObject,
133-
// i.e things like {contentType: <contentType>}
134130
Object.keys(formData).forEach(key => {
135-
if (formData[key] == null ||
136-
isEmptyObject(formData[key]) ||
137-
(formData[key].hasOwnProperty('contentType') && !formData[key].hasOwnProperty('data'))) {
138-
delete formData[key];
139-
}
140-
});
141-
// Convert file form parameters to request-style objects
142-
Object.keys(formData).forEach(key => {
143-
if (formData[key].data != null) {
144-
formData[key] = buildRequestFileObject(formData[key]);
145-
}
146-
});
147-
148-
// Stringify arrays
149-
Object.keys(formData).forEach(key => {
150-
if (Array.isArray(formData[key])) {
151-
formData[key] = formData[key].join(',');
152-
}
153-
});
154-
155-
// Convert non-file form parameters to strings
156-
Object.keys(formData).forEach(key => {
157-
if (!isFileParam(formData[key]) &&
158-
!Array.isArray(formData[key]) &&
159-
typeof formData[key] === 'object') {
160-
(formData[key] = JSON.stringify(formData[key]));
161-
}
162-
});
163-
164-
// build multipart form data
165-
Object.keys(formData).forEach(key => {
166-
// handle files differently to maintain options
167-
if (formData[key].value) {
168-
multipartForm.append(key, formData[key].value, formData[key].options);
169-
} else {
170-
multipartForm.append(key, formData[key]);
171-
}
131+
const values = Array.isArray(formData[key]) ? formData[key] : [formData[key]];
132+
// Skip keys with undefined/null values or empty object value
133+
values.filter(v => v != null && !isEmptyObject(v)).forEach(value => {
134+
135+
// Special case of empty file object
136+
if (value.hasOwnProperty('contentType') && !value.hasOwnProperty('data')) {
137+
return;
138+
}
139+
140+
// Convert file form parameters to request-style objects
141+
if (isFileParamAttributes(value)) {
142+
value = buildRequestFileObject(value);
143+
}
144+
145+
if (isFileObject(value)) {
146+
multipartForm.append(key, value.value, value.options);
147+
} else {
148+
if (typeof value === 'object' && !isFileParam(value)) {
149+
value = JSON.stringify(value);
150+
}
151+
multipartForm.append(key, value);
152+
}
153+
});
172154
});
173155
}
174156

test/unit/requestWrapper.test.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,11 @@ describe('sendRequest', () => {
200200
'add-header': 'add-header-value',
201201
},
202202
formData: {
203-
file: fs.createReadStream('../blank.wav'),
203+
file: fs.createReadStream(__dirname + '/../resources/blank.wav'),
204204
null_item: null,
205205
custom_file: {
206206
filename: 'custom.wav',
207-
data: fs.createReadStream('../blank.wav'),
207+
data: fs.createReadStream(__dirname + '/../resources/blank.wav'),
208208
},
209209
array_item: ['a', 'b'],
210210
object_item: { a: 'a', b: 'b' },
@@ -242,6 +242,13 @@ describe('sendRequest', () => {
242242
expect(JSON.stringify(mockAxiosInstance.mock.calls[0][0])).toMatch(
243243
'Content-Disposition: form-data; name=\\"array_item\\"'
244244
);
245+
// There should be two "array_item" parts
246+
expect(
247+
(
248+
JSON.stringify(mockAxiosInstance.mock.calls[0][0].data).match(/name=\\"array_item\\"/g) ||
249+
[]
250+
).length
251+
).toEqual(2);
245252
expect(JSON.stringify(mockAxiosInstance.mock.calls[0][0])).toMatch(
246253
'Content-Disposition: form-data; name=\\"custom_file\\"'
247254
);

0 commit comments

Comments
 (0)