refactor(runtime): introduce explicit network runtime capabilities (#10037)

* refactor(runtime): introduce explicit network runtime capabilities

Add a RuntimeCapabilities boundary for network operations and initialize
renderer/node implementations from app, plugin, hidden-window, CLI, and test
entrypoints.

Migrate network request execution and HAR export paths to consume
getRuntime().network instead of importing network adapters directly.

* refactor: remove unused network adapter alias from Vitest and Vite config
This commit is contained in:
Bingbing
2026-06-09 02:38:54 +08:00
committed by GitHub
parent 767260931d
commit 002ae2f9f3
19 changed files with 129 additions and 61 deletions

View File

@@ -10,6 +10,8 @@ import { Confirm } from 'enquirer';
import { pick } from 'es-toolkit';
import { isDevelopment, JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from 'insomnia/src/common/constants';
import { insomniaFetch } from 'insomnia/src/common/insomnia-fetch';
import { initRuntime } from 'insomnia/src/common/runtime';
import { nodeRuntime } from 'insomnia/src/common/runtime/runtime.node';
import { getSendRequestCallbackMemDb } from 'insomnia/src/common/send-request';
import { configureFetch } from 'insomnia-api';
import type {
@@ -50,6 +52,7 @@ import { generateDocumentation } from './scripts/docs';
import { getAppDataDir, getDefaultProductName } from './util';
initServices(servicesNodeImpl);
initRuntime(nodeRuntime);
export interface GlobalOptions {
ci: boolean;

View File

@@ -4,7 +4,6 @@ export default defineConfig({
test: {
hideSkippedTests: true,
alias: {
'~/network/network-adapter': new URL('../insomnia/src/network/network-adapter.node.ts', import.meta.url).pathname,
'~/templating/render-adapter': new URL('../insomnia/src/templating/render-adapter.node.ts', import.meta.url)
.pathname,
'~/': new URL('../insomnia/src/', import.meta.url).pathname,

View File

@@ -6,10 +6,13 @@ import { vi } from 'vitest';
import { v4Mock } from '../insomnia-data/__mocks__/uuid';
import { nodeLibcurlMock } from './src/__mocks__/@getinsomnia/node-libcurl';
import { electronMock } from './src/__mocks__/electron';
import { initRuntime } from './src/common/runtime';
import { nodeRuntime } from './src/common/runtime/runtime.node';
import { mainDatabase } from './src/main/database.main';
await initDatabase(mainDatabase, { inMemoryOnly: true }, true);
await initServices(servicesNodeImpl);
initRuntime(nodeRuntime);
vi.mock('electron', () => ({ default: electronMock }));

View File

@@ -2,15 +2,19 @@ import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('~/network/network-adapter', () => ({
getTimelinePath: () => Promise.resolve(''),
appendToTimelineOnError: () => Promise.resolve(),
appendTimelineLines: () => Promise.resolve(),
getAuthHeader: () => Promise.resolve(),
executeCurlRequest: () => Promise.resolve({}),
runScript: () => Promise.resolve({}),
applyRequestHooks: (request: any) => Promise.resolve(request),
applyResponseHooks: (response: any) => Promise.resolve(response),
vi.mock('~/common/runtime', () => ({
getRuntime: () => ({
network: {
getTimelinePath: () => Promise.resolve(''),
appendToTimelineOnError: () => Promise.resolve(),
appendTimelineLines: () => Promise.resolve(),
getAuthHeader: () => Promise.resolve(),
executeCurlRequest: () => Promise.resolve({}),
runScript: () => Promise.resolve({}),
applyRequestHooks: (request: any) => Promise.resolve(request),
applyResponseHooks: (response: any) => Promise.resolve(response),
},
}),
}));
vi.mock('~/utils/crypt-adapter', () => ({
decryptSecretValue: (value: any) => value,

View File

@@ -0,0 +1,19 @@
import type { RuntimeCapabilities } from './types';
let runtime: RuntimeCapabilities | null = null;
export function initRuntime(impl: RuntimeCapabilities) {
if (runtime) {
throw new Error('Runtime has already been initialized.');
}
runtime = impl;
}
export function getRuntime(): RuntimeCapabilities {
if (!runtime) {
throw new Error('Runtime not initialized. Call initRuntime() first.');
}
return runtime;
}
export type { NetworkRuntime, RuntimeCapabilities } from './types';

View File

@@ -0,0 +1,6 @@
import * as networkAdapter from '../../network/network-adapter.node';
import type { RuntimeCapabilities } from './types';
export const nodeRuntime = {
network: networkAdapter,
} satisfies RuntimeCapabilities;

View File

@@ -0,0 +1,6 @@
import * as networkAdapter from '../../network/network-adapter.renderer';
import type { RuntimeCapabilities } from './types';
export const rendererRuntime = {
network: networkAdapter,
} satisfies RuntimeCapabilities;

View File

@@ -0,0 +1,38 @@
import type { Cookie, RequestHeader } from 'insomnia-data';
import type { RequestContext } from '../../../../insomnia-scripting-environment/src/objects';
import type { CurlRequestOptions, CurlRequestOutput, ResponsePatch } from '../../main/network/libcurl-promise';
import type { RenderedRequest } from '../../templating/types';
interface CurlRequestErrorOutput {
statusMessage: string;
error: string;
}
export interface NetworkRuntime {
getTimelinePath: (responseId: string) => Promise<string>;
appendToTimelineOnError: (timelinePath: string, data: string) => Promise<void>;
appendTimelineLines: (timelinePath: string, logs: string[]) => Promise<void>;
getAuthHeader: (request: RenderedRequest, url: string) => Promise<RequestHeader | undefined>;
executeCurlRequest: (options: CurlRequestOptions) => Promise<CurlRequestOutput | CurlRequestErrorOutput>;
extractCookies: (options: {
setCookieStrings: string[];
currentUrl: string;
cookieJar: { cookies: Cookie[] };
settingStoreCookies: boolean;
}) => Promise<{ cookies: Cookie[]; rejectedCookies: string[]; totalSetCookies: number }>;
runScript: (options: { script: string; context: RequestContext }) => Promise<RequestContext | { error: string }>;
applyRequestHooks: (
renderedRequest: RenderedRequest,
renderedContext: Record<string, any>,
) => Promise<RenderedRequest>;
applyResponseHooks: (
response: ResponsePatch,
renderedRequest: RenderedRequest,
renderedContext: Record<string, any>,
) => Promise<ResponsePatch>;
}
export interface RuntimeCapabilities {
network: NetworkRuntime;
}

View File

@@ -8,6 +8,8 @@ import { hydrateRoot } from 'react-dom/client';
import { HydratedRouter } from 'react-router/dom';
import { insomniaFetch } from '~/common/insomnia-fetch';
import { initRuntime } from '~/common/runtime';
import { rendererRuntime } from '~/common/runtime/runtime.renderer';
import { database as clientDatabase } from '~/ui/database.client';
import { clearOAuthWindowSessionId } from '~/ui/spawn-oauth-window';
@@ -36,6 +38,7 @@ if (!window._dataServices) {
initServices(window._dataServices);
// Remove the global services reference after initialization to improve security by preventing unintended access from the global scope.
delete window._dataServices;
initRuntime(rendererRuntime);
configureFetch(options => insomniaFetch({ ...options, onDeepLink: (uri: string) => window.main.openDeepLink(uri) }));

View File

@@ -3,6 +3,8 @@ import { SENTRY_OPTIONS } from 'insomnia/src/common/sentry';
import { initServices } from 'insomnia-data';
import type { RequestContext } from '../../insomnia-scripting-environment/src/objects';
import { initRuntime } from './common/runtime';
import { rendererRuntime } from './common/runtime/runtime.renderer';
import { runScript } from './scripting/run-script';
import { type ScriptSecurityPolicy } from './scripting/sandbox';
@@ -27,6 +29,7 @@ if (!window._dataServices) {
initServices(window._dataServices);
// Remove the global services reference after initialization to improve security by preventing unintended access from the global scope.
delete window._dataServices;
initRuntime(rendererRuntime);
window.bridge.onmessage(
async (data: { script: string; context: RequestContext }, callback: ({ error }: { error: string }) => void) => {

View File

@@ -13,6 +13,8 @@ import { isMac } from 'insomnia-data/common';
import { servicesNodeImpl } from 'insomnia-data/node';
import { insomniaFetch } from '~/common/insomnia-fetch';
import { initRuntime } from '~/common/runtime';
import { nodeRuntime } from '~/common/runtime/runtime.node';
import { mainDatabase } from '~/main/database.main';
import { initElectronStorage } from '~/main/electron-storage';
import { runGitCredentialsMigration } from '~/main/git/migrations';
@@ -127,6 +129,7 @@ app.on('ready', async () => {
await initDatabase(mainDatabase);
// Initialize services for main process
initServices(servicesNodeImpl);
initRuntime(nodeRuntime);
await _createModelInstances();
// backup needs the channel from settings which needs the database
await backupIfNewerVersionAvailable();

View File

@@ -1,6 +1,8 @@
import { ipcRenderer } from 'electron';
import { initDatabase, initServices } from 'insomnia-data';
import { initRuntime } from './common/runtime';
import { rendererRuntime } from './common/runtime/runtime.renderer';
import { pluginWindowDatabase } from './main/database.plugin-window';
import { invokePluginMethod } from './plugins/invoke-method';
import { servicesProxy } from './ui/renderer-services-proxy';
@@ -28,6 +30,7 @@ ipcRenderer.on('plugins.invoke', async (_event, { id, method, args }: PluginInvo
try {
await initDatabase(pluginWindowDatabase);
initServices(servicesProxy);
initRuntime(rendererRuntime);
ipcRenderer.send('plugins.windowReady');
} catch (err) {
console.error('[plugin-window] Initialization failed:', err);

View File

@@ -3,9 +3,9 @@ import type { BaseModel, Cookie, Environment, Request, RequestGroup, Response, W
import { models, services } from 'insomnia-data';
import { Cookie as ToughCookie } from 'tough-cookie';
import { getRuntime } from '~/common/runtime';
import { getAuthHeader } from '~/main/network/get-auth-header';
import { secureReadFile } from '~/main/secure-read-file';
import { applyRequestHooks } from '~/network/network-adapter';
import { getAppVersion } from '../common/constants';
import { database } from '../common/database';
@@ -245,7 +245,7 @@ export async function exportHarRequest(requestId: string, environmentOrWorkspace
export async function exportHarWithRequest(request: Request, environmentId?: string, addContentLength = false) {
try {
const renderResult = await getRenderedRequestAndContext({ request, environment: environmentId });
const renderedRequest = await applyRequestHooks(renderResult.request, renderResult.context);
const renderedRequest = await getRuntime().network.applyRequestHooks(renderResult.request, renderResult.context);
parseGraphQLReqeustBody(renderedRequest);
return exportHarWithRenderedRequest(renderedRequest, addContentLength);
} catch (err) {

View File

@@ -42,20 +42,6 @@ describe('getAuthQueryParams', () => {
});
});
describe('sendCurlAndWriteTimeline()', () => {
beforeEach(() => {
vi.stubGlobal('window', {
main: {
timeline: {
getPath: (responseId: string) => Promise.resolve(`/tmp/${responseId}.timeline`),
appendToFile: vi.fn().mockResolvedValue(null),
},
getAuthHeader,
curlRequest,
cancelCurlRequest: vi.fn(),
},
});
});
it('sends a generic request', async () => {
const workspace = await services.workspace.create();
const settings = await services.settings.getOrCreate();

View File

@@ -77,7 +77,7 @@ export async function applyRequestHooks(
renderedContext: Record<string, any>,
): Promise<RenderedRequest> {
const newRenderedRequest = applyDefaultHeaders(renderedRequest, renderedContext['DEFAULT_HEADERS']);
const pluginIndex = require('../plugins/index');
const pluginIndex = await import('../plugins/index');
for (const { plugin, hook } of await pluginIndex.getRequestHooks()) {
const context = {
...(pluginApp.init() as Record<string, any>),
@@ -104,7 +104,7 @@ export async function applyResponseHooks(
): Promise<ResponsePatch> {
const newResponse = clone(response);
const newRequest = clone(renderedRequest);
const pluginIndex = require('../plugins/index');
const pluginIndex = await import('../plugins/index');
for (const { plugin, hook } of await pluginIndex.getResponseHooks()) {
const context = {
...(pluginApp.init() as Record<string, any>),

View File

@@ -24,17 +24,7 @@ import { EnvironmentType, models, services } from 'insomnia-data';
import { invariant, serializeNDJSON } from 'insomnia-data/common';
import orderedJSON from 'json-order';
import {
appendTimelineLines,
appendToTimelineOnError,
applyRequestHooks,
applyResponseHooks,
executeCurlRequest,
extractCookies,
getAuthHeader,
getTimelinePath,
runScript,
} from '~/network/network-adapter';
import { getRuntime } from '~/common/runtime';
import { getKVPairFromData } from '~/utils/environment-utils';
import type { ExecutionOption, RequestContext } from '../../../insomnia-scripting-environment/src/objects';
@@ -150,7 +140,7 @@ export const fetchRequestGroupData = async (requestGroupId: string) => {
const clientCertificates = await services.clientCertificate.findByParentId(workspaceId);
const caCert = await services.caCertificate.getByParentId(workspaceId);
const responseId = generateId('res');
const timelinePath = await getTimelinePath(responseId);
const timelinePath = await getRuntime().network.getTimelinePath(responseId);
return { environment, settings, clientCertificates, caCert, activeEnvironmentId, timelinePath, responseId };
};
@@ -213,7 +203,7 @@ export const fetchRequestData = async (
const caCert = await services.caCertificate.getByParentId(workspaceId);
const responseId = generateId('res');
const timelinePath = await getTimelinePath(responseId);
const timelinePath = await getRuntime().network.getTimelinePath(responseId);
return {
request,
@@ -252,7 +242,7 @@ export const fetchMcpRequestData = async (mcpRequestId: string) => {
invariant(settings, 'failed to create settings');
const responseId = generateId('res');
const timelinePath = await getTimelinePath(responseId);
const timelinePath = await getRuntime().network.getTimelinePath(responseId);
return {
environment,
@@ -514,7 +504,7 @@ const tryToExecuteScript = async (context: RequestAndContextAndOptionalResponse)
}
try {
const output = await runScript({
const output = await getRuntime().network.runScript({
script,
context: {
request,
@@ -647,7 +637,7 @@ const tryToExecuteScript = async (context: RequestAndContextAndOptionalResponse)
parentFolders: output.parentFolders,
};
} catch (err) {
await appendToTimelineOnError(
await getRuntime().network.appendToTimelineOnError(
timelinePath,
serializeNDJSON([{ value: err.message, name: 'Text', timestamp: Date.now() }]),
);
@@ -798,7 +788,7 @@ export const tryToTransformRequestWithPlugins = async (renderResult: {
}) => {
const { request, context } = renderResult;
try {
return await applyRequestHooks(request, context);
return await getRuntime().network.applyRequestHooks(request, context);
} catch {
throw new Error(`Failed to transform request with plugins: ${request._id}`);
}
@@ -858,7 +848,7 @@ export async function sendCurlAndWriteTimeline(
if (!renderedRequest.settingSendCookies) {
timeline.push({ value: 'Disable cookie sending due to user setting', name: 'Text', timestamp: Date.now() });
}
const authHeader = await getAuthHeader(renderedRequest, finalUrl);
const authHeader = await getRuntime().network.getAuthHeader(renderedRequest, finalUrl);
const requestOptions = {
requestId,
req: renderedRequest,
@@ -870,7 +860,7 @@ export async function sendCurlAndWriteTimeline(
authHeader,
};
const output = await executeCurlRequest(requestOptions);
const output = await getRuntime().network.executeCurlRequest(requestOptions);
if ('error' in output) {
if (runtime) {
@@ -893,7 +883,7 @@ export async function sendCurlAndWriteTimeline(
// todo: move to main process
debugTimeline.forEach(entry => timeline.push(entry));
// transform output
const { cookies, rejectedCookies, totalSetCookies } = await extractCookies({
const { cookies, rejectedCookies, totalSetCookies } = await getRuntime().network.extractCookies({
setCookieStrings: headerResults.flatMap(({ headers }: any) => getSetCookiesFromResponseHeaders(headers)),
currentUrl: getCurrentUrl({ headerResults, finalUrl }),
cookieJar: renderedRequest.cookieJar,
@@ -951,7 +941,7 @@ export const responseTransform = async (
}
console.log(`[network] Response succeeded req=${patch.parentId} status=${response.statusCode || '?'}`);
try {
return await applyResponseHooks(response, renderedRequest, context);
return await getRuntime().network.applyResponseHooks(response, renderedRequest, context);
} catch (err) {
console.log('[plugin] Response hook failed', err, response);
return {
@@ -1025,5 +1015,5 @@ export const getCurrentUrl = ({ headerResults, finalUrl }: { headerResults: any;
};
export const defaultSendActionRuntime: SendActionRuntime = {
appendTimeline: appendTimelineLines,
appendTimeline: (timelinePath, logs) => getRuntime().network.appendTimelineLines(timelinePath, logs),
};

View File

@@ -2,15 +2,19 @@ import { exportRequestsHAR, exportWorkspacesHAR } from 'insomnia/src/main/har';
import { database as db, services } from 'insomnia-data';
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('~/network/network-adapter', () => ({
getTimelinePath: () => Promise.resolve(''),
appendToTimelineOnError: () => Promise.resolve(),
appendTimelineLines: () => Promise.resolve(),
getAuthHeader: () => Promise.resolve(),
executeCurlRequest: () => Promise.resolve({}),
runScript: () => Promise.resolve({}),
applyRequestHooks: (request: any) => Promise.resolve(request),
applyResponseHooks: (response: any) => Promise.resolve(response),
vi.mock('~/common/runtime', () => ({
getRuntime: () => ({
network: {
getTimelinePath: () => Promise.resolve(''),
appendToTimelineOnError: () => Promise.resolve(),
appendTimelineLines: () => Promise.resolve(),
getAuthHeader: () => Promise.resolve(),
executeCurlRequest: () => Promise.resolve({}),
runScript: () => Promise.resolve({}),
applyRequestHooks: (request: any) => Promise.resolve(request),
applyResponseHooks: (response: any) => Promise.resolve(response),
},
}),
}));
// @vitest-environment jsdom

View File

@@ -55,7 +55,6 @@ export default defineConfig(({ mode }) => {
alias: {
// Short-circuit the adapter wrappers to the renderer implementation directly.
// These must appear before the '~' catch-all so the specific path wins.
'~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'),
'~/templating/render-adapter': path.resolve(__dirname, './src/templating/render-adapter.renderer'),
'~/utils/crypt-adapter': path.resolve(__dirname, './src/utils/crypt-adapter.renderer'),
'~': path.resolve(__dirname, './src'),

View File

@@ -14,7 +14,6 @@ export default defineConfig({
},
exclude: ['src/routes/**.*.tsx', '.react-router', 'node_modules'],
alias: {
'~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'),
'~/templating/render-adapter': path.resolve(__dirname, './src/templating/render-adapter.node'),
'~': path.resolve(__dirname, './src'),
'electron/main': 'electron',