UNPKG

trainingpeaks-sdk

Version:
306 lines (305 loc) 11.1 kB
import axios from 'axios'; import { wrapper } from 'axios-cookiejar-support'; import { CookieJar } from 'tough-cookie'; import { createHttpError, } from '../../adapters/errors/http-errors.js'; import { generateConsistentBrowserHeaders } from '../../shared/utils/browser-utils.js'; import { generateCurlCommand, } from '../../shared/utils/curl-generator.js'; import { generateRequestId } from '../../shared/utils/request-id.js'; import { executeRequestWithRefresh } from './request-with-refresh.js'; import { DEFAULT_RETRY_CONFIG, RetryHandler } from './retry-handler.js'; const normalizeHttpClientConfig = (config) => { const browserHeaders = config.enableBrowserHeaders !== false ? generateConsistentBrowserHeaders() : {}; return { timeout: config.timeout ?? 30000, headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...browserHeaders, ...config.headers, }, maxRedirects: config.maxRedirects ?? 5, enableCookies: config.enableCookies, enableBrowserHeaders: config.enableBrowserHeaders, retryAttempts: config.retryAttempts ?? DEFAULT_RETRY_CONFIG.attempts, retryDelay: config.retryDelay ?? DEFAULT_RETRY_CONFIG.delay, retryBackoff: config.retryBackoff ?? DEFAULT_RETRY_CONFIG.backoff, retryMaxDelay: config.retryMaxDelay ?? DEFAULT_RETRY_CONFIG.maxDelay, retryJitter: config.retryJitter ?? DEFAULT_RETRY_CONFIG.jitter, logger: config.logger, sessionStorage: config.sessionStorage, }; }; const createAxiosInstance = (config) => { const normalizedConfig = normalizeHttpClientConfig(config); const client = axios.create({ timeout: normalizedConfig.timeout, headers: normalizedConfig.headers, maxRedirects: normalizedConfig.maxRedirects, }); let jar; if (normalizedConfig.enableCookies) { jar = new CookieJar(); wrapper(client); client.defaults.jar = jar; client.defaults.withCredentials = true; } client.interceptors.request.use((config) => { return config; }, (error) => { if (error instanceof Error) { return Promise.reject(error); } return Promise.reject(new Error(typeof error === 'string' ? error : 'Unknown error in request interceptor')); }); client.interceptors.response.use((response) => { return response; }, (error) => { if (error instanceof Error) { return Promise.reject(error); } return Promise.reject(new Error(typeof error === 'string' ? error : 'Unknown error in response interceptor')); }); return { client, jar }; }; const createAxiosConfig = async (baseConfig, config) => { const axiosConfig = { method: config.method.toLowerCase(), url: config.url, data: config.data, params: config.options?.params, headers: { ...baseConfig.headers, ...config.options?.headers, }, timeout: config.options?.timeout ?? baseConfig.timeout, withCredentials: baseConfig.enableCookies ?? false, }; if (baseConfig.sessionStorage) { try { const session = await baseConfig.sessionStorage.get(); if (session?.token?.accessToken) { const currentHeaders = axiosConfig.headers || {}; const authHeaderExists = Object.keys(currentHeaders).some((key) => key.toLowerCase() === 'authorization'); axiosConfig.headers = { ...currentHeaders, ...(authHeaderExists ? {} : { Authorization: `Bearer ${session.token.accessToken}` }), }; } } catch (error) { if (baseConfig.logger) { baseConfig.logger.warn('Failed to retrieve session for Bearer token', { error, }); } } } if (config.options?.cookies && config.options.cookies.length > 0) { const currentHeaders = axiosConfig.headers || {}; const existingCookieKey = Object.keys(currentHeaders).find((key) => key.toLowerCase() === 'cookie'); const existingCookies = existingCookieKey ? currentHeaders[existingCookieKey] : undefined; const newCookies = config.options.cookies.join('; '); const cookieValue = existingCookies ? `${existingCookies}; ${newCookies}` : newCookies; axiosConfig.headers = { ...currentHeaders, Cookie: cookieValue, }; } return axiosConfig; }; const makeRequest = async (client, jar, baseConfig, config) => { const initialAxiosConfig = await createAxiosConfig(baseConfig, config); const retryHandler = new RetryHandler({ attempts: baseConfig.retryAttempts, delay: baseConfig.retryDelay, backoff: baseConfig.retryBackoff, maxDelay: baseConfig.retryMaxDelay, jitter: baseConfig.retryJitter, }); if (baseConfig.logger) { try { const curlData = { method: config.method, url: config.url, headers: initialAxiosConfig.headers, data: initialAxiosConfig.data, }; if (jar) { const cookieStr = await jar.getCookieString(config.url); if (cookieStr) { curlData.cookies = cookieStr.split(';').map((c) => c.trim()); } } const curlCommand = generateCurlCommand(curlData); baseConfig.logger.debug(`HTTP Request as cURL:\n${curlCommand}`); } catch (curlError) { baseConfig.logger.warn('Failed to generate cURL command', { error: curlError, }); } } const requestId = generateRequestId(); if (baseConfig.logger) { baseConfig.logger.debug('HTTP Request starting', { requestId, method: config.method, url: config.url, hasData: !!config.data, hasParams: !!config.options?.params, }); } try { const response = await retryHandler.execute(async () => { const axiosConfig = await createAxiosConfig(baseConfig, config); if (baseConfig.sessionStorage && baseConfig.logger) { return executeRequestWithRefresh(axiosConfig, config, { client, sessionStorage: baseConfig.sessionStorage, logger: baseConfig.logger, maxRefreshRetries: 1, }); } else { return client.request(axiosConfig); } }, { url: config.url, method: config.method, requestId, logger: baseConfig.logger, }); let cookies = []; if (jar) { const cookieStr = await jar.getCookieString(config.url); cookies = cookieStr ? cookieStr.split(';').map((c) => c.trim()) : []; } if (baseConfig.logger) { baseConfig.logger.info('HTTP Request successful', { requestId, method: config.method, url: config.url, statusCode: response.status, statusText: response.statusText, hasData: !!response.data, cookieCount: cookies.length, }); } return { data: response.data, success: true, cookies, }; } catch (error) { if (baseConfig.logger) { baseConfig.logger.error('HTTP Request failed (final)', { requestId, method: config.method, url: config.url, error: error instanceof Error ? error.message : String(error), errorCode: error && typeof error === 'object' && 'code' in error ? error.code : undefined, stack: error instanceof Error ? error.stack : undefined, }); } return { data: null, success: false, error: handleError(error, config, baseConfig.logger, requestId), }; } }; export const handleError = (error, requestConfig, logger, requestId) => { if (axios.isAxiosError(error)) { const response = error.response; const request = error.request; if (response) { const httpErrorResponse = { status: response.status, statusText: response.statusText, data: response.data, headers: response.headers, }; return createHttpError(httpErrorResponse, { url: requestConfig.url, method: requestConfig.method, requestData: requestConfig.data, requestId, }); } else if (request) { const httpErrorResponse = { status: 0, statusText: 'Network Error', data: { message: error.message }, }; return createHttpError(httpErrorResponse, { url: requestConfig.url, method: requestConfig.method, requestData: requestConfig.data, requestId, }); } } const httpErrorResponse = { status: 0, statusText: 'Unknown Error', data: { message: error instanceof Error ? error.message : 'Unknown error occurred', }, }; return createHttpError(httpErrorResponse, { url: requestConfig.url, method: requestConfig.method, requestData: requestConfig.data, requestId, }); }; export const createHttpClient = (config = {}) => { const { client, jar } = createAxiosInstance(config); const normalizedConfig = normalizeHttpClientConfig(config); return { get: (url, options) => makeRequest(client, jar, normalizedConfig, { url, method: 'GET', options, }), post: (url, data, options) => makeRequest(client, jar, normalizedConfig, { url, method: 'POST', data, options, }), put: (url, data, options) => makeRequest(client, jar, normalizedConfig, { url, method: 'PUT', data, options, }), patch: (url, data, options) => makeRequest(client, jar, normalizedConfig, { url, method: 'PATCH', data, options, }), delete: (url, options) => makeRequest(client, jar, normalizedConfig, { url, method: 'DELETE', options, }), }; };