From 39f1602faa8d62d47578aecaf2e6aa45f49afbac Mon Sep 17 00:00:00 2001 From: Ryan Willis Date: Fri, 7 Feb 2025 13:46:50 -0700 Subject: [PATCH 1/6] fix: consolidate request+codegen headers+authorization --- packages/insomnia/src/common/render.ts | 6 ++++++ packages/insomnia/src/network/network.ts | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/insomnia/src/common/render.ts b/packages/insomnia/src/common/render.ts index 76920f571f8..56253839078 100644 --- a/packages/insomnia/src/common/render.ts +++ b/packages/insomnia/src/common/render.ts @@ -10,6 +10,7 @@ import { PATH_PARAMETER_REGEX, type Request } from '../models/request'; import { isRequestGroup, type RequestGroup } from '../models/request-group'; import type { WebSocketRequest } from '../models/websocket-request'; import { isWorkspace, type Workspace } from '../models/workspace'; +import { getOrInheritAuthentication, getOrInheritHeaders } from '../network/network'; import * as templating from '../templating'; import * as templatingUtils from '../templating/utils'; import { setDefaultProtocol } from '../utils/url/protocol'; @@ -601,6 +602,8 @@ export async function getRenderedRequestAndContext( ): Promise { const ancestors = await getRenderContextAncestors(request); const workspace = ancestors.find(isWorkspace); + const requestGroups = ancestors.filter(isRequestGroup); + const parentId = workspace ? workspace._id : 'n/a'; const cookieJar = await models.cookieJar.getOrCreateForParentId(parentId); const renderContext = await getRenderContext({ request, environment, ancestors, purpose, extraInfo, baseEnvironment, userUploadEnvironment, transientVariables }); @@ -618,6 +621,9 @@ export async function getRenderedRequestAndContext( // Render description separately because it's lower priority const description = request.description; request.description = ''; + + request.headers = getOrInheritHeaders({ request, requestGroups }); + request.authentication = getOrInheritAuthentication({ request, requestGroups }); // Render all request properties const renderResult = await render( { diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index e065a8368ce..935a5dbb4a5 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -125,10 +125,6 @@ export const fetchRequestData = async (requestId: string) => { const workspace = await models.workspace.getById(workspaceId); invariant(workspace, 'failed to find workspace'); const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); - // check for authentication overrides in parent folders - const requestGroups = ancestors.filter(isRequestGroup) as RequestGroup[]; - request.authentication = getOrInheritAuthentication({ request, requestGroups }); - request.headers = getOrInheritHeaders({ request, requestGroups }); // fallback to base environment const activeEnvironmentId = workspaceMeta.activeEnvironmentId; const activeEnvironment = activeEnvironmentId && await models.environment.getById(activeEnvironmentId); From d6b6bd336155020ef194d0c80d779079b5605712 Mon Sep 17 00:00:00 2001 From: Ryan Willis Date: Fri, 7 Feb 2025 14:25:34 -0700 Subject: [PATCH 2/6] try using native fetch api headers class --- packages/insomnia/src/network/network.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index 935a5dbb4a5..ee3a5905715 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -64,14 +64,20 @@ export const getOrInheritAuthentication = ({ request, requestGroups }: { request return { type: 'none' }; }; export function getOrInheritHeaders({ request, requestGroups }: { request: Pick; requestGroups: RequestGroup[] }): RequestHeader[] { - // recurse over each parent folder to append headers - // in case of duplicate, node-libcurl joins on comma - const headers = requestGroups - .reverse() + const httpHeaders = new Headers(); + const originalCaseMap = new Map(); + [...requestGroups + .reverse(), request] .map(({ headers }) => headers || []) - .flat(); - // if parent has foo: bar and child has foo: baz, request will have foo: bar, baz - return [...headers, ...request.headers]; + .flat().forEach(({ name, value, disabled }) => { + if (disabled) { + return; + } + // appending to headers will join matching headers with a comma + httpHeaders.append(name, value); + originalCaseMap.set(name.toLowerCase(), name); + }); + return Array.from(httpHeaders.entries()).map(([name, value]) => ({ name: originalCaseMap.get(name)!, value })); } // (only used for getOAuth2 token) Intended to gather all required database objects and initialize ids export const fetchRequestGroupData = async (requestGroupId: string) => { From 8813d84f85be718eef426882cdaf2a423a60f5c9 Mon Sep 17 00:00:00 2001 From: Ryan Willis Date: Fri, 7 Feb 2025 14:54:21 -0700 Subject: [PATCH 3/6] remove duplicate call --- packages/insomnia/src/common/send-request.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/insomnia/src/common/send-request.ts b/packages/insomnia/src/common/send-request.ts index ef45f77edf3..d5e87653651 100644 --- a/packages/insomnia/src/common/send-request.ts +++ b/packages/insomnia/src/common/send-request.ts @@ -10,8 +10,6 @@ import { getBodyBuffer } from '../models/response'; import type { Settings } from '../models/settings'; import { isWorkspace, type Workspace } from '../models/workspace'; import { - getOrInheritAuthentication, - getOrInheritHeaders, responseTransform, sendCurlAndWriteTimeline, tryToExecuteAfterResponseScript, @@ -76,11 +74,6 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB: const workspace = await models.workspace.getById(workspaceId); invariant(workspace, 'failed to find workspace'); - // check for authentication overrides in parent folders - const requestGroups = ancestors.filter(a => a.type === 'RequestGroup') as RequestGroup[]; - request.authentication = getOrInheritAuthentication({ request, requestGroups }); - request.headers = getOrInheritHeaders({ request, requestGroups }); - const settings = await models.settings.get(); invariant(settings, 'failed to create settings'); const clientCertificates = await models.clientCertificate.findByParentId(workspaceId); From ace79c7143694a5b171dadd3374d26c18e28456a Mon Sep 17 00:00:00 2001 From: Ryan Willis Date: Fri, 7 Feb 2025 14:59:34 -0700 Subject: [PATCH 4/6] case-sort test headers --- .../insomnia/src/common/__tests__/har.test.ts | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/insomnia/src/common/__tests__/har.test.ts b/packages/insomnia/src/common/__tests__/har.test.ts index ac48e3c2eec..a5508caf940 100644 --- a/packages/insomnia/src/common/__tests__/har.test.ts +++ b/packages/insomnia/src/common/__tests__/har.test.ts @@ -35,11 +35,11 @@ describe('export', () => { }, headers: [ { - name: 'Content-Type', + name: 'Accept', value: 'application/json', }, { - name: 'Accept', + name: 'Content-Type', value: 'application/json', disabled: false, }, @@ -89,11 +89,11 @@ describe('export', () => { cookies: [], headers: [ { - name: 'Content-Type', + name: 'Accept', value: 'application/json', }, { - name: 'Accept', + name: 'Content-Type', value: 'application/json', }, ], @@ -334,14 +334,14 @@ describe('export', () => { statusCode: 200, statusMessage: 'OK', headers: [ - { - name: 'Content-Type', - value: 'application/json', - }, { name: 'Content-Length', value: '2', }, + { + name: 'Content-Type', + value: 'application/json', + }, { name: 'Set-Cookie', value: 'sessionid=12345; HttpOnly; Path=/', @@ -364,14 +364,15 @@ describe('export', () => { }, ], headers: [ - { - name: 'Content-Type', - value: 'application/json', - }, { name: 'Content-Length', value: '2', }, + + { + name: 'Content-Type', + value: 'application/json', + }, { name: 'Set-Cookie', value: 'sessionid=12345; HttpOnly; Path=/', From 6996d102674a2655149503e0b7a180e4d6f3cb98 Mon Sep 17 00:00:00 2001 From: Ryan Willis Date: Mon, 10 Feb 2025 11:54:52 -0700 Subject: [PATCH 5/6] prevent header append from joining certain values --- .../insomnia/src/common/common-headers.ts | 16 ++++++++++++++++ packages/insomnia/src/network/network.ts | 19 +++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/insomnia/src/common/common-headers.ts b/packages/insomnia/src/common/common-headers.ts index 51e15225b6d..72be1d691d3 100644 --- a/packages/insomnia/src/common/common-headers.ts +++ b/packages/insomnia/src/common/common-headers.ts @@ -5,6 +5,22 @@ import allEncodings from '../datasets/encodings'; import allHeaderNames from '../datasets/header-names'; import type { RequestHeader } from '../models/request'; +export const SINGLE_VALUE_HEADERS = [ + 'authorization', + 'proxy-authorization', + 'content-length', + 'content-type', + 'content-encoding', + 'content-location', + 'connection', + 'host', + 'upgrade', + 'keep-alive', + 'range', + 'te', + 'trailer', +]; + export const getCommonHeaderValues = (pair: RequestHeader): any[] => { switch (pair.name.toLowerCase()) { case 'content-type': diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index ee3a5905715..7b8081b3b14 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -4,6 +4,7 @@ import type { ExecutionOption, RequestContext, RequestTestResult } from 'insomni import orderedJSON from 'json-order'; import { join as pathJoin } from 'path'; +import { SINGLE_VALUE_HEADERS } from '../common/common-headers'; import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from '../common/constants'; import { database as db } from '../common/database'; import { @@ -71,12 +72,18 @@ export function getOrInheritHeaders({ request, requestGroups }: { request: Pick< .map(({ headers }) => headers || []) .flat().forEach(({ name, value, disabled }) => { if (disabled) { - return; - } - // appending to headers will join matching headers with a comma - httpHeaders.append(name, value); - originalCaseMap.set(name.toLowerCase(), name); - }); + return; + } + const normalizedCase = name.toLowerCase(); + // prevent multiplied headers (Content-Type, etc) + if (SINGLE_VALUE_HEADERS.includes(normalizedCase)) { + httpHeaders.set(normalizedCase, value); + } else { + // appending will join matching header values with a comma + httpHeaders.append(normalizedCase, value); + } + originalCaseMap.set(normalizedCase, name); + }); return Array.from(httpHeaders.entries()).map(([name, value]) => ({ name: originalCaseMap.get(name)!, value })); } // (only used for getOAuth2 token) Intended to gather all required database objects and initialize ids From 73d1d0c4faab6e06a8e0b9db9dacac8db07106a2 Mon Sep 17 00:00:00 2001 From: Ryan Willis Date: Tue, 11 Feb 2025 17:07:03 -0700 Subject: [PATCH 6/6] trim restricted headers and add tests --- .../insomnia/src/common/common-headers.ts | 3 -- .../src/network/__tests__/network.test.ts | 18 +++++++++ packages/insomnia/src/network/network.ts | 37 ++++++++++--------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/insomnia/src/common/common-headers.ts b/packages/insomnia/src/common/common-headers.ts index 72be1d691d3..67ee4745ed8 100644 --- a/packages/insomnia/src/common/common-headers.ts +++ b/packages/insomnia/src/common/common-headers.ts @@ -6,7 +6,6 @@ import allHeaderNames from '../datasets/header-names'; import type { RequestHeader } from '../models/request'; export const SINGLE_VALUE_HEADERS = [ - 'authorization', 'proxy-authorization', 'content-length', 'content-type', @@ -15,9 +14,7 @@ export const SINGLE_VALUE_HEADERS = [ 'connection', 'host', 'upgrade', - 'keep-alive', 'range', - 'te', 'trailer', ]; diff --git a/packages/insomnia/src/network/__tests__/network.test.ts b/packages/insomnia/src/network/__tests__/network.test.ts index cd497eceedd..55505f48b71 100644 --- a/packages/insomnia/src/network/__tests__/network.test.ts +++ b/packages/insomnia/src/network/__tests__/network.test.ts @@ -1051,3 +1051,21 @@ describe('getCurrentUrl for tough-cookie', () => { expect(networkUtils.getCurrentUrl({ headerResults, finalUrl })).toEqual(finalUrl + '/biscuit'); }); }); + +describe('getOrInheritHeaders', () => { + it('should combine headers', () => { + const requestGroups = [{ headers: [{ name: 'foo', value: 'bar' }] }, { headers: [{ name: 'baz', value: 'qux' }] }]; + const request = { headers: [{ name: 'foo', value: 'bar' }, { name: 'baz', value: 'qux' }] }; + expect(networkUtils.getOrInheritHeaders({ request, requestGroups })).toEqual([{ name: 'baz', value: 'qux, qux' }, { name: 'foo', value: 'bar, bar' }]); + }); + it('should use last header casing', () => { + const requestGroups = [{ headers: [{ name: 'x-foo', value: 'bar' }] }]; + const request = { headers: [{ name: 'X-Foo', value: 'baz' }] }; + expect(networkUtils.getOrInheritHeaders({ request, requestGroups })).toEqual([{ name: 'X-Foo', value: 'bar, baz' }]); + }); + it('should not combine special headers', () => { + const requestGroups = [{ headers: [{ name: 'content-type', value: 'application/json' }, { name: 'Connection', value: 'close' }] }]; + const request = { headers: [{ name: 'Content-Type', value: 'text/plain' }, { name: 'connection', value: 'keep-alive' }] }; + expect(networkUtils.getOrInheritHeaders({ request, requestGroups })).toEqual([{ name: 'connection', value: 'keep-alive' }, { name: 'Content-Type', value: 'text/plain' }]); + }); +}); diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index 7b8081b3b14..c34244ec86c 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -64,26 +64,27 @@ export const getOrInheritAuthentication = ({ request, requestGroups }: { request // if no auth is specified on request or folders, default to none return { type: 'none' }; }; -export function getOrInheritHeaders({ request, requestGroups }: { request: Pick; requestGroups: RequestGroup[] }): RequestHeader[] { +export function getOrInheritHeaders({ request, requestGroups }: { request: Pick; requestGroups: Pick[] }): RequestHeader[] { const httpHeaders = new Headers(); const originalCaseMap = new Map(); - [...requestGroups - .reverse(), request] - .map(({ headers }) => headers || []) - .flat().forEach(({ name, value, disabled }) => { - if (disabled) { - return; - } - const normalizedCase = name.toLowerCase(); - // prevent multiplied headers (Content-Type, etc) - if (SINGLE_VALUE_HEADERS.includes(normalizedCase)) { - httpHeaders.set(normalizedCase, value); - } else { - // appending will join matching header values with a comma - httpHeaders.append(normalizedCase, value); - } - originalCaseMap.set(normalizedCase, name); - }); + // parent folders, then child folders, then request + const headerContexts = [...requestGroups.reverse(), request]; + const headers = headerContexts.map(({ headers }) => headers || []).flat(); + headers.forEach(({ name, value, disabled }) => { + if (disabled) { + return; + } + const normalizedCase = name.toLowerCase(); + // preserves the casing of the last header with the same name + originalCaseMap.set(normalizedCase, name); + const isStrictValueHeader = SINGLE_VALUE_HEADERS.includes(normalizedCase); + if (isStrictValueHeader) { + httpHeaders.set(normalizedCase, value); + return; + } + // appending will join matching header values with a comma + httpHeaders.append(normalizedCase, value); + }); return Array.from(httpHeaders.entries()).map(([name, value]) => ({ name: originalCaseMap.get(name)!, value })); } // (only used for getOAuth2 token) Intended to gather all required database objects and initialize ids