Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/insomnia-api/src/client-defaults.ts
Original file line number Diff line number Diff line change
@@ -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, '')}`;
11 changes: 11 additions & 0 deletions packages/insomnia-api/src/configure-v3-client-defaults.ts
Original file line number Diff line number Diff line change
@@ -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,
});
3 changes: 3 additions & 0 deletions packages/insomnia-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
96 changes: 96 additions & 0 deletions packages/insomnia-api/src/insomnia-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { generateRequestId, getApiBaseURL, getClientString, INSOMNIA_FETCH_TIME_OUT, PLAYWRIGHT_TEST } from './client-defaults';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should move insomnia-fetch into insomnia-api since fetch is runtime-related and insomnia-api is runtime-agnostic. What do you think?

import { type FetchConfig, ResponseFailError } from './fetch';

type FetchImplementation = (input: string, init?: RequestInit) => Promise<Response>;

// 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<T = void>({
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<T> {
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<T>);
} 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;
}
}
4 changes: 1 addition & 3 deletions packages/insomnia-inso/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 1 addition & 21 deletions packages/insomnia/src/common/configure-v3-client.ts
Original file line number Diff line number Diff line change
@@ -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';
10 changes: 2 additions & 8 deletions packages/insomnia/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -540,9 +537,6 @@ export const RESPONSE_CODE_REASONS: Record<number, string> = {
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',
Expand Down
99 changes: 1 addition & 98 deletions packages/insomnia/src/common/insomnia-fetch.ts
Original file line number Diff line number Diff line change
@@ -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<Response>;

// 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<T = void>({
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<T> {
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<T>);
} 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';
Loading