fix(ClickUp Node): Unclear error message when using OAuth credentials (#28584)

Co-authored-by: Dawid Myslak <dawid.myslak@gmail.com>
This commit is contained in:
RomanDavydchuk 2026-04-20 13:33:23 +03:00 committed by GitHub
parent 7b3696f3f7
commit 19aadf19f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 52 additions and 2 deletions

View file

@ -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', () => {

View file

@ -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),
);

View file

@ -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,

View file

@ -79,6 +79,7 @@ export interface IBinaryData {
// credentials file.
export interface IOAuth2Options {
includeCredentialsOnRefreshOnBody?: boolean;
skipTokenRefresh?: boolean;
property?: string;
tokenType?: string;
keepBearer?: boolean;