Skip to content

Latest commit

 

History

History
706 lines (557 loc) · 20.4 KB

http-client.md

File metadata and controls

706 lines (557 loc) · 20.4 KB

HTTP Client

Our HTTP Client exposes a Fetch-like interface with a twist: making url part of the request object. It extends Fetch API to support request and response interceptors and performs response & header serialization.

Here is a simple example how to make POST HTTP request:

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/post',
  mode: 'cors',
  method: 'POST',
  body: { data: 3 },
  headers: {
    'Content-Type': 'application/json',
  },
};

SwaggerClient.http(request); // => Promise(Response)

Here is an example how to make POST HTTP request via Fetch compatible interface.

import SwaggerClient from 'swagger-client';

const url = 'https://httpbin.org/post';
const request = {
  url, // notice the url must always be part of Request object
  mode: 'cors',
  method: 'POST',
  body: { data: 3 },
  headers: {
    'Content-Type': 'application/json',
  },
};

SwaggerClient.http(url, request); // => Promise(Response)

Request

If you prefer using object literal to represent Fetch compatible Request here is how you can create a new request.

const request = {
  url: 'https://httpbin.org/post',
  mode: 'cors',
  method: 'POST',
  body: { data: 3 },
  headers: {
    'Content-Type': 'application/json',
  },
};

It is also possible to use instance of Fetch compatible Request class either the one that is native to browsers or provided by 3rd party libraries like cross-fetch.

const request = new Request('https://httpbin.org/post', {
  mode: 'cors',
  method: 'POST',
  body: { data: 3 },
  headers: {
    'Content-Type': 'application/json',
  },
});

You can find documentation of all allowed Request properties in official documentation of Fetch compatible Request.

Additional Request properties are described in the following table.

Type notations are formatted like so:

  • String="" means a String type with a default value of "".
  • String=["a"*, "b", "c", "d"] means a String type that can be a, b, c, or d, with the * indicating that a is the default value.
Property Description
query Object=null. When provided, HTTP Client serialize it and appends the queryString to Request.url property.
loadSpec Boolean=undefined. This property will be present and set to true when the Request was constructed internally by SwaggerClient to fetch the OAS definition defined by url or when resolving remote JSON References.
requestInterceptor Function=identity. Either synchronous or asynchronous function transformer that accepts Request and should return Request.
responseInterceptor Function=identity. Either synchronous or asynchronous function transformer that accepts Response and should return Response.
userFetch Function=cross-fetch. Custom asynchronous fetch function that accepts two arguments: the url and the Request object and must return a Response object.
Query support

HTTP Client support serialization of additional query parameters provided as a query property of a Request object. When provided, the query parameters are serialized and appended to Request.url property as query string.

Basic example:

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/?one=1&two=1',
  query: {
    two: {
      value: 2
    },
    three: {
      value: 3
    }
  },
  method: 'GET',
};

SwaggerClient.http(request);
// Requested URL: https://httpbin.org/?one=1&two=2&three=3

Advanced example:

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/',
  query: {
    anotherOne: ['one', 'two'], // no collection format
    evenMore: 'hi', // string, not an array
    bar: { // has a collectionFormat
      collectionFormat: 'ssv', // supported values: csv, ssv, pipes
      value: [1, 2, 3]
    }
  },
  method: 'GET',
};

SwaggerClient.http(request);
// Requested URL: https://httpbin.org/?anotherOne=one,two&evenMore=hi&bar=1%202%203
Loading specification

loadSpec property signals when the Request was constructed implicitly by SwaggerClient. This can happen in only two circumstances.

  1. When SwaggerClient fetches the OAS definition specified by url
  2. When SwaggerClient resolves the OAS definition by fetching remote JSON References

All other requests will not have this property present, or the property will be set to false.

This following code snippet:

import SwaggerClient from 'swagger-client';

const requestInterceptor = (request) => {
  console.log(request);

  return request;
};

