Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,50 @@ describe('Request Helper Functions', () => {
expect(result).toEqual({ success: true });
expect(mockThis.helpers.httpRequest).toHaveBeenCalledTimes(2);
});

test('should NOT retry on token-expired status when oAuth2Options.skipTokenRefresh is true (isN8nRequest path)', async () => {
mockThis.getCredentials.mockResolvedValue(makeCredentialData());
const error401 = Object.assign(new Error('401'), { response: { status: 401 } });
mockThis.helpers.httpRequest.mockRejectedValueOnce(error401);

await expect(
requestOAuth2.call(
mockThis,
'testOAuth2',
{ method: 'GET', url: `${baseUrl}/data` },
mockNode,
mockAdditionalData,
{ skipTokenRefresh: true },
true,
),
).rejects.toThrow('401');
expect(mockThis.helpers.httpRequest).toHaveBeenCalledTimes(1);
expect(
mockAdditionalData.credentialsHelper.updateCredentialsOauthTokenData,
).not.toHaveBeenCalled();
});

test('should NOT retry on token-expired status when oAuth2Options.skipTokenRefresh is true (legacy request path)', async () => {
mockThis.getCredentials.mockResolvedValue(makeCredentialData());
const error401 = Object.assign(new Error('401'), { statusCode: 401 });
mockThis.helpers.request.mockRejectedValueOnce(error401);

await expect(
requestOAuth2.call(
mockThis,
'testOAuth2',
{ method: 'GET', url: `${baseUrl}/data` },
mockNode,
mockAdditionalData,
{ skipTokenRefresh: true },
false,
),
).rejects.toThrow('401');
expect(mockThis.helpers.request).toHaveBeenCalledTimes(1);
expect(
mockAdditionalData.credentialsHelper.updateCredentialsOauthTokenData,
).not.toHaveBeenCalled();
});
});

describe('requestOAuth2 - client credentials initial token fetch', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@ export async function requestOAuth2(
});
}
const tokenExpiredStatusCode = resolveTokenExpiredStatusCode(oAuth2Options, credentials);
const shouldSkipTokenRefresh = oAuth2Options?.skipTokenRefresh === true;

const refreshCtx: RefreshOAuth2TokenContext = {
credentials,
Expand Down Expand Up @@ -916,7 +917,7 @@ export async function requestOAuth2(

if (isN8nRequest) {
return await this.helpers.httpRequest(newRequestOptions).catch(async (error: AxiosError) => {
if (error.response?.status === tokenExpiredStatusCode) {
if (!shouldSkipTokenRefresh && error.response?.status === tokenExpiredStatusCode) {
return await retryWithNewToken(async (opts) => await this.helpers.httpRequest(opts));
}
throw error;
Expand All @@ -928,6 +929,7 @@ export async function requestOAuth2(
.then((response) => {
const requestOptions = newRequestOptions as any;
if (
!shouldSkipTokenRefresh &&
requestOptions.resolveWithFullResponse === true &&
requestOptions.simple === false &&
response.statusCode === tokenExpiredStatusCode
Expand All @@ -937,7 +939,7 @@ export async function requestOAuth2(
return response;
})
.catch(async (error: IResponseError) => {
if (error.statusCode === tokenExpiredStatusCode) {
if (!shouldSkipTokenRefresh && error.statusCode === tokenExpiredStatusCode) {
return await retryWithNewToken(
async (opts) => await this.helpers.request(opts as IRequestOptions),
);
Expand Down
3 changes: 3 additions & 0 deletions packages/nodes-base/nodes/ClickUp/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export async function clickupApiRequest(
const oAuth2Options: IOAuth2Options = {
keepBearer: false,
tokenType: 'Bearer',
// ClickUp's access token doesn't expire and
// ClickUp does not return refresh tokens
skipTokenRefresh: true,
};
return await this.helpers.requestOAuth2.call(
this,
Expand Down
1 change: 1 addition & 0 deletions packages/workflow/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export interface IBinaryData {
// credentials file.
export interface IOAuth2Options {
includeCredentialsOnRefreshOnBody?: boolean;
skipTokenRefresh?: boolean;
property?: string;
tokenType?: string;
keepBearer?: boolean;
Expand Down
Loading