feat(inso): support request timeouts (#9363)

* feat: support request timeouts in inso

---------

Co-authored-by: Jack Kavanagh <jackkav@gmail.com>
This commit is contained in:
Ryan Willis
2025-11-18 10:50:19 -07:00
committed by GitHub
parent a2ad777837
commit d798be87dc
22 changed files with 196 additions and 144 deletions

View File

@@ -43,6 +43,7 @@ export default tseslint.config(
'unicorn/relative-url-style': 'error',
'unicorn/switch-case-braces': 'error',
'unicorn/throw-new-error': 'error',
'no-throw-literal': 'error',
// 'unicorn/custom-error-definition': 'error', //TODO: Enable this rule
// 'unicorn/expiring-todo-comments': 'error', //TODO: Enable this rule
// 'unicorn/explicit-length-check': 'error', //TODO: Enable this rule

View File

@@ -32,6 +32,7 @@
"scripts": {
"lint": "eslint . --ext .js,.ts,.tsx --cache",
"test:unit": "cross-env NO_COLOR=1 vitest run --exclude '**/cli.test.ts'",
"test:unit:color": "vitest run --exclude '**/cli.test.ts'",
"test:bundle": "vitest cli.test.ts -t \"inso dev bundle\"",
"test:binary": "vitest cli.test.ts -t \"inso packaged binary\"",
"type-check": "tsc --noEmit --project tsconfig.json",

View File

@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
import path from 'node:path';
import { beforeAll, describe, expect, it } from 'vitest';
// Tests both bundle and packaged versions of the CLI with the same commands and expectations.
// Intended to be coarse grained (only checks for success or failure) smoke test to ensure packaging worked as expected.
@@ -57,6 +58,8 @@ const shouldReturnSuccessCode = [
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/with-missing-env-vars.yml -i req_3fd28aabbb18447abab1f45e6ee4bdc1 --env-var firstkey=first --env-var secondkey=second wrk_c992d40',
// globals file path env overrides
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/with-missing-env-vars.yml -i req_3fd28aabbb18447abab1f45e6ee4bdc1 --globals packages/insomnia-inso/src/examples/global-environment.yml wrk_c992d40',
// with timeout success
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/timeout-test.yml -i req_two_seconds --requestTimeout 3000 wrk_timeout_test',
];
const shouldReturnErrorCode = [
@@ -68,6 +71,8 @@ const shouldReturnErrorCode = [
'$PWD/packages/insomnia-inso/bin/inso run test -w packages/insomnia-inso/src/db/fixtures/insomnia-v5/with-tests.yaml -e env_env_7c2769 uts_1c6207',
// after-response script and test
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/after-response-failed-test.yml wrk_616795 --verbose',
// with timeout failure
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/timeout-test.yml -i req_two_seconds --requestTimeout 1000 wrk_timeout_test',
];
beforeAll(async () => {
// ensure the test server is running

View File

@@ -24,7 +24,7 @@ import { v4 as uuidv4 } from 'uuid';
import type { Workspace } from '~/models/workspace';
import { type RequestTestResult } from '../../insomnia-scripting-environment/src/objects';
import type { RequestTestResult } from '../../insomnia-scripting-environment/src/objects';
import packageJson from '../package.json';
import { exportSpecification, writeFileWithCliOptions } from './commands/export-specification';
import { getRuleSetFileFromFolderByFilename, lintSpecification } from './commands/lint-specification';
@@ -402,6 +402,7 @@ export const go = (args?: string[]) => {
.option('-r, --reporter <reporter>', `reporter to use, options are [${reporterTypes.join(', ')}]`, defaultReporter)
.option('-b, --bail', 'abort ("bail") after first test failure', false)
.option('--keepFile', 'do not delete the generated test file', false)
.option('--requestTimeout <duration>', 'milliseconds before request times out', undefined) // defaults to user settings
.option('-k, --disableCertValidation', 'disable certificate validation for requests with SSL', false)
.option('--httpsProxy <proxy>', 'URL for the proxy server for https requests.', proxySettings.httpsProxy)
.option('--httpProxy <proxy>', 'URL for the proxy server for http requests.', proxySettings.httpProxy)
@@ -430,6 +431,7 @@ export const go = (args?: string[]) => {
httpProxy?: string;
noProxy?: string;
dataFolders: string[];
requestTimeout?: string;
},
) => {
const options = await mergeOptionsAndInit(cmd);
@@ -494,6 +496,7 @@ export const go = (args?: string[]) => {
validateSSL: !options.disableCertValidation,
...proxyOptions,
dataFolders: options.dataFolders,
...(options.requestTimeout ? { timeout: parseInt(options.requestTimeout, 10) } : {}),
});
// Generate test file
const testFileContents = generate(
@@ -532,6 +535,7 @@ export const go = (args?: string[]) => {
.option('-e, --env <identifier>', 'environment to use', '')
.option('-g, --globals <identifier>', 'global environment to use (filepath or id)', '')
.option('--delay-request <duration>', 'milliseconds to delay between requests', '0')
.option('--requestTimeout <duration>', 'milliseconds before request times out', undefined) // defaults to user settings
.option('--env-var <key=value>', 'override environment variables', collect, [])
.option('-n, --iteration-count <count>', 'number of times to repeat', '1')
.option('-d, --iteration-data <path/url>', 'file path or url (JSON or CSV)', '')
@@ -584,6 +588,7 @@ export const go = (args?: string[]) => {
output?: string;
includeFullData?: 'redact' | 'plaintext';
acceptRisk: boolean;
requestTimeout?: string;
},
) => {
const options = await mergeOptionsAndInit(cmd);
@@ -824,7 +829,12 @@ export const go = (args?: string[]) => {
environment._id,
db,
transientVariables,
{ validateSSL: !options.disableCertValidation, ...proxyOptions, dataFolders: options.dataFolders },
{
validateSSL: !options.disableCertValidation,
...proxyOptions,
dataFolders: options.dataFolders,
...(options.requestTimeout ? { timeout: parseInt(options.requestTimeout, 10) } : {}),
},
iterationData,
iterationCount,
);
@@ -926,8 +936,8 @@ export const go = (args?: string[]) => {
isIdentifierAFile = identifier && (await fs.promises.stat(identifierAsAbsPath)).isFile();
} catch (err) {}
const pathToSearch = '';
let specContent;
let rulesetFileName;
let specContent: string | undefined;
let rulesetFileName: string | undefined;
if (isIdentifierAFile) {
// try load as a file
logger.trace(`Linting specification file from identifier: \`${identifierAsAbsPath}\``);

View File

@@ -1 +1 @@
{"_id":"crt_c8325243678d49e19d7282f4c0f156d8","type":"CaCertificate","parentId":"wrk_0b96eff84c1c4eaa9c6e67ad74bbc85b","modified":1729664881251,"created":1729664881251,"disabled":false,"path":"packages/insomnia-inso/src/db/fixtures/certs/fake_ca.pem","isPrivate":false}
{"_id":"crt_c8325243678d49e19d7282f4c0f156d8","type":"CaCertificate","parentId":"wrk_0b96eff84c1c4eaa9c6e67ad74bbc85b","modified":1729664881251,"created":1729664881251,"disabled":false,"path":"packages/insomnia-smoke-test/fixtures/certificates/rootCA.pem","isPrivate":false}

View File

@@ -1 +1 @@
{"_id":"crt_998cf6641ec249389690c821fb16a07b","type":"ClientCertificate","parentId":"wrk_0b96eff84c1c4eaa9c6e67ad74bbc85b","modified":1727072507372,"created":1727072507372,"host":"insomnia.rest","passphrase":"","disabled":false,"cert":"packages/insomnia-inso/src/db/fixtures/certs/fake_cert.pem","key":"packages/insomnia-inso/src/db/fixtures/certs/fake_key.pem","pfx":null,"isPrivate":false}
{"_id":"crt_998cf6641ec249389690c821fb16a07b","type":"ClientCertificate","parentId":"wrk_0b96eff84c1c4eaa9c6e67ad74bbc85b","modified":1727072507372,"created":1727072507372,"host":"localhost:4011","passphrase":"","disabled":false,"cert":"packages/insomnia-smoke-test/fixtures/certificates/client.crt","key":"packages/insomnia-smoke-test/fixtures/certificates/client.key","pfx":null,"isPrivate":false}

View File

@@ -2,5 +2,5 @@
{"_id":"req_d50632f88775493485008430eb603237","type":"Request","parentId":"wrk_0b96eff84c1c4eaa9c6e67ad74bbc85b","modified":1748937359916,"created":1748932973049,"url":"http://localhost:4010/echo","name":"Request B","description":"","method":"POST","body":{"mimeType":"text/plain","text":"{% response 'body', 'req_78933d6e08d642d191577df68979e7e4', 'b64::JC5oZWFkZXJzLmhvc3Q=::46b', 'always', 60 %}"},"parameters":[{"id":"pair_8eadf0929d324c168c70ebbb6fa36ace","name":"","value":"","description":"","disabled":false}],"headers":[{"name":"Content-Type","value":"text/plain","id":"pair_4c18bb0ced1b4b49babd2190c509959c"},{"id":"pair_1cb7018af60042b897aab030f5de8b96","name":"client-id","value":"{{ _['client-id'] }}","description":"","disabled":false}],"authentication":{},"metaSortKey":-1748760800983.5,"isPrivate":false,"pathParameters":[],"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"}
{"_id":"req_wrk_012d4860c7da418a85ffea7406e1292a21946b60","type":"Request","parentId":"fld_wrk_012d4860c7da418a85ffea7406e1292a30baa249","modified":1593669699112,"created":1593669324881,"url":"{{ base_url }}/global","name":"/global","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1593669324881,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"}
{"_id":"req_wrk_012d4860c7da418a85ffea7406e1292ab410454b","type":"Request","parentId":"wrk_012d4860c7da418a85ffea7406e1292a","modified":1593669699110,"created":1593669324879,"url":"{{ base_url }}/override","name":"/override","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1593669324879,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"}
{"_id":"req_wrk_012d4860c7da418a85ffea7406e1292ab410454c","type":"Request","parentId":"wrk_0b96eff84c1c4eaa9c6e67ad74bbc85b","modified":1593669699110,"created":1593669324879,"url":"https://insomnia.rest","name":"withCertAndCA","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1593669324879,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"}
{"_id":"req_wrk_012d4860c7da418a85ffea7406e1292ab410454c","type":"Request","parentId":"wrk_0b96eff84c1c4eaa9c6e67ad74bbc85b","modified":1593669699110,"created":1593669324879,"url":"https://localhost:4011/protected/pets/2","name":"withCertAndCA","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1593669324879,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"}
{"_id":"req_wrk_012d4860c7da418a85ffea7406e1292ab410454d","type":"Request","parentId":"wrk_0b96eff84c1c4eaa9c6e67ad74bbc85b","modified":1593669699110,"created":1593669324879,"url":"httpbin.org/redirect-to?url=https%3A%2F%2Finsomnia.rest&status_code=302","name":"withSettings","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1593669324879,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"}

View File

@@ -0,0 +1,43 @@
_type: export
__export_format: 4
__export_date: 2021-01-21T00:00:00.000Z
__export_source: insomnia.desktop.app:v2024.1.0
resources:
- _id: req_two_seconds
parentId: wrk_timeout_test
modified: 1611212400000
created: 1611212400000
url: http://127.0.0.1:4010/delay/seconds/2
name: request that takes 2 seconds
description: ''
method: GET
body: {}
parameters: []
metaSortKey: -2
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: wrk_timeout_test
parentId: null
modified: 1611212400000
created: 1611212400000
name: Timeout Test Collection
description: ''
scope: collection
_type: workspace
- _id: env_timeout_base
parentId: wrk_timeout_test
modified: 1611212400000
created: 1611212400000
name: Base Environment
data: {}
dataPropertyOrder: null
color: null
isPrivate: false
metaSortKey: 1
_type: environment

View File

@@ -272,7 +272,7 @@ export class Variables {
* @returns The value of the variable if found, otherwise undefined
*/
get = (variableName: string) => {
let finalVal: boolean | number | string | object | undefined = undefined;
let finalVal: boolean | number | string | object | undefined;
[
this.localVars,
mergeFolderLevelVars(this.folderLevelVars),
@@ -387,13 +387,13 @@ export class Vault extends Environment {
// throw error on get or set method call if enableVaultInScripts is false
get: (target, prop, receiver) => {
if (!enableVaultInScripts) {
throw 'Vault is disabled in script';
throw new Error('Vault is disabled in script');
}
return Reflect.get(target, prop, receiver);
},
set: (target, prop, value, receiver) => {
if (!enableVaultInScripts) {
throw 'Vault is disabled in script';
throw new Error('Vault is disabled in script');
}
return Reflect.set(target, prop, value, receiver);
},
@@ -402,16 +402,16 @@ export class Vault extends Environment {
/** @ignore */
unset = () => {
throw 'Vault can not be unset in script';
throw new Error('Vault can not be unset in script');
};
/** @ignore */
clear = () => {
throw 'Vault can not be cleared in script';
throw new Error('Vault can not be cleared in script');
};
/** @ignore */
set = () => {
throw 'Vault can not be set in script';
throw new Error('Vault can not be set in script');
};
}

View File

@@ -342,7 +342,7 @@ export function transformToSdkProxyOptions(
let sanitizedProxy = bestProxy;
if (!bestProxy.includes('://')) {
getExistingConsole().warn(`The protocol is missing for proxy, 'https:' is enabled for: ${bestProxy}`);
sanitizedProxy = 'https://' + bestProxy;
sanitizedProxy = `https://${bestProxy}`;
}
try {
@@ -360,7 +360,7 @@ export function transformToSdkProxyOptions(
proxy.authenticate = true;
}
} catch (e) {
throw `Failed to parse proxy (${sanitizedProxy}): ${e.message}`;
throw new Error(`Failed to parse proxy (${sanitizedProxy}): ${e.message}`);
}
}

View File

@@ -123,6 +123,7 @@ collection:
send: true
store: true
- name: New Request
url: http://localhost:4010/echo
meta:
id: req_845c9ad3b2934f2098cb77a5dce8b0f5
created: 1745481338217

View File

@@ -28,7 +28,7 @@ test.describe('gRPC interactions', () => {
await page.click('text=Send');
// Check for the single Unary response
await page.click('text=Response 1');
await page.getByRole('tab', { name: 'Response 1', exact: true }).click();
await expect.soft(statusTag).toContainText('0 OK');
await expect.soft(responseBody).toContainText('Berkshire Valley Management Area Trail');
@@ -42,8 +42,8 @@ test.describe('gRPC interactions', () => {
await streamMessage.click();
// Check for the 3rd stream and response
await page.locator('text=Stream 3').click();
await page.locator('text=Response 3').click();
await page.getByRole('tab', { name: 'Stream 3', exact: true }).click();
await page.getByRole('tab', { name: 'Response 3', exact: true }).click();
// Finish the stream
await page.locator('text=Commit').click();
@@ -60,8 +60,8 @@ test.describe('gRPC interactions', () => {
// Finish the stream and check response
await page.locator('text=Commit').click();
await page.locator('text=Stream 3').click();
await page.locator('text=Response 1').click();
await page.getByRole('tab', { name: 'Stream 3', exact: true }).click();
await page.getByRole('tab', { name: 'Response 1', exact: true }).click();
await expect.soft(statusTag).toContainText('0 OK');
await expect.soft(responseBody).toContainText('point_count": 3');
@@ -72,6 +72,6 @@ test.describe('gRPC interactions', () => {
// Check response
await expect.soft(statusTag).toContainText('0 OK');
await expect.soft(responseBody).toContainText('Patriots Path');
await page.locator('text=Response 64').click();
await page.getByRole('tab', { name: 'Response 64', exact: true }).click();
});
});

View File

@@ -382,13 +382,14 @@ test.describe('pre-request features tests', () => {
// update proxy configuration
await page.getByTestId('settings-button').click();
await page.locator('text=Insomnia Preferences').first().click();
await page.getByLabel('Request timeout (ms)').fill('5000');
await page.getByRole('tab', { name: 'Proxy' }).click();
await page.locator('text=Enable proxy').click();
await page.locator('[name="httpProxy"]').fill('localhost:1111');
await page.locator('[name="httpsProxy"]').fill('localhost:2222');
await page.locator('[name="noProxy"]').fill('http://a.com,https://b.com');
await page.locator('.app').press('Escape');
// add 1s timeout to ensure noProxy settings is applied - INS-4155
await page.getByLabel('Request Collection').getByTestId('test proxies manipulation').press('Enter');
await page.getByRole('tab', { name: 'Body' }).click();

View File

@@ -1,12 +1,13 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { type BaseModel } from '../models';
import type { BaseModel } from '../models';
import * as models from '../models';
import type { Environment, UserUploadEnvironment } from '../models/environment';
import { getBodyBuffer } from '../models/response';
import type { Settings } from '../models/settings';
import {
defaultSendActionRuntime,
fetchRequestData,
responseTransform,
sendCurlAndWriteTimeline,
@@ -14,12 +15,15 @@ import {
tryToExecutePreRequestScript,
tryToInterpolateRequest,
} from '../network/network';
import { defaultSendActionRuntime } from '../network/network';
import { database } from './database';
// The network layer uses settings from the settings model
// We want to give consumers the ability to override certain settings
type SettingsOverride = Pick<Settings, 'validateSSL' | 'dataFolders'>;
interface SettingsOverride {
validateSSL?: Settings['validateSSL'];
dataFolders?: Settings['dataFolders'];
timeout?: Settings['timeout'];
}
const wrapAroundIterationOverIterationData = (
list?: UserUploadEnvironment[],
currentIteration?: number,
@@ -106,6 +110,11 @@ export async function getSendRequestCallbackMemDb(
requestData.responseId,
);
const res = await responseTransform(response, environmentId, renderedRequest, renderedResult.context);
if (res.error) {
throw new Error(res.error);
}
const postMutatedContext = await tryToExecuteAfterResponseScript({
...requestData,
...mutatedContext,
@@ -113,21 +122,12 @@ export async function getSendRequestCallbackMemDb(
transientVariables: mutatedContext.transientVariables || transientVariables,
response,
});
// TODO: figure out how to handle this error
if ('error' in postMutatedContext) {
console.error(
'[network] An error occurred while running after-response script for request named:',
renderedRequest.name,
);
throw {
error: postMutatedContext.error,
response: await responseTransform(
response,
requestData.activeEnvironmentId,
renderedRequest,
renderedResult.context,
),
};
throw new Error(postMutatedContext.error);
}
const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res;

View File

@@ -38,9 +38,14 @@ window.bridge.onmessage(
const result = await window.bridge.Promise.race([timeoutPromise, runScript(data)]);
callback(result);
} catch (err) {
const errMessage = err.message ? `Error from Pre-request or after-response script:\n${err.message};` : err;
const errStack = err.stack ? `Stack: ${err.stack};` : '';
const fullErrMessage = `${errMessage}\n${errStack}`;
const errMessage = err.message
? `Error from Pre-request or after-response script:
${err.message}`
: err;
const fullErrMessage = `${errMessage}
${err.stack ? `Stack: ${err.stack}` : ''}`;
Sentry.captureException(errMessage, {
tags: {
source: 'hidden-window',

View File

@@ -489,7 +489,6 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe
request,
environment,
timelinePath,
responseId,
baseEnvironment,
clientCertificates,
cookieJar,
@@ -663,21 +662,8 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe
timelinePath,
serializeNDJSON([{ value: err.message, name: 'Text', timestamp: Date.now() }]),
);
const requestId = request._id;
// stack trace is ignored as it is always from preload
const errMessage = err.message ? err.message : err;
const responsePatch = {
_id: responseId,
parentId: requestId,
environemntId: environment._id,
globalEnvironmentId: globals?._id,
timelinePath,
statusMessage: 'Error',
error: errMessage,
};
const res = await models.response.create(responsePatch, settings.maxHistoryResponses);
models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: res._id });
return { error: errMessage };
}
};
@@ -951,6 +937,7 @@ export async function sendCurlAndWriteTimeline(
};
}
// Apply plugins to response
export const responseTransform = async (
patch: ResponsePatch,
environmentId: string | null,

View File

@@ -93,6 +93,11 @@ const writeToDownloadPath = (
});
};
// Can fail with errors from:
// 1. pre-request script
// 2. request sending
// 3. after-response script
// In each case we create a new response with the error message and set it to active response
export const sendActionImplementation = async (options: {
requestId: string;
shouldPromptForPathAfterResponse: boolean | undefined;
@@ -103,7 +108,7 @@ export const sendActionImplementation = async (options: {
userUploadEnvironment?: UserUploadEnvironment;
transientVariables?: Environment;
runtime?: SendActionRuntime;
}) => {
}): Promise<{ nextRequestIdOrName: string | undefined } | undefined> => {
const {
requestId,
userUploadEnvironment,
@@ -118,7 +123,7 @@ export const sendActionImplementation = async (options: {
window.main.startExecution({ requestId });
const requestData = await fetchRequestData(requestId);
const requestMeta = await models.requestMeta.getByParentId(requestId);
const requestMeta = await models.requestMeta.getOrCreateByParentId(requestId);
const transientVariables = nullableTransientVariables || {
...models.environment.init(),
_id: uuidv4(),
@@ -139,37 +144,42 @@ export const sendActionImplementation = async (options: {
iterationCount,
runtime,
);
if ('error' in mutatedContext) {
window.main.completeExecutionStep({ requestId });
throw {
// create response with error info, so that we can store response in db and show it in response viewer
response: {
const createdResponse = await models.response.create(
{
_id: requestData.responseId,
parentId: requestId,
environemntId: requestData.environment,
environmentId: requestData.environment._id,
statusMessage: 'Error',
error: mutatedContext.error,
timelinePath: requestData.timelinePath,
},
maxHistoryResponses: requestData.settings.maxHistoryResponses,
requestMeta,
error: mutatedContext.error,
};
requestData.settings.maxHistoryResponses,
);
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: createdResponse._id });
window.main.completeExecutionStep({ requestId });
return { nextRequestIdOrName: mutatedContext.execution?.nextRequestIdOrName };
}
if (mutatedContext.execution?.skipRequest) {
// cancel request running if skipRequest in pre-request script
const responseId = requestData.responseId;
const responsePatch = {
_id: responseId,
parentId: requestId,
environemntId: requestData.environment,
statusMessage: 'Cancelled',
error: 'Request was cancelled by pre-request script',
};
// create and update response to activeResponse
await models.response.create(responsePatch, requestData.settings.maxHistoryResponses);
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: responseId });
const createdResponse = await models.response.create(
{
_id: requestData.responseId,
parentId: requestId,
environmentId: requestData.environment._id,
statusMessage: 'Cancelled',
error: 'Request was cancelled by pre-request script',
timelinePath: requestData.timelinePath,
},
requestData.settings.maxHistoryResponses,
);
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: createdResponse._id });
window.main.completeExecutionStep({ requestId });
return mutatedContext;
return { nextRequestIdOrName: mutatedContext.execution?.nextRequestIdOrName };
}
window.main.completeExecutionStep({ requestId });
@@ -198,8 +208,6 @@ export const sendActionImplementation = async (options: {
// TODO: remove this temporary hack to support GraphQL variables in the request body properly
parseGraphQLReqeustBody(renderedRequest);
invariant(requestMeta, 'RequestMeta not found');
window.main.addExecutionStep({ requestId, stepName: 'Sending request' });
const response = await sendCurlAndWriteTimeline(
renderedRequest,
@@ -211,18 +219,22 @@ export const sendActionImplementation = async (options: {
runtime,
);
window.main.completeExecutionStep({ requestId });
if ('error' in response) {
throw {
response: await responseTransform(
response,
requestData.activeEnvironmentId,
renderedRequest,
renderedResult.context,
),
maxHistoryResponses: requestData.settings.maxHistoryResponses,
requestMeta,
error: response.error,
};
const createdResponse = await models.response.create(
{
_id: requestData.responseId,
parentId: requestId,
environmentId: requestData.environment._id,
statusMessage: 'Error',
error: response.error,
timelinePath: requestData.timelinePath,
},
requestData.settings.maxHistoryResponses,
);
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: createdResponse._id });
window.main.completeExecutionStep({ requestId });
return { nextRequestIdOrName: mutatedContext.execution?.nextRequestIdOrName };
}
const baseResponsePatch = await responseTransform(
@@ -249,18 +261,22 @@ export const sendActionImplementation = async (options: {
iterationCount,
runtime,
});
if ('error' in postMutatedContext) {
throw {
response: await responseTransform(
response,
requestData.activeEnvironmentId,
renderedRequest,
renderedResult.context,
),
maxHistoryResponses: requestData.settings.maxHistoryResponses,
requestMeta,
error: postMutatedContext.error,
};
const createdResponse = await models.response.create(
{
_id: requestData.responseId,
parentId: requestId,
environmentId: requestData.environment._id,
statusMessage: 'Error',
error: postMutatedContext.error,
timelinePath: requestData.timelinePath,
},
requestData.settings.maxHistoryResponses,
);
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: createdResponse._id });
window.main.completeExecutionStep({ requestId });
return { nextRequestIdOrName: postMutatedContext.execution?.nextRequestIdOrName };
}
window.main.completeExecutionStep({ requestId });
@@ -291,7 +307,7 @@ export const sendActionImplementation = async (options: {
if (!shouldWriteToFile) {
const response = await models.response.create(responsePatch, requestData.settings.maxHistoryResponses);
await models.requestMeta.update(requestMeta, { activeResponseId: response._id });
return postMutatedContext;
return { nextRequestIdOrName: postMutatedContext.execution?.nextRequestIdOrName };
}
if (requestMeta.downloadPath) {
@@ -299,12 +315,13 @@ export const sendActionImplementation = async (options: {
const name = header
? contentDisposition.parse(header.value).parameters.filename
: `${requestData.request.name.replace(/\s/g, '-').toLowerCase()}.${(responsePatch.contentType && mimeExtension(responsePatch.contentType)) || 'unknown'}`;
return writeToDownloadPath(
writeToDownloadPath(
path.join(requestMeta.downloadPath, name),
responsePatch,
requestMeta,
requestData.settings.maxHistoryResponses,
);
return { nextRequestIdOrName: postMutatedContext.execution?.nextRequestIdOrName };
}
const defaultPath = window.localStorage.getItem('insomnia.sendAndDownloadLocation');
const { filePath } = await window.dialog.showSaveDialog({
@@ -314,10 +331,11 @@ export const sendActionImplementation = async (options: {
...(defaultPath ? { defaultPath } : {}),
});
if (!filePath) {
return null;
return { nextRequestIdOrName: postMutatedContext.execution?.nextRequestIdOrName };
}
window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath);
return writeToDownloadPath(filePath, responsePatch, requestMeta, requestData.settings.maxHistoryResponses);
writeToDownloadPath(filePath, responsePatch, requestMeta, requestData.settings.maxHistoryResponses);
return { nextRequestIdOrName: postMutatedContext.execution?.nextRequestIdOrName };
};
export async function clientAction({ request, params }: Route.ClientActionArgs) {
@@ -325,41 +343,24 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
const { shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable } = (await request.json()) as SendActionParams;
try {
return await sendActionImplementation({
await sendActionImplementation({
requestId,
shouldPromptForPathAfterResponse,
ignoreUndefinedEnvVariable,
});
return null;
} catch (error) {
const err = error as unknown as {
error: any;
response?: ResponsePatch & { _id: string };
requestMeta?: RequestMeta;
maxHistoryResponses?: number;
};
console.log('[request] Failed to send request', err);
const e = err.error || err;
console.error('[request] Failed to send request', error);
// TODO: consider if interpolation errors should be handled in the send request catch block
// idea: move missing env variable detection to tryToInterpolateRequest
const url = new URL(request.url);
// when after-script error, there is no error in response, we need to set error info into response, so that we can show it in response viewer
if (err.response && err.requestMeta && err.response._id) {
if (!err.response.error) {
err.response.error = e;
err.response.statusMessage = 'Error';
err.response.statusCode = 0;
}
// this part is for persisting useful info (e.g. timeline) for debugging, even there is an error
const existingResponse = await models.response.getById(err.response._id);
const response = existingResponse || (await models.response.create(err.response, err.maxHistoryResponses));
await models.requestMeta.update(err.requestMeta, { activeResponseId: response._id });
} else {
// if the error is not from response, we need to set it to url param and show it in modal
url.searchParams.set('error', e);
if (e?.extraInfo && e?.extraInfo?.subType === 'environmentVariable') {
url.searchParams.set('envVariableMissing', '1');
url.searchParams.set('undefinedEnvironmentVariables', e?.extraInfo?.undefinedEnvironmentVariables);
}
// if the error is not from response, we need to set it to url param and show it in modal
const e = error.error || error;
url.searchParams.set('error', e);
if (e?.extraInfo && e?.extraInfo?.subType === 'environmentVariable') {
url.searchParams.set('envVariableMissing', '1');
url.searchParams.set('undefinedEnvironmentVariables', e?.extraInfo?.undefinedEnvironmentVariables);
}
window.main.completeExecutionStep({ requestId });

View File

@@ -54,7 +54,6 @@ import { useRunnerRequestList } from '~/ui/hooks/use-runner-request-list';
import { moveAfter, moveBefore } from '~/utils';
import { invariant } from '~/utils/invariant';
import { type RequestContext } from '../../../insomnia-scripting-environment/src/objects';
import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner';
const inputStyle =
@@ -64,7 +63,7 @@ const iterationInputStyle =
// TODO: improve the performance for a lot of logs
async function aggregateAllTimelines(errorMsg: string | null, testResult: RunnerTestResult) {
let timelines = new Array<ResponseTimelineEntry>();
let timelines: ResponseTimelineEntry[] = [];
const responsesInfo = testResult.responsesInfo;
for (const respInfo of responsesInfo) {
@@ -135,7 +134,7 @@ const defaultAdvancedConfig = {
keepLog: true,
};
export const Runner: FC<{}> = () => {
export const Runner: FC = () => {
const [searchParams] = useSearchParams();
const [errorMsg, setErrorMsg] = useState<null | string>(null);
@@ -952,7 +951,7 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
while (j < requests.length) {
// TODO: we might find a better way to do runner cancellation
if (getExecution(runnerId) === undefined) {
throw 'Runner has been stopped';
throw new Error('Runner has been stopped');
}
const targetRequest = requests[j];
@@ -1001,7 +1000,7 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
await new Promise(resolve => setTimeout(resolve, delay));
const mutatedContext = (await sendActionImplementation({
const execution = await sendActionImplementation({
requestId: targetRequest.id,
iteration: i + 1,
iterationCount,
@@ -1011,9 +1010,9 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
testResultCollector: resultCollector,
runtime,
transientVariables: testCtx.transientVariables,
})) as RequestContext | null;
if (mutatedContext?.execution?.nextRequestIdOrName) {
nextRequestIdOrName = mutatedContext.execution.nextRequestIdOrName || '';
});
if (execution?.nextRequestIdOrName) {
nextRequestIdOrName = execution.nextRequestIdOrName || '';
}
const requestResults: RunnerResultPerRequest = {
@@ -1086,10 +1085,8 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
window.main.completeExecutionStep({ requestId: runnerId });
} catch (e) {
// the error could be from third party
const errMsg = e.error || e;
updateExecution(runnerId, {
error: errMsg,
});
const errMsg = e.message || e.error || e;
updateExecution(runnerId, { error: errMsg });
return null;
} finally {
cancelExecution(runnerId);

View File

@@ -486,8 +486,8 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(
onCancel={() => setShowEnvVariableMissingModal(false)}
>
<div>
These environment variables have been defined, but have not been valued with in the currently active
environment:
These environment variables have been defined, but have not been assigned a value within the currently
active environment:
<div className="flex max-h-80 flex-wrap gap-2 overflow-y-auto">
{undefinedEnvironmentVariableList?.map(item => {
return (