From 654ec0a6e52396471f85fd17e092b917987c58d8 Mon Sep 17 00:00:00 2001 From: Roman Davydchuk Date: Thu, 16 Apr 2026 15:55:51 +0000 Subject: [PATCH 1/4] feat: Add option to skip OAuth token refresh --- .../utils/request-helper-functions.ts | 6 ++++-- packages/workflow/src/interfaces.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts b/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts index 622a0d87fff02..2f33ffada219a 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts @@ -889,6 +889,7 @@ export async function requestOAuth2( }); } const tokenExpiredStatusCode = resolveTokenExpiredStatusCode(oAuth2Options, credentials); + const shouldSkipTokenRefresh = oAuth2Options?.skipTokenRefresh === true; const refreshCtx: RefreshOAuth2TokenContext = { credentials, @@ -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; @@ -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 @@ -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), ); diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index d76ba3b2a5a68..c7e8c83db0fd6 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -79,6 +79,7 @@ export interface IBinaryData { // credentials file. export interface IOAuth2Options { includeCredentialsOnRefreshOnBody?: boolean; + skipTokenRefresh?: boolean; property?: string; tokenType?: string; keepBearer?: boolean; From b748d517a4c4ff33a4d4a307534ae7e3aecca32e Mon Sep 17 00:00:00 2001 From: Roman Davydchuk Date: Thu, 16 Apr 2026 16:00:34 +0000 Subject: [PATCH 2/4] test: Add tests --- .../request-helper-functions.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts index efb1639921a4c..1f60ad1943ff2 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts @@ -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', () => { From 4fe07996f43b56fe4baebe9c3443173bdd209091 Mon Sep 17 00:00:00 2001 From: Roman Davydchuk Date: Thu, 16 Apr 2026 16:01:45 +0000 Subject: [PATCH 3/4] fix: Don't try to refresh ClickUp's OAuth access token --- packages/nodes-base/nodes/ClickUp/GenericFunctions.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts b/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts index f816ff1843787..4f2b7bdf859e6 100644 --- a/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts @@ -41,6 +41,9 @@ export async function clickupApiRequest( const oAuth2Options: IOAuth2Options = { keepBearer: false, tokenType: 'Bearer', + // ClickUp's access token don't expire and + // ClickUp does not return refresh tokens + skipTokenRefresh: true, }; return await this.helpers.requestOAuth2.call( this, From 90517fafe395928520962c1bb6c96263cb5e4030 Mon Sep 17 00:00:00 2001 From: RomanDavydchuk Date: Mon, 20 Apr 2026 12:39:55 +0300 Subject: [PATCH 4/4] Update packages/nodes-base/nodes/ClickUp/GenericFunctions.ts Co-authored-by: Dawid Myslak --- packages/nodes-base/nodes/ClickUp/GenericFunctions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts b/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts index 4f2b7bdf859e6..88f6a646dc715 100644 --- a/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts @@ -41,7 +41,7 @@ export async function clickupApiRequest( const oAuth2Options: IOAuth2Options = { keepBearer: false, tokenType: 'Bearer', - // ClickUp's access token don't expire and + // ClickUp's access token doesn't expire and // ClickUp does not return refresh tokens skipTokenRefresh: true, };