fix google signup edge case (#18365)
Fixes an edge case when a user signs up with Google and the profile avatar network request times out, we crash instead of creating the user without an avatar. Added `axios-retry` to retry max 2 times and if it still fails we gracefully skip avatar image instead of crashing Fixes Sentry TWENTY-SERVER-FDQ Sonarly https://sonarly.com/issue/6564 --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
+44
@@ -1,9 +1,16 @@
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
|
||||
import axiosRetry from 'axios-retry';
|
||||
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
jest.mock('axios-retry', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const createMockConfigService = (
|
||||
overrides: Record<string, unknown> = {},
|
||||
): TwentyConfigService => {
|
||||
@@ -81,6 +88,43 @@ describe('SecureHttpClientService', () => {
|
||||
|
||||
expect(client.defaults.baseURL).toBe('https://example.com/api');
|
||||
});
|
||||
|
||||
it('should configure axios-retry when retries is greater than 0', () => {
|
||||
jest.mocked(axiosRetry).mockClear();
|
||||
const service = new SecureHttpClientService(createMockConfigService());
|
||||
const client = service.getHttpClient({
|
||||
retries: 2,
|
||||
shouldResetTimeout: true,
|
||||
});
|
||||
|
||||
expect(axiosRetry).toHaveBeenCalledWith(client, {
|
||||
retries: 2,
|
||||
shouldResetTimeout: true,
|
||||
retryCondition: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not configure axios-retry when retries is 0', () => {
|
||||
jest.mocked(axiosRetry).mockClear();
|
||||
const service = new SecureHttpClientService(createMockConfigService());
|
||||
|
||||
service.getHttpClient({ retries: 0 });
|
||||
|
||||
expect(axiosRetry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not leak retry config into axios defaults', () => {
|
||||
const service = new SecureHttpClientService(createMockConfigService());
|
||||
const client = service.getHttpClient({
|
||||
retries: 2,
|
||||
shouldResetTimeout: true,
|
||||
baseURL: 'https://example.com',
|
||||
});
|
||||
|
||||
expect(client.defaults.baseURL).toBe('https://example.com');
|
||||
expect(client.defaults).not.toHaveProperty('retries');
|
||||
expect(client.defaults).not.toHaveProperty('shouldResetTimeout');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInternalHttpClient', () => {
|
||||
|
||||
+24
-4
@@ -1,6 +1,8 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import axios, { type AxiosInstance, type CreateAxiosDefaults } from 'axios';
|
||||
import axiosRetry from 'axios-retry';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { createSsrfSafeAgent } from 'src/engine/core-modules/secure-http-client/utils/create-ssrf-safe-agent.util';
|
||||
import { resolveAndValidateHostname } from 'src/engine/core-modules/secure-http-client/utils/resolve-and-validate-hostname.util';
|
||||
@@ -10,6 +12,11 @@ import { type OutboundRequestContext } from './outbound-request-context.type';
|
||||
|
||||
const MAX_REDIRECTS = 5;
|
||||
|
||||
type SecureHttpClientConfig = CreateAxiosDefaults & {
|
||||
retries?: number;
|
||||
shouldResetTimeout?: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class SecureHttpClientService {
|
||||
private readonly logger = new Logger(SecureHttpClientService.name);
|
||||
@@ -22,24 +29,37 @@ export class SecureHttpClientService {
|
||||
// When context is provided, outbound requests are logged with
|
||||
// workspace/user info for GuardDuty correlation.
|
||||
getHttpClient(
|
||||
config?: CreateAxiosDefaults,
|
||||
config?: SecureHttpClientConfig,
|
||||
context?: OutboundRequestContext,
|
||||
): AxiosInstance {
|
||||
const { retries, shouldResetTimeout, ...axiosConfig } = config ?? {};
|
||||
|
||||
const isSafeModeEnabled = this.twentyConfigService.get(
|
||||
'OUTBOUND_HTTP_SAFE_MODE_ENABLED',
|
||||
);
|
||||
|
||||
const client = isSafeModeEnabled
|
||||
? axios.create({
|
||||
...config,
|
||||
...axiosConfig,
|
||||
httpAgent: createSsrfSafeAgent('http'),
|
||||
httpsAgent: createSsrfSafeAgent('https'),
|
||||
maxRedirects: Math.min(
|
||||
config?.maxRedirects ?? MAX_REDIRECTS,
|
||||
axiosConfig.maxRedirects ?? MAX_REDIRECTS,
|
||||
MAX_REDIRECTS,
|
||||
),
|
||||
})
|
||||
: axios.create(config);
|
||||
: axios.create(axiosConfig);
|
||||
|
||||
if (isDefined(retries) && retries > 0) {
|
||||
axiosRetry(client, {
|
||||
retries,
|
||||
shouldResetTimeout,
|
||||
retryCondition: (error) =>
|
||||
axiosRetry.isNetworkOrIdempotentRequestError(error) &&
|
||||
error.code !== 'ECONNABORTED' &&
|
||||
error.code !== 'ETIMEDOUT',
|
||||
});
|
||||
}
|
||||
|
||||
if (context) {
|
||||
client.interceptors.request.use((requestConfig) => {
|
||||
|
||||
Reference in New Issue
Block a user