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=/', diff --git a/packages/insomnia/src/common/common-headers.ts b/packages/insomnia/src/common/common-headers.ts index 51e15225b6d..67ee4745ed8 100644 --- a/packages/insomnia/src/common/common-headers.ts +++ b/packages/insomnia/src/common/common-headers.ts @@ -5,6 +5,19 @@ import allEncodings from '../datasets/encodings'; import allHeaderNames from '../datasets/header-names'; import type { RequestHeader } from '../models/request'; +export const SINGLE_VALUE_HEADERS = [ + 'proxy-authorization', + 'content-length', + 'content-type', + 'content-encoding', + 'content-location', + 'connection', + 'host', + 'upgrade', + 'range', + 'trailer', +]; + export const getCommonHeaderValues = (pair: RequestHeader): any[] => { switch (pair.name.toLowerCase()) { case 'content-type': 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/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); 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 e065a8368ce..c34244ec86c 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 { @@ -63,15 +64,28 @@ 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[] { - // recurse over each parent folder to append headers - // in case of duplicate, node-libcurl joins on comma - const headers = requestGroups - .reverse() - .map(({ headers }) => headers || []) - .flat(); - // if parent has foo: bar and child has foo: baz, request will have foo: bar, baz - return [...headers, ...request.headers]; +export function getOrInheritHeaders({ request, requestGroups }: { request: Pick; requestGroups: Pick[] }): RequestHeader[] { + const httpHeaders = new Headers(); + const originalCaseMap = new Map(); + // 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 export const fetchRequestGroupData = async (requestGroupId: string) => { @@ -125,10 +139,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);