Refactor/decouple-db-and-request (#5572)

* move certs out of actually send

* extract fetcher

* extract interpolate try catch

* extract plugin transformer

* remove delay hack

* extract response transform

* extract url transformer

* extract cookies

* move custom logic into tranformer function

* decouple cancellation logic

* add bytes read

* only render can cancel requests

* order by usage

* fix tests

* use abort controller for cancellation

* raise methods to fix inso

* fix

* comments

* base env and rename cancel file

* fix import order
This commit is contained in:
Jack Kavanagh 2023-01-11 10:58:26 +01:00 committed by GitHub
parent bdce88d4d3
commit 4eae103503
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 364 additions and 367 deletions

View File

@ -450,6 +450,10 @@ export async function getRenderedGrpcRequestMessage(
}
type RenderRequestOptions = BaseRenderContextOptions & RenderRequest<Request>;
export interface RequestAndContext {
request: RenderedRequest;
context: Record<string, any>;
}
export async function getRenderedRequestAndContext(
{
request,
@ -457,7 +461,7 @@ export async function getRenderedRequestAndContext(
extraInfo,
purpose,
}: RenderRequestOptions,
) {
): Promise<RequestAndContext> {
const ancestors = await getRenderContextAncestors(request);
const workspace = ancestors.find(isWorkspace);
const parentId = workspace ? workspace._id : 'n/a';

View File

@ -2,9 +2,17 @@ import { BaseModel, types as modelTypes } from '../models';
import * as models from '../models';
import { getBodyBuffer } from '../models/response';
import { Settings } from '../models/settings';
import { send } from '../network/network';
import { isWorkspace } from '../models/workspace';
import {
responseTransform,
sendCurlAndWriteTimeline,
tryToInterpolateRequest,
tryToTransformRequestWithPlugins,
} from '../network/network';
import * as plugins from '../plugins';
import { invariant } from '../utils/invariant';
import { database } from './database';
import { RENDER_PURPOSE_SEND } from './render';
// The network layer uses settings from the settings model
// We want to give consumers the ability to override certain settings
@ -35,14 +43,48 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB:
upsert: docs,
remove: [],
});
const fetchInsoRequestData = async (requestId: string) => {
const request = await models.request.getById(requestId);
invariant(request, 'failed to find request');
const ancestors = await database.withAncestors(request, [
models.request.type,
models.requestGroup.type,
models.workspace.type,
]);
const workspaceDoc = ancestors.find(isWorkspace);
const workspaceId = workspaceDoc ? workspaceDoc._id : 'n/a';
const workspace = await models.workspace.getById(workspaceId);
invariant(workspace, 'failed to find workspace');
const settings = await models.settings.getOrCreate();
invariant(settings, 'failed to create settings');
const clientCertificates = await models.clientCertificate.findByParentId(workspaceId);
const caCert = await models.caCertificate.findByParentId(workspaceId);
return { request, settings, clientCertificates, caCert };
};
// Return callback helper to send requests
return async function sendRequest(requestId: string) {
try {
plugins.ignorePlugin('insomnia-plugin-kong-declarative-config');
plugins.ignorePlugin('insomnia-plugin-kong-kubernetes-config');
plugins.ignorePlugin('insomnia-plugin-kong-portal');
const res = await send(requestId, environmentId);
const {
request,
settings,
clientCertificates,
caCert,
} = await fetchInsoRequestData(requestId);
// NOTE: inso ignores active environment, using the one passed in
const renderResult = await tryToInterpolateRequest(request, environmentId, RENDER_PURPOSE_SEND);
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
const response = await sendCurlAndWriteTimeline(
renderedRequest,
clientCertificates,
caCert,
settings,
);
const res = await responseTransform(response, renderedRequest, renderResult.context);
const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res;
const headers = headerArray?.reduce((acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }), []);
const bodyBuffer = await getBodyBuffer(res) as Buffer;

View File

@ -1,8 +1,7 @@
// NOTE: this file should not be imported by electron renderer because node-libcurl is not-context-aware
// Related issue https://github.com/JCMais/node-libcurl/issues/155
if (process.type === 'renderer') {
throw new Error('node-libcurl unavailable in renderer');
}
import { invariant } from '../../utils/invariant';
invariant(process.type !== 'renderer', 'Native abstractions for Nodejs module unavailable in renderer');
import { Curl, CurlAuth, CurlCode, CurlFeature, CurlHttpVersion, CurlInfoDebug, CurlNetrc } from '@getinsomnia/node-libcurl';
import electron from 'electron';
@ -22,7 +21,7 @@ import { ResponseHeader } from '../../models/response';
import { buildMultipart } from './multipart';
import { parseHeaderStrings } from './parse-header-strings';
interface CurlRequestOptions {
export interface CurlRequestOptions {
requestId: string; // for cancellation
req: RequestUsedHere;
finalUrl: string;
@ -63,7 +62,7 @@ export interface ResponseTimelineEntry {
value: string;
}
interface CurlRequestOutput {
export interface CurlRequestOutput {
patch: ResponsePatch;
debugTimeline: ResponseTimelineEntry[];
headerResults: HeaderResult[];
@ -234,9 +233,7 @@ export const curlRequest = (options: CurlRequestOptions) => new Promise<CurlRequ
const { authentication } = req;
if (requestBodyPath) {
// AWS IAM file upload not supported
if (authentication.type === AUTH_AWS_IAM) {
throw new Error('AWS authentication not supported for provided body type');
}
invariant(authentication.type !== AUTH_AWS_IAM, 'AWS authentication not supported for provided body type');
const { size: contentLength } = fs.statSync(requestBodyPath);
curl.setOpt(Curl.option.INFILESIZE_LARGE, contentLength);
curl.setOpt(Curl.option.UPLOAD, 1);
@ -338,7 +335,7 @@ export const curlRequest = (options: CurlRequestOptions) => new Promise<CurlRequ
});
// NOTE: legacy write end callback
curl.on('error', () => responseBodyWriteStream.end());
curl.on('error', async function(err, code) {
curl.on('error', async (err, code) => {
const elapsedTime = curl.getInfo(Curl.info.TOTAL_TIME) as number * 1000;
curl.close();
await waitForStreamToFinish(responseBodyWriteStream);
@ -380,7 +377,7 @@ const closeReadFunction = (fd: number, isMultipart: boolean, path?: string) => {
}
};
interface HeaderResult {
export interface HeaderResult {
headers: ResponseHeader[];
version: string;
code: number;

View File

@ -197,12 +197,8 @@ describe('API Key', () => {
value: 'test',
addTo: 'queryParams',
};
const request = {
url: 'https://insomnia.rest/',
method: 'GET',
authentication,
};
const header = await getAuthQueryParams(request, 'https://insomnia.rest/');
const header = getAuthQueryParams(authentication, 'https://insomnia.rest/');
expect(header).toEqual({
'name': 'x-api-key',
'value': 'test',

View File

@ -28,7 +28,7 @@ window.app = electron.app;
const getRenderedRequest = async (args: Parameters<typeof getRenderedRequestAndContext>[0]) => (await getRenderedRequestAndContext(args)).request;
describe('actuallySend()', () => {
describe('sendCurlAndWriteTimeline()', () => {
beforeEach(async () => {
await globalBeforeEach();
await models.project.all();
@ -101,9 +101,10 @@ describe('actuallySend()', () => {
},
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
settings,
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -176,9 +177,10 @@ describe('actuallySend()', () => {
url: 'http://localhost',
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
settings,
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -276,9 +278,10 @@ describe('actuallySend()', () => {
settingSendCookies: false,
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
settings,
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -336,9 +339,10 @@ describe('actuallySend()', () => {
},
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
settings,
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -416,9 +420,10 @@ describe('actuallySend()', () => {
},
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
settings,
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -477,9 +482,10 @@ describe('actuallySend()', () => {
method: 'GET',
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
settings,
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -517,9 +523,10 @@ describe('actuallySend()', () => {
method: 'HEAD',
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
settings,
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -556,9 +563,10 @@ describe('actuallySend()', () => {
method: 'GET',
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
settings,
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -596,9 +604,10 @@ describe('actuallySend()', () => {
},
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
settings,
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -693,9 +702,10 @@ describe('actuallySend()', () => {
},
});
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
const response = await networkUtils.sendCurlAndWriteTimeline(
renderedRequest,
'',
[],
null,
{ ...settings, validateSSL: false },
);
const bodyBuffer = models.response.getBodyBuffer(response);
@ -745,7 +755,7 @@ describe('actuallySend()', () => {
parentId: workspace._id,
});
const renderedRequest = await getRenderedRequest({ request });
const responseV1 = await networkUtils._actuallySend(renderedRequest, '', {
const responseV1 = await networkUtils.sendCurlAndWriteTimeline(renderedRequest, [], null, {
...settings,
preferredHttpVersion: HttpVersions.V1_0,
});

View File

@ -11,7 +11,7 @@ import {
AUTH_OAUTH_2,
} from '../common/constants';
import type { RenderedRequest } from '../common/render';
import { RequestParameter } from '../models/request';
import { RequestAuthentication, RequestParameter } from '../models/request';
import { COOKIE, HEADER, QUERY_PARAMS } from './api-key/constants';
import { getBasicAuthHeader } from './basic-auth/get-header';
import { getBearerAuthHeader } from './bearer-auth/get-header';
@ -160,9 +160,7 @@ export async function getAuthHeader(renderedRequest: RenderedRequest, url: strin
return;
}
export async function getAuthQueryParams(renderedRequest: RenderedRequest) {
const { authentication } = renderedRequest;
export function getAuthQueryParams(authentication: RequestAuthentication) {
if (authentication.disabled) {
return;
}

View File

@ -0,0 +1,44 @@
import type { CurlRequestOptions, CurlRequestOutput } from '../main/network/libcurl-promise';
const cancelRequestFunctionMap = new Map<string, () => void>();
export async function cancelRequestById(requestId: string) {
const cancel = cancelRequestFunctionMap.get(requestId);
if (cancel) {
return cancel();
}
console.log(`[network] Failed to cancel req=${requestId} because cancel function not found`);
}
export const cancellableCurlRequest = async (requestOptions: CurlRequestOptions) => {
const requestId = requestOptions.requestId;
const controller = new AbortController();
const cancelRequest = () => {
window.main.cancelCurlRequest(requestId);
controller.abort();
};
cancelRequestFunctionMap.set(requestId, cancelRequest);
try {
const result = await cancellablePromise({ signal: controller.signal, fn: window.main.curlRequest(requestOptions) });
return result as CurlRequestOutput;
} catch (err) {
cancelRequestFunctionMap.delete(requestId);
if (err.name === 'AbortError') {
return { statusMessage: 'Cancelled', error: 'Request was cancelled' };
}
console.log('[network] Error', err);
return { statusMessage: 'Error', error: err.message || 'Something went wrong' };
}
};
const cancellablePromise = ({ signal, fn }: { signal: AbortSignal; fn: Promise<any> }) => {
if (signal?.aborted) {
return Promise.reject(new DOMException('Aborted', 'AbortError'));
}
return new Promise((resolve, reject) => {
const abortHandler = () => {
reject(new DOMException('Aborted', 'AbortError'));
};
fn.then(res => {
resolve(res);
signal?.removeEventListener('abort', abortHandler);
}, reject);
signal?.addEventListener('abort', abortHandler);
});
};

View File

@ -4,33 +4,31 @@ import mkdirp from 'mkdirp';
import { join as pathJoin } from 'path';
import { v4 as uuidv4 } from 'uuid';
import {
STATUS_CODE_PLUGIN_ERROR,
} from '../common/constants';
import { cookiesFromJar, jarFromCookies } from '../common/cookies';
import { database as db } from '../common/database';
import { getDataDirectory } from '../common/electron-helpers';
import {
delay,
getContentTypeHeader,
getLocationHeader,
getSetCookieHeaders,
} from '../common/misc';
import type { ExtraRenderInfo, RenderedRequest } from '../common/render';
import type { ExtraRenderInfo, RenderedRequest, RenderPurpose, RequestAndContext } from '../common/render';
import {
getRenderedRequestAndContext,
RENDER_PURPOSE_NO_RENDER,
RENDER_PURPOSE_SEND,
} from '../common/render';
import type { ResponsePatch, ResponseTimelineEntry } from '../main/network/libcurl-promise';
import type { HeaderResult, ResponsePatch, ResponseTimelineEntry } from '../main/network/libcurl-promise';
import * as models from '../models';
import { Cookie, CookieJar } from '../models/cookie-jar';
import type { Environment } from '../models/environment';
import type { Request } from '../models/request';
import { CaCertificate } from '../models/ca-certificate';
import { ClientCertificate } from '../models/client-certificate';
import { Cookie } from '../models/cookie-jar';
import type { Request, RequestAuthentication, RequestParameter } from '../models/request';
import type { Settings } from '../models/settings';
import { isWorkspace } from '../models/workspace';
import * as pluginContexts from '../plugins/context/index';
import * as plugins from '../plugins/index';
import { invariant } from '../utils/invariant';
import { setDefaultProtocol } from '../utils/url/protocol';
import {
buildQueryStringFromParams,
@ -38,160 +36,243 @@ import {
smartEncodeUrl,
} from '../utils/url/querystring';
import { getAuthHeader, getAuthQueryParams } from './authentication';
import { cancellableCurlRequest } from './cancellation';
import { urlMatchesCertHost } from './url-matches-cert-host';
// Time since user's last keypress to wait before making the request
const MAX_DELAY_TIME = 1000;
// used for oauth grant types
// creates a new request with the patch args
// and uses env and settings from workspace
// not cancellable but currently is
// used indirectly by send and getAuthHeader to fetch tokens
// @TODO unpack oauth into regular timeline and remove oauth timeine dialog
export async function sendWithSettings(
requestId: string,
requestPatch: Record<string, any>,
) {
console.log(`[network] Sending with settings req=${requestId}`);
const { request,
environment,
settings,
clientCertificates,
caCert } = await fetchRequestData(requestId);
const cancelRequestFunctionMap: Record<string, () => void> = {};
const newRequest: Request = await models.initModel(models.request.type, requestPatch, {
_id: request._id + '.other',
parentId: request._id,
});
let lastUserInteraction = Date.now();
const renderResult = await tryToInterpolateRequest(newRequest, environment._id);
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
export async function cancelRequestById(requestId: string) {
const hasCancelFunction = cancelRequestFunctionMap.hasOwnProperty(requestId) && typeof cancelRequestFunctionMap[requestId] === 'function';
if (hasCancelFunction) {
return cancelRequestFunctionMap[requestId]();
}
console.log(`[network] Failed to cancel req=${requestId} because cancel function not found`);
const response = await sendCurlAndWriteTimeline(
renderResult.request,
clientCertificates,
caCert,
{ ...settings, validateSSL: settings.validateAuthSSL },
);
return responseTransform(response, renderedRequest, renderResult.context);
}
export async function _actuallySend(
// used by test feature, inso, and plugin api
// not all need to be cancellable or to use curl
export async function send(
requestId: string,
environmentId?: string,
extraInfo?: ExtraRenderInfo,
) {
console.log(`[network] Sending req=${requestId} env=${environmentId || 'null'}`);
const { request,
environment,
settings,
clientCertificates,
caCert } = await fetchRequestData(requestId);
const renderResult = await tryToInterpolateRequest(request, environment._id, RENDER_PURPOSE_SEND, extraInfo);
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
const response = await sendCurlAndWriteTimeline(
renderedRequest,
clientCertificates,
caCert,
settings,
);
return responseTransform(response, renderedRequest, renderResult.context);
}
const fetchRequestData = async (requestId: string) => {
const request = await models.request.getById(requestId);
invariant(request, 'failed to find request');
const ancestors = await db.withAncestors(request, [
models.request.type,
models.requestGroup.type,
models.workspace.type,
]);
const workspaceDoc = ancestors.find(isWorkspace);
const workspaceId = workspaceDoc ? workspaceDoc._id : 'n/a';
const workspace = await models.workspace.getById(workspaceId);
invariant(workspace, 'failed to find workspace');
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
// fallback to base environment
const environment = workspaceMeta.activeEnvironmentId ?
await models.environment.getById(workspaceMeta.activeEnvironmentId)
: await models.environment.getOrCreateForParentId(workspace._id);
invariant(environment, 'failed to find environment');
const settings = await models.settings.getOrCreate();
invariant(settings, 'failed to create settings');
const clientCertificates = await models.clientCertificate.findByParentId(workspaceId);
const caCert = await models.caCertificate.findByParentId(workspaceId);
return { request, environment, settings, clientCertificates, caCert };
};
export const tryToInterpolateRequest = async (request: Request, environmentId: string, purpose?: RenderPurpose, extraInfo?: ExtraRenderInfo) => {
try {
return await getRenderedRequestAndContext({
request: request,
environmentId,
purpose,
extraInfo,
});
} catch (err) {
throw new Error(`Failed to render request: ${request._id}`);
}
};
export const tryToTransformRequestWithPlugins = async (renderResult: RequestAndContext) => {
const { request, context } = renderResult;
try {
return await _applyRequestPluginHooks(request, context);
} catch (err) {
throw new Error(`Failed to transform request with plugins: ${request._id}`);
}
};
export async function sendCurlAndWriteTimeline(
renderedRequest: RenderedRequest,
workspaceId: string,
clientCertificates: ClientCertificate[],
caCert: CaCertificate | null,
settings: Settings,
) {
return new Promise<ResponsePatch>(async resolve => {
const timeline: ResponseTimelineEntry[] = [];
const requestId = renderedRequest._id;
const timeline: ResponseTimelineEntry[] = [];
try {
// Setup the cancellation logic
cancelRequestFunctionMap[renderedRequest._id] = async () => {
const timelinePath = await storeTimeline(timeline);
// Tear Down the cancellation logic
if (cancelRequestFunctionMap.hasOwnProperty(renderedRequest._id)) {
delete cancelRequestFunctionMap[renderedRequest._id];
}
// NOTE: conditionally use ipc bridge, renderer cannot import native modules directly
const nodejsCancelCurlRequest = process.type === 'renderer'
? window.main.cancelCurlRequest
: (await import('../main/network/libcurl-promise')).cancelCurlRequest;
const { finalUrl, socketPath } = transformUrl(renderedRequest.url, renderedRequest.parameters, renderedRequest.authentication, renderedRequest.settingEncodeUrl);
nodejsCancelCurlRequest(renderedRequest._id);
return resolve({
elapsedTime: 0,
bytesRead: 0,
url: renderedRequest.url,
statusMessage: 'Cancelled',
error: 'Request was cancelled',
timelinePath,
});
};
const authQueryParam = await getAuthQueryParams(renderedRequest);
// Set the URL, including the query parameters
const qs = buildQueryStringFromParams(
authQueryParam
? renderedRequest.parameters.concat([authQueryParam])
: renderedRequest.parameters
);
const url = joinUrlAndQueryString(renderedRequest.url, qs);
const isUnixSocket = url.match(/https?:\/\/unix:\//);
let finalUrl, socketPath;
if (!isUnixSocket) {
finalUrl = smartEncodeUrl(url, renderedRequest.settingEncodeUrl);
} else {
// URL prep will convert "unix:/path" hostname to "unix/path"
const match = smartEncodeUrl(url, renderedRequest.settingEncodeUrl).match(/(https?:)\/\/unix:?(\/[^:]+):\/(.+)/);
const protocol = (match && match[1]) || '';
socketPath = (match && match[2]) || '';
const socketUrl = (match && match[3]) || '';
finalUrl = `${protocol}//${socketUrl}`;
}
timeline.push({ value: `Preparing request to ${finalUrl}`, name: 'Text', timestamp: Date.now() });
timeline.push({ value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() });
timeline.push({ value: `${renderedRequest.settingEncodeUrl ? 'Enable' : 'Disable'} automatic URL encoding`, name: 'Text', timestamp: Date.now() });
timeline.push({ value: `Preparing request to ${finalUrl}`, name: 'Text', timestamp: Date.now() });
timeline.push({ value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() });
timeline.push({ value: `${renderedRequest.settingEncodeUrl ? 'Enable' : 'Disable'} automatic URL encoding`, name: 'Text', timestamp: Date.now() });
if (!renderedRequest.settingSendCookies) {
timeline.push({ value: 'Disable cookie sending due to user setting', name: 'Text', timestamp: Date.now() });
}
const clientCertificates = await models.clientCertificate.findByParentId(workspaceId);
const certificates = clientCertificates.filter(c => !c.disabled && urlMatchesCertHost(setDefaultProtocol(c.host, 'https:'), renderedRequest.url));
const caCert = await models.caCertificate.findByParentId(workspaceId);
const caCertficatePath = caCert?.disabled === false ? caCert.path : null;
const authHeader = await getAuthHeader(renderedRequest, finalUrl);
if (!renderedRequest.settingSendCookies) {
timeline.push({ value: 'Disable cookie sending due to user setting', name: 'Text', timestamp: Date.now() });
}
// NOTE: conditionally use ipc bridge, renderer cannot import native modules directly
const nodejsCurlRequest = process.type === 'renderer'
? window.main.curlRequest
: (await import('../main/network/libcurl-promise')).curlRequest;
const authHeader = await getAuthHeader(renderedRequest, finalUrl);
const requestOptions = {
requestId,
req: renderedRequest,
finalUrl,
socketPath,
settings,
certificates: clientCertificates.filter(c => !c.disabled && urlMatchesCertHost(setDefaultProtocol(c.host, 'https:'), renderedRequest.url)),
caCertficatePath: caCert?.disabled === false ? caCert.path : null,
authHeader,
};
const requestOptions = {
requestId: renderedRequest._id,
req: renderedRequest,
finalUrl,
socketPath,
settings,
certificates,
caCertficatePath,
authHeader,
};
const { patch, debugTimeline, headerResults, responseBodyPath } = await nodejsCurlRequest(requestOptions);
const { cookieJar, settingStoreCookies } = renderedRequest;
// NOTE: conditionally use ipc bridge, renderer cannot import native modules directly
const nodejsCurlRequest = process.type === 'renderer'
? cancellableCurlRequest
: (await import('../main/network/libcurl-promise')).curlRequest;
const output = await nodejsCurlRequest(requestOptions);
// add set-cookie headers to file(cookiejar) and database
if (settingStoreCookies) {
// supports many set-cookies over many redirects
const redirects: string[][] = headerResults.map(({ headers }: any) => getSetCookiesFromResponseHeaders(headers));
const setCookieStrings: string[] = redirects.flat();
const totalSetCookies = setCookieStrings.length;
if (totalSetCookies) {
const currentUrl = getCurrentUrl({ headerResults, finalUrl });
const { cookies, rejectedCookies } = await addSetCookiesToToughCookieJar({ setCookieStrings, currentUrl, cookieJar });
rejectedCookies.forEach(errorMessage => timeline.push({ value: `Rejected cookie: ${errorMessage}`, name: 'Text', timestamp: Date.now() }));
const hasCookiesToPersist = totalSetCookies > rejectedCookies.length;
if (hasCookiesToPersist) {
const patch: Partial<CookieJar> = { cookies };
await models.cookieJar.update(cookieJar, patch);
timeline.push({ value: `Saved ${totalSetCookies} cookies`, name: 'Text', timestamp: Date.now() });
}
}
}
if ('error' in output) {
const timelinePath = await storeTimeline(timeline);
const lastRedirect = headerResults[headerResults.length - 1];
const responsePatch: ResponsePatch = {
contentType: getContentTypeHeader(lastRedirect.headers)?.value || '',
headers: lastRedirect.headers,
httpVersion: lastRedirect.version,
statusCode: lastRedirect.code,
statusMessage: lastRedirect.reason,
...patch,
};
const timelinePath = await storeTimeline([...timeline, ...debugTimeline]);
// Tear Down the cancellation logic
if (cancelRequestFunctionMap.hasOwnProperty(renderedRequest._id)) {
delete cancelRequestFunctionMap[renderedRequest._id];
}
return resolve({
timelinePath,
bodyPath: responseBodyPath,
...responsePatch,
});
} catch (err) {
console.log('[network] Error', err);
const timelinePath = await storeTimeline(timeline);
// Tear Down the cancellation logic
if (cancelRequestFunctionMap.hasOwnProperty(renderedRequest._id)) {
delete cancelRequestFunctionMap[renderedRequest._id];
}
return resolve({
url: renderedRequest.url,
error: err.message || 'Something went wrong',
elapsedTime: 0, // 0 because this path is hit during plugin calls
statusMessage: 'Error',
timelinePath,
});
}
});
return {
parentId: requestId,
url: requestOptions.finalUrl,
error: output.error,
elapsedTime: 0, // 0 because this path is hit during plugin calls
bytesRead: 0,
statusMessage: output.statusMessage,
timelinePath,
};
}
const { patch, debugTimeline, headerResults, responseBodyPath } = output;
const timelinePath = await storeTimeline([...timeline, ...debugTimeline]);
// transform output
const { cookies, rejectedCookies, totalSetCookies } = await extractCookies(headerResults, renderedRequest.cookieJar, finalUrl, renderedRequest.settingStoreCookies);
rejectedCookies.forEach(errorMessage => timeline.push({ value: `Rejected cookie: ${errorMessage}`, name: 'Text', timestamp: Date.now() }));
if (cookies) {
await models.cookieJar.update(renderedRequest.cookieJar, { cookies });
timeline.push({ value: `Saved ${totalSetCookies} cookies`, name: 'Text', timestamp: Date.now() });
}
const lastRedirect = headerResults[headerResults.length - 1];
return {
parentId: renderedRequest._id,
timelinePath,
bodyPath: responseBodyPath,
contentType: getContentTypeHeader(lastRedirect.headers)?.value || '',
headers: lastRedirect.headers,
httpVersion: lastRedirect.version,
statusCode: lastRedirect.code,
statusMessage: lastRedirect.reason,
...patch,
};
}
export const responseTransform = (patch: ResponsePatch, renderedRequest: RenderedRequest, context: Record<string, any>) => {
const response = {
...patch,
environmentId: patch.environmentId,
bodyCompression: null,
settingSendCookies: renderedRequest.settingSendCookies,
settingStoreCookies: renderedRequest.settingStoreCookies,
};
if (response.error) {
console.log(`[network] Response failed req=${patch.parentId} err=${response.error || 'n/a'}`);
return response;
}
console.log(`[network] Response succeeded req=${patch.parentId} status=${response.statusCode || '?'}`,);
return _applyResponsePluginHooks(
response,
renderedRequest,
context,
);
};
export const transformUrl = (url: string, params: RequestParameter[], authentication: RequestAuthentication, shouldEncode: boolean) => {
const authQueryParam = getAuthQueryParams(authentication);
const customUrl = joinUrlAndQueryString(url, buildQueryStringFromParams(authQueryParam ? params.concat([authQueryParam]) : params));
const isUnixSocket = customUrl.match(/https?:\/\/unix:\//);
if (!isUnixSocket) {
return { finalUrl: smartEncodeUrl(customUrl, shouldEncode) };
}
// URL prep will convert "unix:/path" hostname to "unix/path"
const match = smartEncodeUrl(customUrl, shouldEncode).match(/(https?:)\/\/unix:?(\/[^:]+):\/(.+)/);
const protocol = (match && match[1]) || '';
const socketPath = (match && match[2]) || '';
const socketUrl = (match && match[3]) || '';
return { finalUrl: `${protocol}//${socketUrl}`, socketPath };
};
const extractCookies = async (headerResults: HeaderResult[], cookieJar: any, finalUrl: string, settingStoreCookies: boolean) => {
// add set-cookie headers to file(cookiejar) and database
if (settingStoreCookies) {
// supports many set-cookies over many redirects
const redirects: string[][] = headerResults.map(({ headers }: any) => getSetCookiesFromResponseHeaders(headers));
const setCookieStrings: string[] = redirects.flat();
const totalSetCookies = setCookieStrings.length;
if (totalSetCookies) {
const currentUrl = getCurrentUrl({ headerResults, finalUrl });
const { cookies, rejectedCookies } = await addSetCookiesToToughCookieJar({ setCookieStrings, currentUrl, cookieJar });
const hasCookiesToPersist = totalSetCookies > rejectedCookies.length;
if (hasCookiesToPersist) {
return { cookies, rejectedCookies, totalSetCookies };
}
}
}
return { cookies: [], rejectedCookies: [], totalSetCookies: 0 };
};
export const getSetCookiesFromResponseHeaders = (headers: any[]) => getSetCookieHeaders(headers).map(h => h.value);
@ -227,169 +308,6 @@ export const addSetCookiesToToughCookieJar = async ({ setCookieStrings, currentU
return { cookies, rejectedCookies };
};
export async function sendWithSettings(
requestId: string,
requestPatch: Record<string, any>,
) {
console.log(`[network] Sending with settings req=${requestId}`);
const request = await models.request.getById(requestId);
if (!request) {
throw new Error(`Failed to find request: ${requestId}`);
}
const settings = await models.settings.getOrCreate();
const ancestors = await db.withAncestors(request, [
models.request.type,
models.requestGroup.type,
models.workspace.type,
]);
const workspaceDoc = ancestors.find(isWorkspace);
const workspaceId = workspaceDoc ? workspaceDoc._id : 'n/a';
const workspace = await models.workspace.getById(workspaceId);
if (!workspace) {
throw new Error(`Failed to find workspace for: ${requestId}`);
}
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
const environmentId: string = workspaceMeta.activeEnvironmentId || 'n/a';
const newRequest: Request = await models.initModel(models.request.type, requestPatch, {
_id: request._id + '.other',
parentId: request._id,
});
let renderResult: {
request: RenderedRequest;
context: Record<string, any>;
};
try {
renderResult = await getRenderedRequestAndContext({ request: newRequest, environmentId });
} catch (err) {
throw new Error(`Failed to render request: ${requestId}`);
}
const environment: Environment | null = await models.environment.getById(environmentId || 'n/a');
const responseEnvironmentId = environment ? environment._id : null;
const response = await _actuallySend(
renderResult.request,
workspace._id,
{ ...settings, validateSSL: settings.validateAuthSSL },
);
response.parentId = renderResult.request._id;
response.environmentId = responseEnvironmentId;
response.bodyCompression = null;
response.settingSendCookies = renderResult.request.settingSendCookies;
response.settingStoreCookies = renderResult.request.settingStoreCookies;
if (response.error) {
return response;
}
return _applyResponsePluginHooks(
response,
renderResult.request,
renderResult.context,
);
}
export async function send(
requestId: string,
environmentId?: string,
extraInfo?: ExtraRenderInfo,
) {
console.log(`[network] Sending req=${requestId} env=${environmentId || 'null'}`);
// HACK: wait for all debounces to finish
/*
* TODO: Do this in a more robust way
* The following block adds a "long" delay to let potential debounces and
* database updates finish before making the request. This is done by tracking
* the time of the user's last keypress and making sure the request is sent a
* significant time after the last press.
*/
const timeSinceLastInteraction = Date.now() - lastUserInteraction;
const delayMillis = Math.max(0, MAX_DELAY_TIME - timeSinceLastInteraction);
if (delayMillis > 0) {
await delay(delayMillis);
}
// Fetch some things
const request = await models.request.getById(requestId);
const settings = await models.settings.getOrCreate();
const ancestors = await db.withAncestors(request, [
models.request.type,
models.requestGroup.type,
models.workspace.type,
]);
if (!request) {
throw new Error(`Failed to find request to send for ${requestId}`);
}
const renderResult = await getRenderedRequestAndContext(
{
request,
environmentId,
purpose: RENDER_PURPOSE_SEND,
extraInfo,
},
);
const renderedRequestBeforePlugins = renderResult.request;
const renderedContextBeforePlugins = renderResult.context;
const workspaceDoc = ancestors.find(isWorkspace);
const workspace = await models.workspace.getById(workspaceDoc ? workspaceDoc._id : 'n/a');
if (!workspace) {
throw new Error(`Failed to find workspace for request: ${requestId}`);
}
const environment: Environment | null = await models.environment.getById(environmentId || 'n/a');
const responseEnvironmentId = environment ? environment._id : null;
let renderedRequest: RenderedRequest;
try {
renderedRequest = await _applyRequestPluginHooks(
renderedRequestBeforePlugins,
renderedContextBeforePlugins,
);
} catch (err) {
return {
environmentId: responseEnvironmentId,
error: err.message || 'Something went wrong',
parentId: renderedRequestBeforePlugins._id,
settingSendCookies: renderedRequestBeforePlugins.settingSendCookies,
settingStoreCookies: renderedRequestBeforePlugins.settingStoreCookies,
statusCode: STATUS_CODE_PLUGIN_ERROR,
statusMessage: err.plugin ? `Plugin ${err.plugin.name}` : 'Plugin',
url: renderedRequestBeforePlugins.url,
} as ResponsePatch;
}
const response = await _actuallySend(
renderedRequest,
workspace._id,
settings,
);
response.parentId = renderResult.request._id;
response.environmentId = responseEnvironmentId;
response.bodyCompression = null;
response.settingSendCookies = renderedRequest.settingSendCookies;
response.settingStoreCookies = renderedRequest.settingStoreCookies;
console.log(
response.error
? `[network] Response failed req=${requestId} err=${response.error || 'n/a'}`
: `[network] Response succeeded req=${requestId} status=${response.statusCode || '?'}`,
);
if (response.error) {
return response;
}
return _applyResponsePluginHooks(
response,
renderedRequest,
renderedContextBeforePlugins,
);
}
async function _applyRequestPluginHooks(
renderedRequest: RenderedRequest,
renderedContext: Record<string, any>,
@ -475,16 +393,3 @@ export function storeTimeline(timeline: ResponseTimelineEntry[]): Promise<string
});
});
}
if (global.document) {
document.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey || event.altKey) {
return;
}
lastUserInteraction = Date.now();
});
document.addEventListener('paste', () => {
lastUserInteraction = Date.now();
});
}

View File

@ -10,6 +10,7 @@ export function getSendRequestCallback(environmentId?: string) {
plugins.ignorePlugin('insomnia-plugin-kong-declarative-config');
plugins.ignorePlugin('insomnia-plugin-kong-kubernetes-config');
plugins.ignorePlugin('insomnia-plugin-kong-portal');
// NOTE: unit tests will use the UI selected environment
const res = await send(requestId, environmentId);
const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res;
const headers = headerArray?.reduce((acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }), []);

View File

@ -9,7 +9,7 @@ import { getSetCookieHeaders } from '../../../common/misc';
import * as models from '../../../models';
import type { Request } from '../../../models/request';
import type { Response } from '../../../models/response';
import { cancelRequestById } from '../../../network/network';
import { cancelRequestById } from '../../../network/cancellation';
import { jsonPrettify } from '../../../utils/prettify/json';
import { updateRequestMetaByParentId } from '../../hooks/create-request';
import { selectActiveResponse, selectResponseFilter, selectResponseFilterHistory, selectResponsePreviewMode, selectSettings } from '../../redux/selectors';