From d798be87dc9de6491e5487dd6776b2c6cdc1bafa Mon Sep 17 00:00:00 2001 From: Ryan Willis Date: Tue, 18 Nov 2025 10:50:19 -0700 Subject: [PATCH] feat(inso): support request timeouts (#9363) * feat: support request timeouts in inso --------- Co-authored-by: Jack Kavanagh --- eslint.config.mjs | 1 + packages/insomnia-inso/package.json | 1 + packages/insomnia-inso/src/cli.test.ts | 5 + packages/insomnia-inso/src/cli.ts | 18 +- .../fixtures/nedb/insomnia.CaCertificate.db | 2 +- .../nedb/insomnia.ClientCertificate.db | 2 +- .../fixtures/nedb/insomnia.CloudCredential.db | 0 .../db/fixtures/nedb/insomnia.CookieJar.db | 0 .../src/db/fixtures/nedb/insomnia.Request.db | 2 +- .../fixtures/nedb/insomnia.WorkspaceMeta.db | 0 .../src/examples/timeout-test.yml | 43 +++++ .../src/objects/environments.ts | 12 +- .../src/objects/proxy-configs.ts | 4 +- .../insomnia-smoke-test/fixtures/simple.yaml | 1 + .../tests/smoke/grpc-interactions.test.ts | 12 +- .../smoke/pre-request-script-features.test.ts | 3 +- packages/insomnia/src/common/send-request.ts | 26 +-- packages/insomnia/src/entry.hidden-window.ts | 11 +- packages/insomnia/src/network/network.ts | 15 +- ...kspaceId.debug.request.$requestId.send.tsx | 157 +++++++++--------- ...Id.workspace.$workspaceId.debug.runner.tsx | 21 +-- .../src/ui/components/request-url-bar.tsx | 4 +- 22 files changed, 196 insertions(+), 144 deletions(-) create mode 100644 packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CloudCredential.db create mode 100644 packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CookieJar.db create mode 100644 packages/insomnia-inso/src/db/fixtures/nedb/insomnia.WorkspaceMeta.db create mode 100644 packages/insomnia-inso/src/examples/timeout-test.yml diff --git a/eslint.config.mjs b/eslint.config.mjs index f976f95b17..c1e17b93d8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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 diff --git a/packages/insomnia-inso/package.json b/packages/insomnia-inso/package.json index 2b7ea9dc2f..f6bd1bfeac 100644 --- a/packages/insomnia-inso/package.json +++ b/packages/insomnia-inso/package.json @@ -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", diff --git a/packages/insomnia-inso/src/cli.test.ts b/packages/insomnia-inso/src/cli.test.ts index a0aa2a32d7..2c17f56e7f 100644 --- a/packages/insomnia-inso/src/cli.test.ts +++ b/packages/insomnia-inso/src/cli.test.ts @@ -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 diff --git a/packages/insomnia-inso/src/cli.ts b/packages/insomnia-inso/src/cli.ts index a2af13b7ca..43b6353a6b 100644 --- a/packages/insomnia-inso/src/cli.ts +++ b/packages/insomnia-inso/src/cli.ts @@ -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 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 ', 'milliseconds before request times out', undefined) // defaults to user settings .option('-k, --disableCertValidation', 'disable certificate validation for requests with SSL', false) .option('--httpsProxy ', 'URL for the proxy server for https requests.', proxySettings.httpsProxy) .option('--httpProxy ', '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 ', 'environment to use', '') .option('-g, --globals ', 'global environment to use (filepath or id)', '') .option('--delay-request ', 'milliseconds to delay between requests', '0') + .option('--requestTimeout ', 'milliseconds before request times out', undefined) // defaults to user settings .option('--env-var ', 'override environment variables', collect, []) .option('-n, --iteration-count ', 'number of times to repeat', '1') .option('-d, --iteration-data ', '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}\``); diff --git a/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CaCertificate.db b/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CaCertificate.db index b62492be3a..25dcea3ea3 100644 --- a/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CaCertificate.db +++ b/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CaCertificate.db @@ -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} diff --git a/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.ClientCertificate.db b/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.ClientCertificate.db index 34f9154f84..b6019f2cf1 100644 --- a/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.ClientCertificate.db +++ b/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.ClientCertificate.db @@ -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} diff --git a/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CloudCredential.db b/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CloudCredential.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CookieJar.db b/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.CookieJar.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.Request.db b/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.Request.db index 4ad5878888..682058c1e8 100644 --- a/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.Request.db +++ b/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.Request.db @@ -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"} diff --git a/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.WorkspaceMeta.db b/packages/insomnia-inso/src/db/fixtures/nedb/insomnia.WorkspaceMeta.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-inso/src/examples/timeout-test.yml b/packages/insomnia-inso/src/examples/timeout-test.yml new file mode 100644 index 0000000000..4e13a72327 --- /dev/null +++ b/packages/insomnia-inso/src/examples/timeout-test.yml @@ -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 diff --git a/packages/insomnia-scripting-environment/src/objects/environments.ts b/packages/insomnia-scripting-environment/src/objects/environments.ts index 0b5d2c2243..a9e302c82f 100644 --- a/packages/insomnia-scripting-environment/src/objects/environments.ts +++ b/packages/insomnia-scripting-environment/src/objects/environments.ts @@ -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'); }; } diff --git a/packages/insomnia-scripting-environment/src/objects/proxy-configs.ts b/packages/insomnia-scripting-environment/src/objects/proxy-configs.ts index 28762c1f84..546abed1fe 100644 --- a/packages/insomnia-scripting-environment/src/objects/proxy-configs.ts +++ b/packages/insomnia-scripting-environment/src/objects/proxy-configs.ts @@ -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}`); } } diff --git a/packages/insomnia-smoke-test/fixtures/simple.yaml b/packages/insomnia-smoke-test/fixtures/simple.yaml index 339196feca..42cb662058 100644 --- a/packages/insomnia-smoke-test/fixtures/simple.yaml +++ b/packages/insomnia-smoke-test/fixtures/simple.yaml @@ -123,6 +123,7 @@ collection: send: true store: true - name: New Request + url: http://localhost:4010/echo meta: id: req_845c9ad3b2934f2098cb77a5dce8b0f5 created: 1745481338217 diff --git a/packages/insomnia-smoke-test/tests/smoke/grpc-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/grpc-interactions.test.ts index b4bbab3c83..6ba7d3d65a 100644 --- a/packages/insomnia-smoke-test/tests/smoke/grpc-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/grpc-interactions.test.ts @@ -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(); }); }); diff --git a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts index 1e183be6f4..3103dd36e9 100644 --- a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts @@ -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(); diff --git a/packages/insomnia/src/common/send-request.ts b/packages/insomnia/src/common/send-request.ts index 41c86ce301..bb9492d118 100644 --- a/packages/insomnia/src/common/send-request.ts +++ b/packages/insomnia/src/common/send-request.ts @@ -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; +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; diff --git a/packages/insomnia/src/entry.hidden-window.ts b/packages/insomnia/src/entry.hidden-window.ts index 7ac5d59031..08521a8e3f 100644 --- a/packages/insomnia/src/entry.hidden-window.ts +++ b/packages/insomnia/src/entry.hidden-window.ts @@ -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', diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index 8df66178c8..3fe6fc5572 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -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, diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx index 80e94cd7b2..d02d6b76c2 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx @@ -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 }); diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx index d0c4f07c0c..dd99703f57 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx @@ -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(); + 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); @@ -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); diff --git a/packages/insomnia/src/ui/components/request-url-bar.tsx b/packages/insomnia/src/ui/components/request-url-bar.tsx index 5613817ee0..ddbb0a6d91 100644 --- a/packages/insomnia/src/ui/components/request-url-bar.tsx +++ b/packages/insomnia/src/ui/components/request-url-bar.tsx @@ -486,8 +486,8 @@ export const RequestUrlBar = forwardRef( onCancel={() => setShowEnvVariableMissingModal(false)} >
- 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:
{undefinedEnvironmentVariableList?.map(item => { return (