Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: process cookies in failed responses #251

Merged
merged 5 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v16.20.2
1 change: 0 additions & 1 deletion lib/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,6 @@ export class BaseService {
* @param parameters - see `parameters` in `createRequest`
* @param deserializerFn - the deserializer function that is applied on the response object
* @param isMap - is `true` when the response object should be handled as a map
* @protected
* @returns a Promise
*/
protected createRequestAndDeserializeResponse(
Expand Down
111 changes: 72 additions & 39 deletions lib/cookie-support.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* (C) Copyright IBM Corp. 2022.
* (C) Copyright IBM Corp. 2022, 2023.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,33 +14,24 @@
* limitations under the License.
*/

import { Axios, AxiosResponse, InternalAxiosRequestConfig, isAxiosError } from 'axios';
import extend from 'extend';
import { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { Cookie, CookieJar } from 'tough-cookie';
import logger from './logger';

export class CookieInterceptor {
private readonly cookieJar: CookieJar;

constructor(cookieJar: CookieJar | boolean) {
if (cookieJar) {
if (cookieJar === true) {
logger.debug('CookieInterceptor: creating new CookieJar');
this.cookieJar = new CookieJar();
} else {
logger.debug('CookieInterceptor: using supplied CookieJar');
this.cookieJar = cookieJar;
}
} else {
throw new Error('Must supply a cookie jar or true.');
}
}

public async requestInterceptor(config: InternalAxiosRequestConfig) {
const internalCreateCookieInterceptor = (cookieJar: CookieJar) => {
/**
* This is called by Axios when a request is about to be sent in order to
* copy the cookie string from the URL to a request header.
*
* @param config the Axios request config
* @returns the request config
*/
async function requestInterceptor(config: InternalAxiosRequestConfig) {
logger.debug('CookieInterceptor: intercepting request');
if (config && config.url) {
logger.debug(`CookieInterceptor: getting cookies for: ${config.url}`);
const cookieHeaderValue = await this.cookieJar.getCookieString(config.url);
const cookieHeaderValue = await cookieJar.getCookieString(config.url);
if (cookieHeaderValue) {
logger.debug('CookieInterceptor: setting cookie header');
const cookieHeader = { cookie: cookieHeaderValue };
Expand All @@ -54,25 +45,67 @@ export class CookieInterceptor {
return config;
}

public async responseInterceptor(response: AxiosResponse) {
logger.debug('CookieInterceptor: intercepting response.');
if (response && response.headers) {
logger.debug('CookieInterceptor: checking for set-cookie headers.');
const cookies: string[] = response.headers['set-cookie'];
if (cookies) {
logger.debug(`CookieInterceptor: setting cookies in jar for URL ${response.config.url}.`);
// Write cookies sequentially by chaining the promises in a reduce
await cookies.reduce(
(cookiePromise: Promise<Cookie>, cookie: string) =>
cookiePromise.then(() => this.cookieJar.setCookie(cookie, response.config.url)),
Promise.resolve(null)
);
} else {
logger.debug('CookieInterceptor: no set-cookie headers.');
}
/**
* This is called by Axios when a 2xx response has been received.
* We'll invoke the configured cookie jar's setCookie() method to handle
* the "set-cookie" header.
* @param response the Axios response object
* @returns the response object
*/
async function responseInterceptor(response: AxiosResponse) {
logger.debug('CookieInterceptor: intercepting response to check for set-cookie headers.');
const cookies: string[] = response.headers['set-cookie'];
if (cookies) {
logger.debug(`CookieInterceptor: setting cookies in jar for URL ${response.config.url}.`);
// Write cookies sequentially by chaining the promises in a reduce
await cookies.reduce(
(cookiePromise: Promise<Cookie>, cookie: string) =>
cookiePromise.then(() => cookieJar.setCookie(cookie, response.config.url)),
Promise.resolve(null)
);
} else {
logger.debug('CookieInterceptor: no response headers.');
logger.debug('CookieInterceptor: no set-cookie headers.');
}

return response;
}
}

/**
* This is called by Axios when a non-2xx response has been received.
* We'll simply invoke the "responseFulfilled" method since we want to
* do the same cookie handler as for a success response.
* @param error the Axios error object that describes the non-2xx response
* @returns the error object
*/
async function responseRejected(error: any) {
logger.debug('CookieIntercepter: intercepting error response');

if (isAxiosError(error)) {
logger.debug('CookieIntercepter: delegating to responseInterceptor()');
await responseInterceptor(error.response);
} else {
logger.debug('CookieInterceptor: no response field in error object, skipping...');
}

return Promise.reject(error);
}

return (axios: Axios) => {
axios.interceptors.request.use(requestInterceptor);
axios.interceptors.response.use(responseInterceptor, responseRejected);
};
};

export const createCookieInterceptor = (cookieJar: CookieJar | boolean) => {
if (cookieJar) {
if (cookieJar === true) {
logger.debug('CookieInterceptor: creating new CookieJar');
return internalCreateCookieInterceptor(new CookieJar());
} else {
logger.debug('CookieInterceptor: using supplied CookieJar');
return internalCreateCookieInterceptor(cookieJar);
}
} else {
throw new Error('Must supply a cookie jar or true.');
}
};
13 changes: 4 additions & 9 deletions lib/request-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */

/**
* (C) Copyright IBM Corp. 2014, 2022.
* (C) Copyright IBM Corp. 2014, 2023.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,7 +35,7 @@ import {
} from './helper';
import logger from './logger';
import { streamToPromise } from './stream-to-promise';
import { CookieInterceptor } from './cookie-support';
import { createCookieInterceptor } from './cookie-support';
import { chainError } from './chain-error';

/**
Expand Down Expand Up @@ -101,14 +101,9 @@ export class RequestWrapper {
this.axiosInstance.defaults.headers[op]['Content-Type'] = 'application/json';
});

// if a cookie jar is provided, wrap the axios instance and update defaults
// if a cookie jar is provided, register our cookie interceptors with axios
if (axiosOptions.jar) {
const cookieInterceptor = new CookieInterceptor(axiosOptions.jar);
const requestCookieInterceptor = (config) => cookieInterceptor.requestInterceptor(config);
const responseCookieInterceptor = (response) =>
cookieInterceptor.responseInterceptor(response);
this.axiosInstance.interceptors.request.use(requestCookieInterceptor);
this.axiosInstance.interceptors.response.use(responseCookieInterceptor);
createCookieInterceptor(axiosOptions.jar)(this.axiosInstance);
}

// get retry config properties and conditionally enable retries
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"eslint-plugin-node": "^9.0.0",
"eslint-plugin-prettier": "^3.0.1",
"jest": "^29.3.1",
"nock": "^13.2.9",
"nock": "^13.3.3",
"npm-run-all": "^4.1.5",
"package-json-reducer": "^1.0.18",
"prettier": "~2.3.0",
Expand Down
Loading