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(linear-retries): [nan-2309] handle linear case #3108

Merged
2 changes: 2 additions & 0 deletions packages/shared/lib/models/Proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export interface InternalProxyConfiguration {
export interface RetryHeaderConfig {
at?: string;
after?: string;
status_code?: string;
body_contains?: string;
}

export enum PaginationType {
Expand Down
31 changes: 29 additions & 2 deletions packages/shared/lib/services/proxy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { ResponseType, ApplicationConstructedProxyConfiguration, UserProvid

import { interpolateIfNeeded, connectionCopyWithParsedConnectionConfig, mapProxyBaseUrlInterpolationFormat } from '../utils/utils.js';
import { NangoError } from '../utils/error.js';
import type { MessageRowInsert } from '@nangohq/types';
import type { MessageRowInsert, RetryHeaderConfig } from '@nangohq/types';
import { getProvider } from './providers.js';
import { redactHeaders, redactURL } from '../utils/http.js';

Expand Down Expand Up @@ -259,7 +259,8 @@ class ProxyService {
(error.response?.status === 403 && error.response.headers['x-ratelimit-remaining'] && error.response.headers['x-ratelimit-remaining'] === '0') ||
error.response?.status === 429 ||
['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED'].includes(error.code as string) ||
config.retryOn?.includes(Number(error.response?.status))
config.retryOn?.includes(Number(error.response?.status)) ||
this.isProviderRetryTriggered(config.provider.proxy?.retry, error)
) {
if (config.retryHeader) {
const type = config.retryHeader.at ? 'at' : 'after';
Expand Down Expand Up @@ -298,6 +299,32 @@ class ProxyService {
return false;
};

private isProviderRetryTriggered(retryConfig: RetryHeaderConfig | undefined, response: AxiosError): boolean {
if (!retryConfig) {
return false;
}

const { status_code, body_contains } = retryConfig;

const statusCodeValid =
status_code && status_code.includes('x')
bodinsamuel marked this conversation as resolved.
Show resolved Hide resolved
? (() => {
const statusCode = response.response?.status?.toString().charAt(0);
return statusCode && status_code.includes(statusCode);
})()
: true;

const bodyContainsValid = body_contains
? (() => {
const body = response.response?.data;
const bodyString = typeof body === 'string' ? body : JSON.stringify(body);
return bodyString.includes(body_contains);
})()
: true;

return Boolean(statusCodeValid && bodyContainsValid);
}

/**
* Send to http method
* @desc route the call to a HTTP request based on HTTP method passed in
Expand Down
139 changes: 139 additions & 0 deletions packages/shared/lib/services/proxy.service.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,145 @@ describe('Proxy service Construct URL Tests', () => {
expect(diff).toBeGreaterThan(1000);
expect(diff).toBeLessThan(2000);
});

it('Should retry based on provider specific config', async () => {
const nowInSecs = Date.now() / 1000;
const mockAxiosError = {
response: {
status: 400,
code: 400,
headers: {
'x-ratelimit-requests-reset': nowInSecs + 1
},
data: {
errors: [
{
extensions: {
userError: true,
code: 'RATELIMITED',
meta: {},
type: 'ratelimited',
userPresentableMessage: 'Rate limit exceeded. Only 1200 requests are allowed per 1 hour.'
},
message:
'Rate limit exceeded. Only 1200 requests are allowed per 1 hour. For more information see our developer docs at: https://developers.linear.app/docs/graphql/working-with-the-graphql-api/rate-limiting'
}
]
},
statusText: 'Bad Request',
config: {} as InternalAxiosRequestConfig
} as AxiosResponse
} as AxiosError;
const config = {
provider: {
auth_mode: 'OAUTH2',
proxy: {
retry: {
at: 'x-ratelimit-requests-reset',
status_code: '4xx',
body_contains: 'RATELIMITED'
}
}
},
token: 'some-oauth-access-token'
} as ApplicationConstructedProxyConfiguration;
const before = Date.now();
const willRetry = await proxyService.retry(config, [], mockAxiosError, 0);
const after = Date.now();
const diff = after - before;
expect(diff).toBeGreaterThan(1000);
expect(diff).toBeLessThan(2000);
expect(willRetry).toBe(true);
});

it('Should not retry based on provider specific config if the body does not contain the value', async () => {
const nowInSecs = Date.now() / 1000;
const mockAxiosError = {
response: {
status: 400,
code: 400,
headers: {
'x-ratelimit-requests-reset': nowInSecs + 1
},
data: {
errors: [
{
extensions: {
userError: true,
code: 'NO',
meta: {},
type: 'something_else',
userPresentableMessage: ''
},
message: 'Nothing'
}
]
},
statusText: 'Bad Request',
config: {} as InternalAxiosRequestConfig
} as AxiosResponse
} as AxiosError;
const config = {
provider: {
auth_mode: 'OAUTH2',
proxy: {
retry: {
at: 'x-ratelimit-requests-reset',
status_code: '6xx',
body_contains: 'RATELIMITED'
}
}
},
token: 'some-oauth-access-token'
} as ApplicationConstructedProxyConfiguration;
const willRetry = await proxyService.retry(config, [], mockAxiosError, 0);
expect(willRetry).toBe(false);
});

it('Should not retry based on provider specific config', async () => {
const nowInSecs = Date.now() / 1000;
const mockAxiosError = {
response: {
status: 400,
code: 400,
headers: {
'x-ratelimit-requests-reset': nowInSecs + 1
},
data: {
errors: [
{
extensions: {
userError: true,
code: 'RATELIMITED',
meta: {},
type: 'ratelimited',
userPresentableMessage: 'Rate limit exceeded. Only 1200 requests are allowed per 1 hour.'
},
message:
'Rate limit exceeded. Only 1200 requests are allowed per 1 hour. For more information see our developer docs at: https://developers.linear.app/docs/graphql/working-with-the-graphql-api/rate-limiting'
}
]
},
statusText: 'Bad Request',
config: {} as InternalAxiosRequestConfig
} as AxiosResponse
} as AxiosError;
const config = {
provider: {
auth_mode: 'OAUTH2',
proxy: {
retry: {
at: 'x-ratelimit-requests-reset',
status_code: '6xx',
body_contains: 'RATELIMITED'
}
}
},
token: 'some-oauth-access-token'
} as ApplicationConstructedProxyConfiguration;
const willRetry = await proxyService.retry(config, [], mockAxiosError, 0);
expect(willRetry).toBe(false);
});
});

describe('Proxy service configure', () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/providers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3639,6 +3639,10 @@ linear:
prompt: consent
proxy:
base_url: https://api.linear.app
retry:
at: 'X-RateLimit-Requests-Reset'
status_code: 4xx
body_contains: RATELIMITED
disable_pkce: true
webhook_routing_script: linearWebhookRouting
post_connection_script: linearPostConnection
Expand Down
2 changes: 2 additions & 0 deletions packages/types/lib/proxy/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export interface InternalProxyConfiguration {
export interface RetryHeaderConfig {
at?: string;
after?: string;
status_code?: string;
body_contains?: string;
}

export enum PaginationType {
Expand Down
6 changes: 6 additions & 0 deletions scripts/validation/providers/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@
},
"at": {
"type": "string"
},
"status_code": {
"type": "string"
bodinsamuel marked this conversation as resolved.
Show resolved Hide resolved
},
"body_contains": {
"type": "string"
}
}
},
Expand Down
Loading