source: node_modules/swagger-client/es/http/index.js@ d24f17c

main
Last change on this file since d24f17c was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 14.2 KB
Line 
1import qs from 'qs';
2import jsYaml from 'js-yaml';
3import '../helpers/fetch-polyfill.node.js';
4import { encodeDisallowedCharacters } from '../execute/oas3/style-serializer.js';
5
6// For testing
7export const self = {
8 serializeRes,
9 mergeInQueryOrForm
10};
11
12// Handles fetch-like syntax and the case where there is only one object passed-in
13// (which will have the URL as a property). Also serializes the response.
14export default async function http(url, request = {}) {
15 if (typeof url === 'object') {
16 request = url;
17 url = request.url;
18 }
19 request.headers = request.headers || {};
20
21 // Serializes query, for convenience
22 // Should be the last thing we do, as its hard to mutate the URL with
23 // the search string, but much easier to manipulate the req.query object
24 self.mergeInQueryOrForm(request);
25
26 // Newlines in header values cause weird error messages from `window.fetch`,
27 // so let's message them out.
28 // Context: https://stackoverflow.com/a/50709178
29 if (request.headers) {
30 Object.keys(request.headers).forEach(headerName => {
31 const value = request.headers[headerName];
32 if (typeof value === 'string') {
33 request.headers[headerName] = value.replace(/\n+/g, ' ');
34 }
35 });
36 }
37
38 // Wait for the request interceptor, if it was provided
39 // WARNING: don't put anything between this and the request firing unless
40 // you have a good reason!
41 if (request.requestInterceptor) {
42 request = (await request.requestInterceptor(request)) || request;
43 }
44
45 /**
46 * For content-type=multipart/form-data remove content-type from request before fetch,
47 * so that correct one with `boundary` is set when request body is different from boundary encoded string.
48 */
49 const contentType = request.headers['content-type'] || request.headers['Content-Type'];
50 if (/multipart\/form-data/i.test(contentType)) {
51 delete request.headers['content-type'];
52 delete request.headers['Content-Type'];
53 }
54
55 // eslint-disable-next-line no-undef
56 let res;
57 try {
58 res = await (request.userFetch || fetch)(request.url, request);
59 res = await self.serializeRes(res, url, request);
60 if (request.responseInterceptor) {
61 res = (await request.responseInterceptor(res)) || res;
62 }
63 } catch (resError) {
64 if (!res) {
65 // res is completely absent, so we can't construct our own error
66 // so we'll just throw the error we got
67 throw resError;
68 }
69 const error = new Error(res.statusText || `response status is ${res.status}`);
70 error.status = res.status;
71 error.statusCode = res.status;
72 error.responseError = resError;
73 throw error;
74 }
75 if (!res.ok) {
76 const error = new Error(res.statusText || `response status is ${res.status}`);
77 error.status = res.status;
78 error.statusCode = res.status;
79 error.response = res;
80 throw error;
81 }
82 return res;
83}
84
85// exported for testing
86export const shouldDownloadAsText = (contentType = '') => /(json|xml|yaml|text)\b/.test(contentType);
87function parseBody(body, contentType) {
88 if (contentType && (contentType.indexOf('application/json') === 0 || contentType.indexOf('+json') > 0)) {
89 return JSON.parse(body);
90 }
91 return jsYaml.load(body);
92}
93
94// Serialize the response, returns a promise with headers and the body part of the hash
95export function serializeRes(oriRes, url, {
96 loadSpec = false
97} = {}) {
98 const res = {
99 ok: oriRes.ok,
100 url: oriRes.url || url,
101 status: oriRes.status,
102 statusText: oriRes.statusText,
103 headers: serializeHeaders(oriRes.headers)
104 };
105 const contentType = res.headers['content-type'];
106 const useText = loadSpec || shouldDownloadAsText(contentType);
107 const getBody = useText ? oriRes.text : oriRes.blob || oriRes.buffer;
108 return getBody.call(oriRes).then(body => {
109 res.text = body;
110 res.data = body;
111 if (useText) {
112 try {
113 const obj = parseBody(body, contentType);
114 res.body = obj;
115 res.obj = obj;
116 } catch (e) {
117 res.parseError = e;
118 }
119 }
120 return res;
121 });
122}
123function serializeHeaderValue(value) {
124 const isMulti = value.includes(', ');
125 return isMulti ? value.split(', ') : value;
126}
127
128// Serialize headers into a hash, where mutliple-headers result in an array.
129//
130// eg: Cookie: one
131// Cookie: two
132// = { Cookie: [ "one", "two" ]
133export function serializeHeaders(headers = {}) {
134 if (typeof headers.entries !== 'function') return {};
135 return Array.from(headers.entries()).reduce((acc, [header, value]) => {
136 acc[header] = serializeHeaderValue(value);
137 return acc;
138 }, {});
139}
140export function isFile(obj, navigatorObj) {
141 if (!navigatorObj && typeof navigator !== 'undefined') {
142 // eslint-disable-next-line no-undef
143 navigatorObj = navigator;
144 }
145 if (navigatorObj && navigatorObj.product === 'ReactNative') {
146 if (obj && typeof obj === 'object' && typeof obj.uri === 'string') {
147 return true;
148 }
149 return false;
150 }
151 if (typeof File !== 'undefined' && obj instanceof File) {
152 return true;
153 }
154 if (typeof Blob !== 'undefined' && obj instanceof Blob) {
155 return true;
156 }
157 if (ArrayBuffer.isView(obj)) {
158 return true;
159 }
160 return obj !== null && typeof obj === 'object' && typeof obj.pipe === 'function';
161}
162function isArrayOfFile(obj, navigatorObj) {
163 return Array.isArray(obj) && obj.some(v => isFile(v, navigatorObj));
164}
165const STYLE_SEPARATORS = {
166 form: ',',
167 spaceDelimited: '%20',
168 pipeDelimited: '|'
169};
170const SEPARATORS = {
171 csv: ',',
172 ssv: '%20',
173 tsv: '%09',
174 pipes: '|'
175};
176
177/**
178 * Specialized sub-class of File class, that only
179 * accepts string data and retain this data in `data`
180 * public property throughout the lifecycle of its instances.
181 *
182 * This sub-class is exclusively used only when Encoding Object
183 * is defined within the Media Type Object (OpenAPI 3.x.y).
184 */
185class FileWithData extends File {
186 constructor(data, name = '', options = {}) {
187 super([data], name, options);
188 this.data = data;
189 }
190 valueOf() {
191 return this.data;
192 }
193 toString() {
194 return this.valueOf();
195 }
196}
197
198// Formats a key-value and returns an array of key-value pairs.
199//
200// Return value example 1: [['color', 'blue']]
201// Return value example 2: [['color', 'blue,black,brown']]
202// Return value example 3: [['color', ['blue', 'black', 'brown']]]
203// Return value example 4: [['color', 'R,100,G,200,B,150']]
204// Return value example 5: [['R', '100'], ['G', '200'], ['B', '150']]
205// Return value example 6: [['color[R]', '100'], ['color[G]', '200'], ['color[B]', '150']]
206function formatKeyValue(key, input, skipEncoding = false) {
207 const {
208 collectionFormat,
209 allowEmptyValue,
210 serializationOption,
211 encoding
212 } = input;
213 // `input` can be string
214 const value = typeof input === 'object' && !Array.isArray(input) ? input.value : input;
215 const encodeFn = skipEncoding ? k => k.toString() : k => encodeURIComponent(k);
216 const encodedKey = encodeFn(key);
217 if (typeof value === 'undefined' && allowEmptyValue) {
218 return [[encodedKey, '']];
219 }
220
221 // file
222 if (isFile(value) || isArrayOfFile(value)) {
223 return [[encodedKey, value]];
224 }
225
226 // for OAS 3 Parameter Object for serialization
227 if (serializationOption) {
228 return formatKeyValueBySerializationOption(key, value, skipEncoding, serializationOption);
229 }
230
231 // for OAS 3 Encoding Object
232 if (encoding) {
233 if ([typeof encoding.style, typeof encoding.explode, typeof encoding.allowReserved].some(type => type !== 'undefined')) {
234 const {
235 style,
236 explode,
237 allowReserved
238 } = encoding;
239 return formatKeyValueBySerializationOption(key, value, skipEncoding, {
240 style,
241 explode,
242 allowReserved
243 });
244 }
245 if (typeof encoding.contentType === 'string') {
246 if (encoding.contentType.startsWith('application/json')) {
247 // if value is a string, assume value is already a JSON string
248 const json = typeof value === 'string' ? value : JSON.stringify(value);
249 const encodedJson = encodeFn(json);
250 const file = new FileWithData(encodedJson, 'blob', {
251 type: encoding.contentType
252 });
253 return [[encodedKey, file]];
254 }
255 const encodedData = encodeFn(String(value));
256 const blob = new FileWithData(encodedData, 'blob', {
257 type: encoding.contentType
258 });
259 return [[encodedKey, blob]];
260 }
261
262 // Primitive
263 if (typeof value !== 'object') {
264 return [[encodedKey, encodeFn(value)]];
265 }
266
267 // Array of primitives
268 if (Array.isArray(value) && value.every(v => typeof v !== 'object')) {
269 return [[encodedKey, value.map(encodeFn).join(',')]];
270 }
271
272 // Array or object
273 return [[encodedKey, encodeFn(JSON.stringify(value))]];
274 }
275
276 // for OAS 2 Parameter Object
277 // Primitive
278 if (typeof value !== 'object') {
279 return [[encodedKey, encodeFn(value)]];
280 }
281
282 // Array
283 if (Array.isArray(value)) {
284 if (collectionFormat === 'multi') {
285 // In case of multipart/formdata, it is used as array.
286 // Otherwise, the caller will convert it to a query by qs.stringify.
287 return [[encodedKey, value.map(encodeFn)]];
288 }
289 return [[encodedKey, value.map(encodeFn).join(SEPARATORS[collectionFormat || 'csv'])]];
290 }
291
292 // Object
293 return [[encodedKey, '']];
294}
295function formatKeyValueBySerializationOption(key, value, skipEncoding, serializationOption) {
296 const style = serializationOption.style || 'form';
297 const explode = typeof serializationOption.explode === 'undefined' ? style === 'form' : serializationOption.explode;
298 // eslint-disable-next-line no-nested-ternary
299 const escape = skipEncoding ? false : serializationOption && serializationOption.allowReserved ? 'unsafe' : 'reserved';
300 const encodeFn = v => encodeDisallowedCharacters(v, {
301 escape
302 });
303 const encodeKeyFn = skipEncoding ? k => k : k => encodeDisallowedCharacters(k, {
304 escape
305 });
306
307 // Primitive
308 if (typeof value !== 'object') {
309 return [[encodeKeyFn(key), encodeFn(value)]];
310 }
311
312 // Array
313 if (Array.isArray(value)) {
314 if (explode) {
315 // In case of multipart/formdata, it is used as array.
316 // Otherwise, the caller will convert it to a query by qs.stringify.
317 return [[encodeKeyFn(key), value.map(encodeFn)]];
318 }
319 return [[encodeKeyFn(key), value.map(encodeFn).join(STYLE_SEPARATORS[style])]];
320 }
321
322 // Object
323 if (style === 'deepObject') {
324 return Object.keys(value).map(valueKey => [encodeKeyFn(`${key}[${valueKey}]`), encodeFn(value[valueKey])]);
325 }
326 if (explode) {
327 return Object.keys(value).map(valueKey => [encodeKeyFn(valueKey), encodeFn(value[valueKey])]);
328 }
329 return [[encodeKeyFn(key), Object.keys(value).map(valueKey => [`${encodeKeyFn(valueKey)},${encodeFn(value[valueKey])}`]).join(',')]];
330}
331function buildFormData(reqForm) {
332 /**
333 * Build a new FormData instance, support array as field value
334 * OAS2.0 - when collectionFormat is multi
335 * OAS3.0 - when explode of Encoding Object is true
336 *
337 * This function explicitly handles Buffers (for backward compatibility)
338 * if provided as a values to FormData. FormData can only handle USVString
339 * or Blob.
340 *
341 * @param {Object} reqForm - ori req.form
342 * @return {FormData} - new FormData instance
343 */
344 return Object.entries(reqForm).reduce((formData, [name, input]) => {
345 // eslint-disable-next-line no-restricted-syntax
346 for (const [key, value] of formatKeyValue(name, input, true)) {
347 if (Array.isArray(value)) {
348 // eslint-disable-next-line no-restricted-syntax
349 for (const v of value) {
350 if (ArrayBuffer.isView(v)) {
351 const blob = new Blob([v]);
352 formData.append(key, blob);
353 } else {
354 formData.append(key, v);
355 }
356 }
357 } else if (ArrayBuffer.isView(value)) {
358 const blob = new Blob([value]);
359 formData.append(key, blob);
360 } else {
361 formData.append(key, value);
362 }
363 }
364 return formData;
365 }, new FormData());
366}
367
368// Encodes an object using appropriate serializer.
369export function encodeFormOrQuery(data) {
370 /**
371 * Encode parameter names and values
372 * @param {Object} result - parameter names and values
373 * @param {string} parameterName - Parameter name
374 * @return {object} encoded parameter names and values
375 */
376 const encodedQuery = Object.keys(data).reduce((result, parameterName) => {
377 // eslint-disable-next-line no-restricted-syntax
378 for (const [key, value] of formatKeyValue(parameterName, data[parameterName])) {
379 if (value instanceof FileWithData) {
380 result[key] = value.valueOf();
381 } else {
382 result[key] = value;
383 }
384 }
385 return result;
386 }, {});
387 return qs.stringify(encodedQuery, {
388 encode: false,
389 indices: false
390 }) || '';
391}
392
393// If the request has a `query` object, merge it into the request.url, and delete the object
394// If file and/or multipart, also create FormData instance
395export function mergeInQueryOrForm(req = {}) {
396 const {
397 url = '',
398 query,
399 form
400 } = req;
401 const joinSearch = (...strs) => {
402 const search = strs.filter(a => a).join('&'); // Only truthy value
403 return search ? `?${search}` : ''; // Only add '?' if there is a str
404 };
405 if (form) {
406 const hasFile = Object.keys(form).some(key => {
407 const {
408 value
409 } = form[key];
410 return isFile(value) || isArrayOfFile(value);
411 });
412 const contentType = req.headers['content-type'] || req.headers['Content-Type'];
413 if (hasFile || /multipart\/form-data/i.test(contentType)) {
414 const formdata = buildFormData(req.form);
415 req.formdata = formdata;
416 req.body = formdata;
417 } else {
418 req.body = encodeFormOrQuery(form);
419 }
420 delete req.form;
421 }
422 if (query) {
423 const [baseUrl, oriSearch] = url.split('?');
424 let newStr = '';
425 if (oriSearch) {
426 const oriQuery = qs.parse(oriSearch);
427 const keysToRemove = Object.keys(query);
428 keysToRemove.forEach(key => delete oriQuery[key]);
429 newStr = qs.stringify(oriQuery, {
430 encode: true
431 });
432 }
433 const finalStr = joinSearch(newStr, encodeFormOrQuery(query));
434 req.url = baseUrl + finalStr;
435 delete req.query;
436 }
437 return req;
438}
439
440// Wrap a http function ( there are otherways to do this, consider this deprecated )
441export function makeHttp(httpFn, preFetch, postFetch) {
442 postFetch = postFetch || (a => a);
443 preFetch = preFetch || (a => a);
444 return req => {
445 if (typeof req === 'string') {
446 req = {
447 url: req
448 };
449 }
450 self.mergeInQueryOrForm(req);
451 req = preFetch(req);
452 return postFetch(httpFn(req));
453 };
454}
Note: See TracBrowser for help on using the repository browser.