SwaggerClient({ url: 'https://petstore.swagger.io/v2/swagger.json', requestInterceptor })
  .then(client => client.execute({
      operationId: "findPetsByStatus",
      parameters: { status: 3 },
      requestInterceptor,
  }));

Will result in two requests, with only one containing loadSpec set to true:

{
  url: 'https://petstore.swagger.io/v2/swagger.json',
  loadSpec: true,
  requestInterceptor: [Function: requestInterceptor],
  responseInterceptor: null,
  headers: { Accept: 'application/json, application/yaml' },
  credentials: 'same-origin'
}
{
  url: 'https://petstore.swagger.io/v2/pet/findByStatus?status=3',
  credentials: 'same-origin',
  headers: {},
  requestInterceptor: [Function: requestInterceptor],
  method: 'GET'
}
Request Interceptor

Request interceptor can be configured via requestInterceptor property in Request object. When set, it intercepts the Request object before the actual HTTP request is made and after the query serialization kicked in. This means that intercepted Request object will never contain query property. All other properties will be present though. Request interceptor can be defined as synchronous (transformer) or asynchronous function (allows other async operations inside).

Transformer usage:

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
  requestInterceptor: req => {
    req.url += '?param=value';
    return req;
  },
};

SwaggerClient.http(request); 
// Requested URL: https://httpbin.org/?param=value

Async operation usage:

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
  requestInterceptor: async req => {
    const { body: { idToken } } = await SwaggerClient.http({ url: 'https://identity.com/idtoken.json' });
    req.url += `?idToken=${idToken}`;
    return req;
  },
};

SwaggerClient.http(request); 
// Requested URL: https://httpbin.org/?idToken=2342398423948923

You're not limited to using one Request interceptor function. You can easily compose pipe of request interceptors.

import SwaggerClient from 'swagger-client';

const pipeP = (...fns) => args => fns.reduce((arg, fn) => arg.then(fn), Promise.resolve(args))

const interceptor1 = req => {
  req.url += '?param1=value1';
  return req;
}
const interceptor2 = async req => {
  req.url += '&param2=value2';
  return Promise.resolve(req);
};

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
  requestInterceptor: pipeP(interceptor1, interceptor2),
};

SwaggerClient.http(request); 
// Requested URL: https://httpbin.org/?param1=value1&param2=value2

Note: you can mutate or assign any property of Request object as long as your interceptor produces a valid Request object again.

Response

Although we internally use Fetch Compatible Response, we expose a POJO compatible in shape with Fetch Compatible Response.

Below is a list of Response properties:

Property Description
ok Boolean. A boolean indicating whether the response was successful (status in the range 200–299) or not.
status Number. The status code of the response. (This will be 200 for a success).
statusText String. The status message corresponding to the status code. (e.g., OK for 200).
url String. Request url.
headers Object. The Headers object associated with the response.
text String | Blob. Textual body, or Blob.
data String | Blob. Textual body, or Blob. (Legacy property)
body Object=undefined. JSON object or undefined.
obj Object=undefined. JSON object or undefined. Mirrors body property. (Legacy property)
Response Interceptor

Response interceptor can be configured via responseInterceptor property in Request object. When set, it intercepts the Response object after the actual HTTP request was made. Response interceptor can be defined as synchronous (transformer) or asynchronous function (allows other async operations inside).

Transformer usage:

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
  responseInterceptor: res => {
    res.arbitraryProp = 'arbitrary value';
    return res;
  },
};

SwaggerClient.http(request); 
/**
 * Promise({
 *   ok: true,
 *   status: 200,
 *   statusText: 'OK', 
 *   url: 'https://httpbin.org/',
 *   headers: {...},
 *   text: '{"prop":"value"}',
 *   data: '{"prop":"value"}',
 *   body: {prop: 'value'},
 *   obj: {prop: 'value'},
 *   arbitraryProp: 'arbitrary value',
 * })
 */

Async operation usage:

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
  responseInterceptor: async res => {
    const { body: { idToken } } = await SwaggerClient.http({ url: 'https://identity.com/idtoken.json' });
    res.idToken = idToken;
    return res;
  },
};

