Skip to content

Commit

Permalink
fix: process cookies in failed responses (#251)
Browse files Browse the repository at this point in the history
* fix: process cookies in failed responses

This commit updates our support for cookies
to allow cookies to be scraped from non-2xx
responses in addition to 2xx responses.

Signed-off-by: Phil Adams <phil_adams@us.ibm.com>
Signed-off-by: Dustin Popp <dpopp07@gmail.com>
Co-authored-by: Dustin Popp <dpopp07@gmail.com>
  • Loading branch information
padamstx and dpopp07 authored Sep 20, 2023
1 parent 2b8ea26 commit 52f758b
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 75 deletions.
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

0 comments on commit 52f758b

Please sign in to comment.