fix: support variables in websocket requests (#10067)

This commit is contained in:
Ryan Willis
2026-06-16 09:44:47 -07:00
committed by GitHub
parent 9c78ccb2a4
commit feb24e8644
15 changed files with 372 additions and 82 deletions

View File

@@ -22,7 +22,7 @@ import type {
RequestHeader,
RequestParameter,
} from 'insomnia-data';
import { services } from 'insomnia-data';
import { models, services } from 'insomnia-data';
import { v4 as uuidv4 } from 'uuid';
import { beforeEach, describe, expect, it } from 'vitest';
@@ -832,4 +832,47 @@ describe('Request Model - Comprehensive Tests', () => {
expect(deletedRequest).toBeUndefined();
});
});
describe('applyPathParametersToUrl', () => {
const { applyPathParametersToUrl } = models.request;
it('substitutes a single path parameter', () => {
expect(
applyPathParametersToUrl('ws://localhost/api/users/:id', [{ name: 'id', value: '1234' }]),
).toBe('ws://localhost/api/users/1234');
});
it('substitutes multiple path parameters', () => {
expect(
applyPathParametersToUrl('https://api.example.com/:org/:repo', [
{ name: 'org', value: 'kong' },
{ name: 'repo', value: 'insomnia' },
]),
).toBe('https://api.example.com/kong/insomnia');
});
it('URL-encodes path parameter values', () => {
expect(
applyPathParametersToUrl('https://api.example.com/users/:id', [{ name: 'id', value: 'a/b c' }]),
).toBe('https://api.example.com/users/a%2Fb%20c');
});
it('leaves unmatched :param segments unchanged', () => {
expect(
applyPathParametersToUrl('https://api.example.com/users/:id', [{ name: 'other', value: '1234' }]),
).toBe('https://api.example.com/users/:id');
});
it('leaves :param segments unchanged when value is empty', () => {
expect(
applyPathParametersToUrl('https://api.example.com/users/:id', [{ name: 'id', value: '' }]),
).toBe('https://api.example.com/users/:id');
});
it('returns url unchanged when pathParameters is empty or undefined', () => {
const url = 'https://api.example.com/users/:id';
expect(applyPathParametersToUrl(url, [])).toBe(url);
expect(applyPathParametersToUrl(url)).toBe(url);
});
});
});

View File