SwaggerClient.http(request); 
/**
 * Promise({
 *   ok: true,
 *   status: 200,
 *   statusText: 'OK', 
 *   url: 'https://httpbin.org/',
 *   headers: {...},
 *   text: '{"prop":"value"}',
 *   data: '{"prop":"value"}',
 *   body: {prop: 'value'},
 *   obj: {prop: 'value'},
 *   idToken: '308949304832084320',
 * })
 */

You're not limited to using one Response interceptor function. You can easily compose pipe of response interceptors.

import SwaggerClient from 'swagger-client';

const pipeP = (...fns) => args => fns.reduce((arg, fn) => arg.then(fn), Promise.resolve(args))

const interceptor1 = res => {
  res.prop += 'value1';
  return res;
}
const interceptor2 = async res => {
  res.prop += '+value2';
  return Promise.resolve(res);
};

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
  responseInterceptor: pipeP(interceptor1, interceptor2),
};

SwaggerClient.http(request); 
/**
 * Promise({
 *   ok: true,
 *   status: 200,
 *   statusText: 'OK', 
 *   url: 'https://httpbin.org/',
 *   headers: {...},
 *   text: '{"prop":"value"}',
 *   data: '{"prop":"value"}',
 *   body: {prop: 'value'},
 *   obj: {prop: 'value'},
 *   prop: 'value1+value2',
 * })
 */

Note: you can mutate or assign any property of Response object as long as your interceptor produces a valid Response object again.

Accessing request in Response Interceptor

In order to access original Request in responseInterceptor you have to declare responseInterceptor either as function declaration or function expression. By doing so, the interceptor will be bound to the original Request.

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
  responseInterceptor: function(res) {
    const request = this;
    console.log(request.url); // https://httpbin.org/
    console.log(request.method); // GET
    
    res.arbitraryProp = 'arbitrary value';
    return res;
  },
};

SwaggerClient.http(request); 

Note: this will not work if arrow functions are used to define a responseInterceptor.

Response serialization

We detect the response Content-Type which identifies the payload of the Response. If the Content-Type headers signals that the data can be represented as something we can parse, we parse the data as and expose the result as Response.body property.

Supported serialization formats: json, yaml

Headers serialization

Our headers serialization algorithm serializes headers into an object, where mutliple-headers result in an array.

// eg: Cookie: one
//     Cookie: two
//  =  { Cookie: [ "one", "two" ]

Errors

HTTP Error

HTTP Client promise will reject with a TypeError when a network error is encountered or CORS is misconfigured on the server-side, although this usually means permission issues or similar.

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
};

SwaggerClient.http(request); // network problem or CORS misconfiguration
/**
 * Rejected promise will be returned.
 *
 * Promise.<Error> 
 */
Status Error

HTTP Client will reject all responses with ok field set to false - status outside of the range 200-299. This is another distinction from standard Fetch API.

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/non-existing-path',
  method: 'GET',
};

SwaggerClient.http(request);
/**
 * Rejected promise will be returned.
 *
 * Promise.<Error{
 *   message: 'NOT FOUND',
 *   statusCode: 404,
 *   response: Response // original response object
 * }>
 *    
 */
Serialization Error

When there is an error during headers or response serialization, the Response object will contain one additional property parseError and body/obj properties will not be present in Response.

Property Description
parseError Error=undefined. Error produced during headers or response serialization.
Interceptor Error

If you defined responseInterceptor on Request object, it can theoretically produce an unexpected Error. When it happens HTTP Client will produce a rejected promise with following signature:

import SwaggerClient from 'swagger-client';

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
  responseInterceptor: () => { throw new Error('error'); },
};

SwaggerClient.http(request);
/**
 * Rejected promise will be returned.
 *
 * Promise.<Error{
 *   message: 'OK',
 *   statusCode: 200,
 *   responseError: Error // error thrown in responseInterceptor
 * }>
 *    
 */

