Skip to content

Instantly share code, notes, and snippets.

@lancegliser
Created November 6, 2019 15:36
Show Gist options
  • Select an option

  • Save lancegliser/a1c7394c91f9dbddc540eae8c5dc7515 to your computer and use it in GitHub Desktop.

Select an option

Save lancegliser/a1c7394c91f9dbddc540eae8c5dc7515 to your computer and use it in GitHub Desktop.
A module providing an axios based service that uses interceptors for handling authentication, logging, and errors in calls
import axios, {AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse} from "axios";
import {Constants} from "./Constants";
import AxiosUtilities from "./Utilities/AxiosUtilities";
import {
GetServiceCredentials,
IServicesCredentials,
} from "./IServiceTypes";
/**
* Provides constants and functionality common to and api service.
*/
export default class Service {
protected static LOCAL_SERVER = 'https://local.example.com/';
protected static TEST_SERVER = 'https://test.example.com/';
protected static PRODUCTION_SERVER = 'https://example.com/';
protected static ENDPOINT_V1 = 'v1';
// https://example.com/auth/v1
public static ENDPOINT_AUTH = 'auth/' + Service.ENDPOINT_V1;
protected static ANONYMOUS_ENDPOINTS = [
Service.ENDPOINT_AUTH,
];
protected static DEFAULT_TIMEOUT = Constants.DEFAULT_TIMEOUT;
protected env!: string;
protected client!: AxiosInstance;
protected getAccessToken: GetServiceCredentials | undefined;
protected credentials: IServicesCredentials | undefined;
public static getServiceUrl(environment: string) {
switch (environment) {
case Constants.ENVIRONMENT_LOCAL:
return Service.LOCAL_SERVER;
case Constants.ENVIRONMENT_TEST:
case Constants.ENVIRONMENT_UAT:
return Service.TEST_SERVER;
case Constants.ENVIRONMENT_PRODUCTION:
return Service.PRODUCTION_SERVER;
default:
throw new Error('Unhandled environment: ' + environment);
}
}
/**
* @param environment
*/
constructor(environment: string) {
this.setEnvironment(environment);
}
/**
* @param environment
*/
setEnvironment(environment: string) {
this.credentials = undefined;
this.client = axios.create({
baseURL: Service.getServiceUrl(environment),
timeout: Service.DEFAULT_TIMEOUT,
});
// Bind to incoming requests
this.client.interceptors.request.use(this.onRequest.bind(this));
// Bind to responses
this.client.interceptors.response.use(this.onResponseSuccess.bind(this), this.onResponseError.bind(this));
this.env = environment;
return this;
}
/**
* Modifies the original request adding authorization if required.
*/
protected onRequest(originalRequest: AxiosRequestConfig): Promise<AxiosRequestConfig> {
// Return a promise as some requests may need to try auth
return new Promise((resolve, reject) => {
this.addAuthorizationHeader(originalRequest)
.then(resolve);
});
}
/**
* Provides success response handling
*/
protected onResponseSuccess(response: AxiosResponse): Promise<AxiosResponse> {
// Return a promise as some requests may need to retry auth
return new Promise((resolve, reject) => {
resolve(response);
});
}
/**
* Provides failure response handling
*/
protected onResponseError(error: AxiosError) {
// Return a promise as some requests may need to retry auth
return new Promise((resolve, reject) => {
if (
error.response &&
error.response.status === 403
) {
// TODO clear the existing authentication and try again
}
AxiosUtilities.logResponseError(error);
reject(error);
});
}
/**
* @param func
* @return this
*/
setGetAccessToken(func: GetServiceCredentials) {
this.getAccessToken = func;
return this;
}
/**
* Middleware
*/
addAuthorizationHeader(originalRequest: AxiosRequestConfig): Promise<AxiosRequestConfig> {
// Ignore anonymous endpoint requests
if (!originalRequest.url || Service.ANONYMOUS_ENDPOINTS.indexOf(originalRequest.url) >= 0) {
// console.debug('Skipping authorization: ' + originalRequest.url);
return Promise.resolve(originalRequest);
}
return this.addAuthorization(originalRequest);
}
protected addAuthorization(originalRequest: AxiosRequestConfig) {
const _this = this;
// TODO remove the credentials if this.credentials.ValidTill has passed
if (!this.credentials) {
return this.getCredentials()
.then((v1Credentials) => {
this.credentials = v1Credentials;
attachAuthorization();
return Promise.resolve(originalRequest);
});
}
if (this.credentials) {
attachAuthorization();
}
return Promise.resolve(originalRequest);
function attachAuthorization() {
if (!_this.credentials) {
throw new Error('Credentials were not available');
}
originalRequest.headers.Authorization = `Bearer ${_this.credentials.Token}`;
}
}
/**
* Gets the credential for the service using intermediary auth function providing during initialization
*/
async getCredentials(): Promise<IServicesCredentials> {
if (!this.getAccessToken) {
return new Promise((resolve, reject) => {
reject("getAccessToken is not defined");
});
}
return this.getAccessToken();
}
}
import {AxiosError} from "axios";
export default class AxiosUtilities {
static logResponseError(error: AxiosError): void {
console.error('Response error:', error.message, error.config.method, error.config.url);
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.debug('Status: ', error.response.status);
console.debug('Headers:', error.response.headers);
console.debug('Data:', error.response.data);
} else {
if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.debug('Request:', error.request);
}
}
console.debug('Configuration:', error.config);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment