diff --git a/packages/insomnia-api/src/client-defaults.ts b/packages/insomnia-api/src/client-defaults.ts new file mode 100644 index 000000000000..d9fddac21676 --- /dev/null +++ b/packages/insomnia-api/src/client-defaults.ts @@ -0,0 +1,31 @@ +import { platform } from 'insomnia-data/common'; + +import { version } from '../package.json'; + +interface ClientEnv { + PLAYWRIGHT_TEST: string | undefined; + INSOMNIA_ENV: string | undefined; + INSOMNIA_API_URL: string | undefined; +} + +// Renderer reads env from the preload (`window.env`); main process, UtilityProcess and the inso +// CLI have no `window` and fall back to `process.env`. +const env: ClientEnv = + typeof window !== 'undefined' && (window as unknown as { env?: ClientEnv }).env + ? (window as unknown as { env: ClientEnv }).env + : (process.env as unknown as ClientEnv); + +export const PLAYWRIGHT_TEST = env.PLAYWRIGHT_TEST; + +export const INSOMNIA_FETCH_TIME_OUT = 30_000; + +export const getApiBaseURL = () => env.INSOMNIA_API_URL || 'https://api.insomnia.rest'; + +const getAppEnvironment = () => env.INSOMNIA_ENV || process.env.INSOMNIA_ENV || 'production'; + +// All workspace packages in this monorepo are released with the same version number. +const getAppVersion = () => version; + +export const getClientString = () => `${getAppEnvironment()}::${platform}::${getAppVersion()}`; + +export const generateRequestId = (prefix: string) => `${prefix}_${crypto.randomUUID().replace(/-/g, '')}`; diff --git a/packages/insomnia-api/src/configure-v3-client-defaults.ts b/packages/insomnia-api/src/configure-v3-client-defaults.ts new file mode 100644 index 000000000000..9a24ceea17b9 --- /dev/null +++ b/packages/insomnia-api/src/configure-v3-client-defaults.ts @@ -0,0 +1,11 @@ +import { generateRequestId, getApiBaseURL, getClientString } from './client-defaults'; +import { proxyAwareFetch } from './insomnia-fetch'; +import { configureV3Client } from './spaces'; + +export const configureV3ClientDefaults = () => + configureV3Client({ + getBaseURL: getApiBaseURL, + getClientString, + generateRequestId: () => generateRequestId('desk'), + fetchApi: proxyAwareFetch, + }); diff --git a/packages/insomnia-api/src/index.ts b/packages/insomnia-api/src/index.ts index 8a1e6c177ba0..808e169ff742 100644 --- a/packages/insomnia-api/src/index.ts +++ b/packages/insomnia-api/src/index.ts @@ -11,3 +11,6 @@ export * from './mock'; export * from './vcs'; export { configureFetch, type FetchConfig, ResponseFailError, isApiError } from './fetch'; +export { getApiBaseURL, getClientString, INSOMNIA_FETCH_TIME_OUT, PLAYWRIGHT_TEST } from './client-defaults'; +export { insomniaFetch, proxyAwareFetch, setFetchImplementation } from './insomnia-fetch'; +export { configureV3ClientDefaults } from './configure-v3-client-defaults'; diff --git a/packages/insomnia-api/src/insomnia-fetch.ts b/packages/insomnia-api/src/insomnia-fetch.ts new file mode 100644 index 000000000000..89d6fa513a1a --- /dev/null +++ b/packages/insomnia-api/src/insomnia-fetch.ts @@ -0,0 +1,96 @@ +import { generateRequestId, getApiBaseURL, getClientString, INSOMNIA_FETCH_TIME_OUT, PLAYWRIGHT_TEST } from './client-defaults'; +import { type FetchConfig, ResponseFailError } from './fetch'; + +type FetchImplementation = (input: string, init?: RequestInit) => Promise; + +// node fetch ignores the system proxy and OS certs — main swaps in net.fetch (entry.main.ts) +let fetchImpl: FetchImplementation = (input, init) => globalThis.fetch(input, init); + +export function setFetchImplementation(impl: FetchImplementation) { + fetchImpl = impl; +} + +// Stable, proxy-aware fetch handle for callers that need a raw `fetch` (e.g. the v3 SDK's +// Configuration.fetchApi). It delegates to the current `fetchImpl` on every call rather than +// capturing it, so it works regardless of whether setFetchImplementation() has run yet. +export const proxyAwareFetch: typeof globalThis.fetch = (input, init) => + fetchImpl(input as string, init as RequestInit | undefined); + +// Adds headers, retries and opens deep links returned from the api +export async function insomniaFetch({ + method, + path, + data, + sessionId, + organizationId, + origin, + headers, + timeout = INSOMNIA_FETCH_TIME_OUT, + onDeepLink, +}: FetchConfig & { + // It's not used at all, should be removed? + retries?: number; + onDeepLink?: (uri: string) => void; +}): Promise { + const config: RequestInit = { + method, + headers: { + ...headers, + 'X-Insomnia-Client': getClientString(), + 'insomnia-request-id': generateRequestId('desk'), + 'X-Origin': origin || getApiBaseURL(), + ...(sessionId ? { 'X-Session-Id': sessionId } : {}), + ...(data ? { 'Content-Type': 'application/json' } : {}), + ...(organizationId ? { 'X-Insomnia-Org-Id': organizationId } : {}), + ...(PLAYWRIGHT_TEST ? { 'X-Mockbin-Test': 'true' } : {}), + }, + ...(data ? { body: JSON.stringify(data) } : {}), + signal: AbortSignal.timeout(timeout), + }; + if (sessionId === undefined) { + throw new Error(`No session ID provided to ${method}:${path}`); + } + + try { + const response = await fetchImpl((origin || getApiBaseURL()) + path, config); + const uri = response.headers.get('x-insomnia-command'); + if (uri && onDeepLink) { + onDeepLink(uri); + } + const isJson = response.headers.get('content-type')?.includes('application/json') || path.match(/\.json$/); + if (!response.ok) { + let errName = `CODE-${response.status}`; + let errMsg = response.statusText; + if (isJson) { + try { + const json = await response.json(); + if (typeof json?.error === 'string') { + errName = json.error; + } + if (typeof json?.message === 'string') { + errMsg = json.message; + } + } catch {} + } + throw new ResponseFailError(errName, errMsg, response); + } + return isJson ? response.json() : (response.text() as Promise); + } catch (err) { + if (!(err instanceof Error)) { + throw err; + } + // AbortSignal.timeout() gives TimeoutError, not AbortError + if (err.name === 'AbortError' || err.name === 'TimeoutError') { + throw new Error(`insomniaFetch timed out: ${method} ${path}`, { cause: err }); + } + // the real error (ECONNREFUSED, cert problems) hides in err.cause, sometimes nested in an AggregateError + const cause = (err as { cause?: string | { code?: string; message?: string; errors?: { code?: string }[] } }) + .cause; + const detail = typeof cause === 'string' ? cause : cause?.code || cause?.errors?.[0]?.code || cause?.message; + if (detail) { + // fresh Error (don't mutate err.message) so a re-observed/retried error doesn't append the detail twice + throw new Error(`${err.message} (${detail})`, { cause: err }); + } + throw err; + } +} diff --git a/packages/insomnia-inso/src/cli.ts b/packages/insomnia-inso/src/cli.ts index ee6be82bca6a..5254d19d3e88 100644 --- a/packages/insomnia-inso/src/cli.ts +++ b/packages/insomnia-inso/src/cli.ts @@ -8,13 +8,11 @@ import { cosmiconfig } from 'cosmiconfig'; // @ts-expect-error the enquirer types are incomplete https://github.com/enquirer/enquirer/pull/307 import { Confirm } from 'enquirer'; import { pick } from 'es-toolkit'; -import { configureV3ClientDefaults } from 'insomnia/src/common/configure-v3-client'; import { isDevelopment, JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from 'insomnia/src/common/constants'; -import { insomniaFetch } from 'insomnia/src/common/insomnia-fetch'; import { getSendRequestCallbackMemDb } from 'insomnia/src/network/send-request.node'; import { initRuntime } from 'insomnia/src/runtimes'; import { nodeRuntime } from 'insomnia/src/runtimes/runtime.node'; -import { configureFetch } from 'insomnia-api'; +import { configureFetch, configureV3ClientDefaults, insomniaFetch } from 'insomnia-api'; import type { BaseModel, Environment, diff --git a/packages/insomnia/src/common/configure-v3-client.ts b/packages/insomnia/src/common/configure-v3-client.ts index 63fa3416baf7..e0872e33467a 100644 --- a/packages/insomnia/src/common/configure-v3-client.ts +++ b/packages/insomnia/src/common/configure-v3-client.ts @@ -1,21 +1 @@ -import { configureV3Client } from 'insomnia-api'; - -import { getApiBaseURL, getClientString } from './constants'; -import { proxyAwareFetch } from './insomnia-fetch'; -import { generateId } from './misc'; - -/** - * Wires the v3 API client with the standard desktop/CLI configuration. The inputs are identical - * across every entrypoint (renderer, main, CLI), so this lives in one place to keep the request-id - * prefix and base-URL wiring from drifting between them. Unlike `configureFetch`, which injects - * entrypoint-specific behaviour (e.g. deep-link handling) and is wired inline per entrypoint. - */ -export const configureV3ClientDefaults = () => - configureV3Client({ - getBaseURL: getApiBaseURL, - getClientString, - generateRequestId: () => generateId('desk'), - // Mirrors `configureFetch` above: route SDK requests through the same proxy-aware fetch - // (net.fetch in main) so system proxy settings work like they did before the v3 migration. - fetchApi: proxyAwareFetch, - }); +export { configureV3ClientDefaults } from 'insomnia-api'; diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts index 3bede5e30423..3d3bfbb57d04 100644 --- a/packages/insomnia/src/common/constants.ts +++ b/packages/insomnia/src/common/constants.ts @@ -7,12 +7,13 @@ import { isMac, isWindows, METHOD_GET, - platform, } from 'insomnia-data/common'; import appConfig from '../../config/config.json'; import { version } from '../../package.json'; +export { getApiBaseURL, getClientString, INSOMNIA_FETCH_TIME_OUT, PLAYWRIGHT_TEST } from 'insomnia-api'; + // In the renderer (nodeIntegration disabled) env vars come from the preload via window.env. // In the inso CLI and main process, fall back to process.env. const ENV = 'env'; @@ -23,7 +24,6 @@ const env = typeof window !== 'undefined' && window.env ? window.env : process[E export const INSOMNIA_GITLAB_REDIRECT_URI = env.INSOMNIA_GITLAB_REDIRECT_URI; export const INSOMNIA_GITLAB_CLIENT_ID = env.INSOMNIA_GITLAB_CLIENT_ID; export const INSOMNIA_GITLAB_API_URL = env.INSOMNIA_GITLAB_API_URL; -export const PLAYWRIGHT_TEST = env.PLAYWRIGHT_TEST; export const OAUTH_WINDOW_SESSION_ID_KEY = 'current-oauth-session-id'; // App Stuff @@ -69,8 +69,6 @@ export function updatesSupported() { export type UpdateStatus = 'idle' | 'checking' | 'downloading' | 'readyToRestart'; -export const getClientString = () => `${getAppEnvironment()}::${platform}::${getAppVersion()}`; - // Global Stuff export const DEBOUNCE_MILLIS = 100; @@ -104,7 +102,6 @@ export const getOauthRedirectUrl = () => env.OAUTH_REDIRECT_URL || 'https://app. export const getOauthRelayUrl = () => env.OAUTH_RELAY_URL || 'https://app.insomnia.rest/oauth/relay'; // API -export const getApiBaseURL = () => env.INSOMNIA_API_URL || 'https://api.insomnia.rest'; export const getMockServiceURL = () => env.INSOMNIA_MOCK_API_URL || 'https://mock.insomnia.run'; export const getMockServiceBinURL = (mockServer: MockServer, path: string) => { @@ -540,9 +537,6 @@ export const RESPONSE_CODE_REASONS: Record = { 599: 'Network Connect Timeout Error', }; -// (ms) curently server timeout is 30s -export const INSOMNIA_FETCH_TIME_OUT = 30_000; - // channel names for real time events (websocket/socket-io/mcp) export const REALTIME_EVENTS_CHANNELS = { READY_STATE: 'readyState', diff --git a/packages/insomnia/src/common/insomnia-fetch.ts b/packages/insomnia/src/common/insomnia-fetch.ts index ef5ce9f3f286..4cbcbc65691c 100644 --- a/packages/insomnia/src/common/insomnia-fetch.ts +++ b/packages/insomnia/src/common/insomnia-fetch.ts @@ -1,98 +1 @@ -import { type FetchConfig, ResponseFailError } from 'insomnia-api'; - -import { getApiBaseURL, getClientString, INSOMNIA_FETCH_TIME_OUT, PLAYWRIGHT_TEST } from './constants'; -import { generateId } from './misc'; - -type FetchImplementation = (input: string, init?: RequestInit) => Promise; - -// node fetch ignores the system proxy and OS certs — main swaps in net.fetch (entry.main.ts) -let fetchImpl: FetchImplementation = (input, init) => globalThis.fetch(input, init); - -export function setFetchImplementation(impl: FetchImplementation) { - fetchImpl = impl; -} - -// Stable, proxy-aware fetch handle for callers that need a raw `fetch` (e.g. the v3 SDK's -// Configuration.fetchApi). It delegates to the current `fetchImpl` on every call rather than -// capturing it, so it works regardless of whether setFetchImplementation() has run yet. -export const proxyAwareFetch: typeof globalThis.fetch = (input, init) => - fetchImpl(input as string, init as RequestInit | undefined); - -// Adds headers, retries and opens deep links returned from the api -export async function insomniaFetch({ - method, - path, - data, - sessionId, - organizationId, - origin, - headers, - timeout = INSOMNIA_FETCH_TIME_OUT, - onDeepLink, -}: FetchConfig & { - // It's not used at all, should be removed? - retries?: number; - onDeepLink?: (uri: string) => void; -}): Promise { - const config: RequestInit = { - method, - headers: { - ...headers, - 'X-Insomnia-Client': getClientString(), - 'insomnia-request-id': generateId('desk'), - 'X-Origin': origin || getApiBaseURL(), - ...(sessionId ? { 'X-Session-Id': sessionId } : {}), - ...(data ? { 'Content-Type': 'application/json' } : {}), - ...(organizationId ? { 'X-Insomnia-Org-Id': organizationId } : {}), - ...(PLAYWRIGHT_TEST ? { 'X-Mockbin-Test': 'true' } : {}), - }, - ...(data ? { body: JSON.stringify(data) } : {}), - signal: AbortSignal.timeout(timeout), - }; - if (sessionId === undefined) { - throw new Error(`No session ID provided to ${method}:${path}`); - } - - try { - const response = await fetchImpl((origin || getApiBaseURL()) + path, config); - const uri = response.headers.get('x-insomnia-command'); - if (uri && onDeepLink) { - onDeepLink(uri); - } - const isJson = response.headers.get('content-type')?.includes('application/json') || path.match(/\.json$/); - if (!response.ok) { - let errName = `CODE-${response.status}`; - let errMsg = response.statusText; - if (isJson) { - try { - const json = await response.json(); - if (typeof json?.error === 'string') { - errName = json.error; - } - if (typeof json?.message === 'string') { - errMsg = json.message; - } - } catch {} - } - throw new ResponseFailError(errName, errMsg, response); - } - return isJson ? response.json() : (response.text() as Promise); - } catch (err) { - if (!(err instanceof Error)) { - throw err; - } - // AbortSignal.timeout() gives TimeoutError, not AbortError - if (err.name === 'AbortError' || err.name === 'TimeoutError') { - throw new Error(`insomniaFetch timed out: ${method} ${path}`, { cause: err }); - } - // the real error (ECONNREFUSED, cert problems) hides in err.cause, sometimes nested in an AggregateError - const cause = (err as { cause?: string | { code?: string; message?: string; errors?: { code?: string }[] } }) - .cause; - const detail = typeof cause === 'string' ? cause : cause?.code || cause?.errors?.[0]?.code || cause?.message; - if (detail) { - // fresh Error (don't mutate err.message) so a re-observed/retried error doesn't append the detail twice - throw new Error(`${err.message} (${detail})`, { cause: err }); - } - throw err; - } -} +export { insomniaFetch, proxyAwareFetch, setFetchImplementation } from 'insomnia-api';