@@ -216,6 +216,24 @@ export interface RequestPathParameter {
export const PATH_PARAMETER_REGEX = /\/:[^/?#:]+/g;
/** Replace `:param` url segments with their URL-encoded values; unmatched or empty params are left unchanged */
export const applyPathParametersToUrl = (
url: string,
pathParameters?: RequestPathParameter[],
): string => {
if (!pathParameters?.length) {
return url;
}
return url.replace(PATH_PARAMETER_REGEX, match => {
const paramName = match.replace('/:', '');
const param = pathParameters.find(p => p.name === paramName);
if (param?.value) {
return `/${encodeURIComponent(param.value)}`;
}
return match;
});
};
export const getPathParametersFromUrl = (url: string): string[] => {
// Find all path parameters in the URL. Path parameters are defined as segments of the URL that start with a colon.
const urlPathParameters =

View File

@@ -79,6 +79,21 @@ collection:
headers:
- name: duration
value: "5"
- url: ws://localhost:4010/chat/:id
name: path-param
meta:
id: ws-req_path_param_test
created: 1666695713693
modified: 1666696265532
pathParameters:
- name: id
value: "1234"
settings:
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
cookieJar:
name: Default Jar
meta:

View File

@@ -95,6 +95,9 @@ const upgrade = (wss: WebSocketServer, request: IncomingMessage, socket: Socket,
}
return redirectOnSuccess(socket);
}
if (request.url === '/chat/1234') {
return redirectOnSuccess(socket);
}
wss.handleUpgrade(request, socket, head, ws => {
wss.emit('connection', ws, request);
});

View File

@@ -51,10 +51,18 @@ test('can make websocket connection', async ({ app, page, insomnia }) => {
await page.getByRole('tab', { name: 'Console' }).click();
await expect.soft(responseBody).toContainText('WebSocket connection established');
// Can connect with path parameters substituted in the URL
await insomnia.navigationSidebar.clickRequestOrFolder('path-param');
await expect.soft(page.locator('.app')).toContainText('ws://localhost:4010/chat/:id');
await page.click('text=Connect');
await expect.soft(statusTag).toContainText('101 Switching Protocols');
await page.getByRole('tab', { name: 'Console' }).click();
await expect.soft(responseBody).toContainText('WebSocket connection established');
const webSocketActiveConnections = page.getByTestId('WebSocketSpinner__Connected');
// Basic auth, Bearer auth, and Redirect connections are displayed as open
await expect.soft(webSocketActiveConnections).toHaveCount(3);
// Basic auth, Bearer auth, Redirect, and path-param connections are displayed as open
await expect.soft(webSocketActiveConnections).toHaveCount(4);
// Can disconnect from all connections
await page.locator('button[name="DisconnectDropdown__DropdownButton"]').click();

View File

@@ -31,7 +31,7 @@ import { setDefaultProtocol } from '../utils/url/protocol';
import { CONTENT_TYPE_GRAPHQL, JSON_ORDER_SEPARATOR } from './constants';
import { database as db } from './database';
const { PATH_PARAMETER_REGEX } = models.request;
const { applyPathParametersToUrl } = models.request;
const { isRequestGroup } = models.requestGroup;
export async function buildRenderContext({
@@ -611,22 +611,7 @@ export async function getRenderedRequestAndContext({
// Default the proto if it doesn't exist
renderedRequest.url = setDefaultProtocol(renderedRequest.url);
// Render path parameters
if (renderedRequest.pathParameters) {
// Replace path parameters in URL with their rendered values
// Path parameters are path segments that start with a colon, e.g. :id
renderedRequest.url = renderedRequest.url.replace(PATH_PARAMETER_REGEX, match => {
const paramName = match.replace('\/:', '');
const param = renderedRequest.pathParameters?.find(p => p.name === paramName);
if (param && param.value) {
// The parameter value needs to be URL encoded
return `/${encodeURIComponent(param.value)}`;
}
return match;
});
}
renderedRequest.url = applyPathParametersToUrl(renderedRequest.url, renderedRequest.pathParameters);
return {
context: renderContext,

View File

@@ -149,6 +149,7 @@ interface OpenSocketIORequestOptions {
cookieJar: CookieJar;
path?: string;
initialPayload?: string;
suppressUserAgent?: boolean;
}
const getCertificates = async ({
@@ -291,7 +292,7 @@ const openSocketIOConnection = async (
const lowerCasedEnabledHeaders = headers
.filter(({ name, disabled }) => Boolean(name) && !disabled)
.reduce(reduceArrayToLowerCaseKeyedDictionary, {});
if (!request.disableUserAgentHeader && !hasUserAgentHeader) {
if (!options.suppressUserAgent && !request.disableUserAgentHeader && !hasUserAgentHeader) {
lowerCasedEnabledHeaders['user-agent'] = `insomnia/${version}`;
}

View File

@@ -147,6 +147,7 @@ interface OpenWebSocketRequestOptions {
cookieJar: CookieJar;
initialPayload?: string;
isGraphqlSubscriptionRequest?: boolean;
suppressUserAgent?: boolean;
}
const openWebSocketConnection = async (
_event: Electron.IpcMainInvokeEvent,
@@ -234,7 +235,7 @@ const openWebSocketConnection = async (
const lowerCasedEnabledHeaders = headers
.filter(({ name, disabled }) => Boolean(name) && !disabled)
.reduce(reduceArrayToLowerCaseKeyedDictionary, {});
if (!request.disableUserAgentHeader && !hasUserAgentHeader) {
if (!options.suppressUserAgent && !request.disableUserAgentHeader && !hasUserAgentHeader) {
lowerCasedEnabledHeaders['user-agent'] = `insomnia/${version}`;
}
const settings = await services.settings.get();

View File

@@ -47,6 +47,7 @@ export async function clientAction({ params, request }: Route.ClientActionArgs)
headers: rendered.headers,
authentication: rendered.authentication,
cookieJar: rendered.cookieJar,
suppressUserAgent: rendered.suppressUserAgent,
});
window.main.trackAnalyticsEvent({
event: AnalyticsEvent.requestExecuted,
@@ -74,6 +75,7 @@ export async function clientAction({ params, request }: Route.ClientActionArgs)
}),
authentication: rendered.authentication,
cookieJar: rendered.cookieJar,
suppressUserAgent: rendered.suppressUserAgent,
});
window.main.trackAnalyticsEvent({
event: AnalyticsEvent.requestExecuted,
@@ -108,6 +110,7 @@ export async function clientAction({ params, request }: Route.ClientActionArgs)
authentication: rendered.authentication,
query: rendered.query || {},
path: rendered.path,
suppressUserAgent: rendered.suppressUserAgent,
});
window.main.trackAnalyticsEvent({
event: AnalyticsEvent.requestExecuted,

View File

@@ -85,22 +85,7 @@ export const RenderedQueryString: FC<Props> = ({ request }) => {
}
const { parameters, pathParameters, authQueryParams: renderedAuthQueryParams } = result;
let { url } = result;
if (pathParameters) {
// Replace path parameters in URL with their rendered values
// Path parameters are path segments that start with a colon, e.g. :id
url = url.replace(models.request.PATH_PARAMETER_REGEX, match => {
const pathParam = match.replace('/:', '');
const param = pathParameters?.find(p => p.name === pathParam);
if (param && param.value) {
return `/${encodeURIComponent(param.value)}`;
}
// The parameter should also be URL encoded
return match;
});
}
const url = models.request.applyPathParametersToUrl(result.url, pathParameters);
const mergedParams = [...parameters, ...renderedAuthQueryParams];
const qs = buildQueryStringFromParams(mergedParams, false, { encodeParams: request.settingEncodeUrl });

View File

@@ -1,4 +1,3 @@
import type { Request, RequestGroup } from 'insomnia-data';
import { models, services } from 'insomnia-data';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Button, Link } from 'react-aria-components';
@@ -19,15 +18,13 @@ import { OneLineEditor, type OneLineEditorHandle } from '~/ui/components/.client
import { showSettingsModal } from '~/ui/components/modals/settings-modal';
import { recordProjectRecentRequest } from '~/ui/utils/recent-project-requests';
import { database as db } from '../../common/database';
import { getOrInheritAuthentication, getOrInheritHeaders } from '../../network/network';
import { useWorkspaceLoaderData } from '../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
import {
type RequestLoaderData,
useRequestLoaderData,
} from '../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { AnalyticsEvent } from '../../ui/analytics';
import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../utils/try-interpolate';
import { renderRealtimeConnectPayload } from '../../utils/render-realtime-connect';
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../utils/url/querystring';
import { useInsomniaTabContext } from '../context/app/insomnia-tab-context';
import { useReadyState } from '../hooks/use-ready-state';
@@ -43,7 +40,6 @@ import { InputVaultKeyModal } from './modals/input-vault-key-modal';
import { PromptModal } from './modals/prompt-modal';
import { VariableMissingErrorModal } from './modals/variable-missing-error-modal';
const { isRequestGroup } = models.requestGroup;
const { isEventStreamRequest, isGraphqlSubscriptionRequest } = models.request;
interface Props {
handleAutocompleteUrls: () => Promise<string[]>;
@@ -194,24 +190,10 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(
const startListening = async () => {
const environmentId = activeEnvironment._id;
const workspaceId = activeWorkspace._id;
// Render any Liquid template tags in the url/headers/authentication settings/cookies
const workspaceCookieJar = await services.cookieJar.getOrCreateForParentId(workspaceId);
const ancestors = await db.withAncestors<Request | RequestGroup>(activeRequest, [models.requestGroup.type]);
// check for authentication overrides in parent folders
const requestGroups = ancestors.filter(isRequestGroup) as RequestGroup[];
activeRequest.authentication = getOrInheritAuthentication({ request: activeRequest, requestGroups });
activeRequest.headers = getOrInheritHeaders({ request: activeRequest, requestGroups });
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({
const rendered = await renderRealtimeConnectPayload({
request: activeRequest,
environmentId,
payload: {
url: activeRequest.url,
headers: activeRequest.headers,
authentication: activeRequest.authentication,
parameters: activeRequest.parameters.filter(p => !p.disabled),
workspaceCookieJar,
},
workspaceId,
});
rendered &&
connect({

View File

@@ -1,5 +1,4 @@
import type { SocketIORequest, WebSocketRequest } from 'insomnia-data';
import { services } from 'insomnia-data';
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef } from 'react';
import { useParams } from 'react-router';
@@ -10,7 +9,7 @@ import {
import { OneLineEditor, type OneLineEditorHandle } from '~/ui/components/.client/codemirror/one-line-editor';
import { recordProjectRecentRequest } from '~/ui/utils/recent-project-requests';
import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../../utils/try-interpolate';
import { renderRealtimeConnectPayload } from '../../../utils/render-realtime-connect';
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context';
import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydown-binder';
@@ -60,20 +59,10 @@ export const WebSocketActionBar = forwardRef<WebSocketActionBarHandle, ActionBar
);
const generateConnectParams = useCallback(async () => {
// Render any Liquid template tags in the url/headers/authentication settings/cookies
const workspaceCookieJar = await services.cookieJar.getOrCreateForParentId(workspaceId);
// Render any Liquid template tags in the url/headers/authentication settings/cookies
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({
const rendered = await renderRealtimeConnectPayload({
request,
environmentId,
payload: {
url: request.url,
headers: request.headers,
authentication: request.authentication,
parameters: request.parameters.filter(p => !p.disabled),
workspaceCookieJar,
},
workspaceId,
});
if (request.type === 'WebSocketRequest' && rendered) {
return {

View File

@@ -19,6 +19,7 @@ import {
type WebSocketRequestLoaderData,
} from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { RenderError } from '../../../templating/render-error';
import { renderRealtimeConnectPayload } from '../../../utils/render-realtime-connect';
import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../../utils/try-interpolate';
import {
buildQueryStringFromParams,
@@ -89,18 +90,14 @@ const WebSocketRequestForm: FC<FormProps> = ({ request, previewMode, environment
const renderedMessage = await tryToInterpolateRequestOrShowRenderErrorModal({ request, environmentId, payload });
const readyState = await window.main.webSocket.readyState.getCurrent({ requestId: request._id });
if (!readyState) {
const workspaceCookieJar = await services.cookieJar.getOrCreateForParentId(workspaceId);
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({
const rendered = await renderRealtimeConnectPayload({
request,
environmentId,
payload: {
url: request.url,
headers: request.headers,
authentication: request.authentication,
parameters: request.parameters.filter(p => !p.disabled),
workspaceCookieJar,
},
workspaceId,
});
if (!rendered) {
return;
}
window.main.webSocket.open({
requestId: request._id,
workspaceId,
@@ -109,6 +106,7 @@ const WebSocketRequestForm: FC<FormProps> = ({ request, previewMode, environment
authentication: rendered.authentication,
cookieJar: rendered.workspaceCookieJar,
initialPayload: renderedMessage,
suppressUserAgent: rendered.suppressUserAgent,
});
return;
}

View File

@@ -0,0 +1,187 @@
import type { WebSocketRequest } from 'insomnia-data';
import { services } from 'insomnia-data';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { database as db } from '../common/database';
import { renderRealtimeConnectPayload } from './render-realtime-connect';
import type * as TryInterpolate from './try-interpolate';
const { tryToInterpolateRequestOrShowRenderErrorModal } = vi.hoisted(() => ({
tryToInterpolateRequestOrShowRenderErrorModal: vi.fn(),
}));
vi.mock('./try-interpolate', () => ({
tryToInterpolateRequestOrShowRenderErrorModal,
}));
vi.mock('../ui/components/modals', () => ({
showModal: vi.fn(),
}));
describe('renderRealtimeConnectPayload', () => {
let workspaceId: string;
let environmentId: string;
let request: WebSocketRequest;
const mockRendered = async (overrides: Record<string, unknown> = {}) => {
tryToInterpolateRequestOrShowRenderErrorModal.mockResolvedValue({
url: 'ws://localhost/api/chat/1234',
headers: [],
authentication: { type: 'none' },
parameters: [],
pathParameters: [{ name: 'id', value: '1234' }],
workspaceCookieJar: await services.cookieJar.getOrCreateForParentId(workspaceId),
...overrides,
});
};
beforeEach(async () => {
vi.clearAllMocks();
await db.init({ inMemoryOnly: true }, true);
const project = await services.project.create({
_id: 'proj_render_connect',
name: 'Render Connect Project',
});
const workspace = await services.workspace.create({
_id: 'wrk_render_connect',
name: 'Render Connect Workspace',
parentId: project._id,
scope: 'collection',
});
workspaceId = workspace._id;
const environment = await services.environment.getOrCreateForParentId(workspaceId);
environmentId = environment._id;
request = await services.webSocketRequest.create({
_id: 'ws-req_render_connect',
name: 'WS Path Param',
parentId: workspaceId,
url: 'ws://localhost/api/chat/:id',
pathParameters: [{ name: 'id', value: '1234' }],
metaSortKey: 0,
});
});
it('substitutes path parameters in the rendered url', async () => {
await mockRendered({ url: 'ws://localhost/api/chat/:id' });
const result = await renderRealtimeConnectPayload({ request, environmentId, workspaceId });
expect(result?.url).toBe('ws://localhost/api/chat/1234');
});
it('uses inherited folder auth and headers in the render payload', async () => {
const folder = await services.requestGroup.create({
_id: 'fld_render_connect',
name: 'Folder',
parentId: workspaceId,
metaSortKey: 0,
authentication: { type: 'bearer', token: 'folder-token', disabled: false },
headers: [{ name: 'X-Folder-Header', value: 'folder-value' }],
});
request = await services.webSocketRequest.update(request, { parentId: folder._id });
await mockRendered();
await renderRealtimeConnectPayload({ request, environmentId, workspaceId });
expect(tryToInterpolateRequestOrShowRenderErrorModal).toHaveBeenCalledWith(
expect.objectContaining({
payload: expect.objectContaining({
authentication: expect.objectContaining({ type: 'bearer', token: 'folder-token' }),
headers: expect.arrayContaining([expect.objectContaining({ name: 'X-Folder-Header' })]),
}),
}),
);
});
it('returns undefined when rendering fails', async () => {
tryToInterpolateRequestOrShowRenderErrorModal.mockImplementation(async () => {});
const result = await renderRealtimeConnectPayload({ request, environmentId, workspaceId });
expect(result).toBeUndefined();
});
it('computes suppressUserAgent from request settings', async () => {
request = await services.webSocketRequest.update(request, { disableUserAgentHeader: true });
await mockRendered();
const result = await renderRealtimeConnectPayload({ request, environmentId, workspaceId });
expect(result?.suppressUserAgent).toBe(true);
});
it('suppresses the user agent when all user-agent headers are disabled', async () => {
request = await services.webSocketRequest.update(request, {
headers: [{ name: 'User-Agent', value: 'custom-agent', disabled: true }],
});
await mockRendered();
const result = await renderRealtimeConnectPayload({ request, environmentId, workspaceId });
expect(result?.suppressUserAgent).toBe(true);
});
it('does not suppress the user agent when an enabled user-agent header exists on a parent folder', async () => {
const folder = await services.requestGroup.create({
_id: 'fld_render_connect_ua',
name: 'UA Folder',
parentId: workspaceId,
metaSortKey: 0,
headers: [{ name: 'User-Agent', value: 'folder-agent' }],
});
request = await services.webSocketRequest.update(request, {
parentId: folder._id,
headers: [{ name: 'User-Agent', value: 'custom-agent', disabled: true }],
});
await mockRendered({ headers: [{ name: 'User-Agent', value: 'folder-agent' }] });
const result = await renderRealtimeConnectPayload({ request, environmentId, workspaceId });
expect(result?.suppressUserAgent).toBe(false);
});
});
describe('renderRealtimeConnectPayload integration', () => {
beforeEach(async () => {
await db.init({ inMemoryOnly: true }, true);
});
it('templates path parameter values from environment variables', async () => {
const actual = await vi.importActual<typeof TryInterpolate>('./try-interpolate');
tryToInterpolateRequestOrShowRenderErrorModal.mockImplementation(
actual.tryToInterpolateRequestOrShowRenderErrorModal,
);
const project = await services.project.create({
_id: 'proj_render_connect_env',
name: 'Render Connect Env Project',
});
const workspace = await services.workspace.create({
_id: 'wrk_render_connect_env',
name: 'Render Connect Env Workspace',
parentId: project._id,
scope: 'collection',
});
const environment = await services.environment.getOrCreateForParentId(workspace._id);
await services.environment.update(environment, { data: { chatId: '5678' } });
const request = await services.webSocketRequest.create({
_id: 'ws-req_render_connect_env',
name: 'WS Env Path Param',
parentId: workspace._id,
url: 'ws://localhost/api/chat/:id',
pathParameters: [{ name: 'id', value: '{{ _.chatId }}' }],
metaSortKey: 0,
});
const result = await renderRealtimeConnectPayload({
request,
environmentId: environment._id,
workspaceId: workspace._id,
});
expect(result?.url).toBe('ws://localhost/api/chat/5678');
});
});

View File

@@ -0,0 +1,72 @@
import type { CookieJar, Request, RequestAuthentication, RequestGroup, RequestHeader, RequestParameter, SocketIORequest, WebSocketRequest } from 'insomnia-data';
import { models, services } from 'insomnia-data';
import { database as db } from '../common/database';
import { getOrInheritAuthentication, getOrInheritHeaders } from '../network/network';
import { tryToInterpolateRequestOrShowRenderErrorModal } from './try-interpolate';
const { applyPathParametersToUrl } = models.request;
const { isRequestGroup, type: requestGroupType } = models.requestGroup;
export interface RenderedRealtimeConnectPayload {
/** rendered url with path parameters applied */
url: string;
headers: RequestHeader[];
authentication: RequestAuthentication;
parameters: RequestParameter[];
workspaceCookieJar: CookieJar;
suppressUserAgent: boolean;
}
export async function renderRealtimeConnectPayload({
request,
environmentId,
workspaceId,
}: {
request: WebSocketRequest | SocketIORequest | Request;
environmentId: string;
workspaceId: string;
}): Promise<RenderedRealtimeConnectPayload | undefined> {
const workspaceCookieJar = await services.cookieJar.getOrCreateForParentId(workspaceId);
const ancestors = await db.withAncestors<Request | WebSocketRequest | SocketIORequest | RequestGroup>(request, [
requestGroupType,
]);
const requestGroups = ancestors.filter(isRequestGroup);
const headers = getOrInheritHeaders({ request, requestGroups });
const authentication = getOrInheritAuthentication({ request, requestGroups });
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({
request,
environmentId,
payload: {
url: request.url,
headers,
authentication,
parameters: request.parameters.filter(p => !p.disabled),
pathParameters: request.pathParameters,
workspaceCookieJar,
},
});
if (!rendered) {
return undefined;
}
// getOrInheritHeaders drops disabled headers, so disabled User-Agent headers are only visible on the raw request/folder headers
const userAgentHeaders = [...requestGroups, request]
.flatMap(doc => doc.headers || [])
.filter(h => h.name?.toLowerCase() === 'user-agent');
const suppressUserAgent =
Boolean(request.disableUserAgentHeader) || (userAgentHeaders.length > 0 && userAgentHeaders.every(h => h.disabled));
const url = applyPathParametersToUrl(rendered.url, rendered.pathParameters);
return {
url,
headers: rendered.headers,
authentication: rendered.authentication,
parameters: rendered.parameters,
workspaceCookieJar: rendered.workspaceCookieJar,
suppressUserAgent,
};
}