Custom Fetch

HTTP client allows you to override the behavior of how it makes actual HTTP Request by supplying an asynchronous function under the property called userFetch in Request object.

Warning: userFetch function must return instance of Fetch Compatible Response class or a compatible object of expected shape or behavior. After importing the SwaggerClient in your current module, the Response symbol will be available in the current scope.

Example 1.)

Using axios library as override.

import SwaggerClient from 'swagger-client';
import axios from 'axios';

const request = {
  url: 'https://httpbin.org/',
  method: 'GET',
  userFetch: async (url, req) => {
    const axiosRequest = { ...req, data: req.body };
    const axiosResponse = await axios(axiosRequest);

    return new Response(axiosResponse.data.data, {
      status: response.status,
      headers: response.headers,
    });
  },
};

SwaggerClient.http(request);
Example 2.)

Using axios library as override in browser and tracking upload progress.

<html>
  <head>
    <script src="//unpkg.com/swagger-client"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.js"></script>
    <script>
      const request = {
        url: 'https://httpbin.org/post',
        method: 'POST',
        body: "data".repeat(1000000),
        userFetch: async (url, req) => {
          const onUploadProgress = (progressEvent) => {
            const completed = Math.round((progressEvent.loaded * 100) / progressEvent.total);
            console.log(`${completed}%`);
          }
          const axiosRequest = { ...req, data: req.body, onUploadProgress };
          const axiosResponse = await axios(axiosRequest);

          return new Response(axiosResponse.data.data, {
            status: response.status,
            headers: response.headers,
          });
        },
      };

      SwaggerClient.http(request);
    </script>
  </head>
  <body>
    check console in browser's dev. tools
  </body>
</html>

CORS

Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell browsers to give a web application running at one origin, access to selected resources from a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own.

HTTP Client supports cors via two properties on Request object.

Property Description
mode String=["cors"*, "no-cors", "same-origin", "navigate"]. Contains the mode of the request. See more in Request.mode documentation.
credentials String=["omit", "same-origin"*, "include"]. Contains the credentials of the request. See more in Request.credentials documentation.
CORS enabled globally

If you need to activate CORS globally for every request, just enable it by withCredentials on HTTP Client. When enabled, it automatically sets credentials="include" on every request implicitly for you.

import SwaggerClient from 'swagger-client';

SwaggerClient.http.withCredentials = true;

Request cancellation with AbortSignal

You may cancel requests with AbortController. The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired. Using AbortController, you can easily implement request timeouts.

Node.js

AbortController needs to be introduced in Node.js environment via abort-controller npm package.

const SwaggerClient = require('swagger-client');
const AbortController = require('abort-controller');

const controller = new AbortController();
const { signal } = controller;
const timeout = setTimeout(() => {
  controller.abort();
}, 1);

(async () => {
  try {
    await SwaggerClient.http({ url: 'https://www.google.com', signal });
  } catch (error) {
    if (error.name === 'AbortError') {
      console.error('request was aborted');
    }
  } finally {
    clearTimeout(timeout);
  }
})();
Browser

AbortController is part of modern Web APIs. No need to install it explicitly.

<html>
  <head>
    <script src="//unpkg.com/swagger-client"></script>
    <script>
        const controller = new AbortController();
        const { signal } = controller;
        const timeout = setTimeout(() => {
          controller.abort();
        }, 1);

        (async () => {
          try {
            await SwaggerClient.http({ url: 'https://www.google.com', signal });
          } catch (error) {
            if (error.name === 'AbortError') {
              console.error('request was aborted');
            }
          } finally {
            clearTimeout(timeout);
          }
        })();
    </script>
  </head>
  <body>
    check console in browser's dev. tools
  </body>
</html>

Alternate API

It's also possible (for convenience) to call HTTP Client from SwaggerClient instances.

import SwaggerClient from 'swagger-client';

const { client } = new SwaggerClient({ spec: 'http://petstore.swagger.io/v2/swagger.json' });
client.http(request);