mirror of
https://github.com/Kong/insomnia.git
synced 2026-11-03 03:25:26 -05:00
* change option text * add space between icon * add option toggle prompt * fix smoke test * fix comment * update response clear UI
209 lines
7.0 KiB
TypeScript
209 lines
7.0 KiB
TypeScript
import fs from 'node:fs';
|
|
import zlib from 'node:zlib';
|
|
|
|
import type {
|
|
Compression,
|
|
McpResponse,
|
|
Response,
|
|
ResponseTimelineEntry,
|
|
RunnerTestResult,
|
|
SocketIOResponse,
|
|
WebSocketResponse,
|
|
} from 'insomnia-data';
|
|
import { database as db, models } from 'insomnia-data';
|
|
import { deserializeNDJSON } from 'insomnia-data/common';
|
|
|
|
import * as settingsService from '../settings';
|
|
|
|
const { isResponse, type: responseType } = models.response;
|
|
|
|
export async function removeResponsesForRequest(requestId: string, environmentId?: string | null) {
|
|
const settings = await settingsService.get();
|
|
const query: Record<string, any> = {
|
|
parentId: requestId,
|
|
};
|
|
|
|
// Only add if not undefined. null is not the same as undefined
|
|
// null: find responses sent from base environment
|
|
// undefined: find all responses
|
|
if (environmentId !== undefined && settings.filterResponsesByEnv) {
|
|
query.environmentId = environmentId;
|
|
}
|
|
|
|
const type = models.webSocketRequest.isWebSocketRequestId(requestId)
|
|
? models.webSocketResponse.type
|
|
: models.socketIORequest.isSocketIORequestId(requestId)
|
|
? models.socketIOResponse.type
|
|
: models.mcpRequest.isMcpRequestId(requestId)
|
|
? models.mcpResponse.type
|
|
: responseType;
|
|
|
|
if (
|
|
type === models.webSocketResponse.type ||
|
|
type === models.socketIOResponse.type ||
|
|
type === models.mcpResponse.type
|
|
) {
|
|
const toDelete = await db.find<WebSocketResponse | SocketIOResponse | McpResponse>(type, query);
|
|
for (const doc of toDelete) {
|
|
fs.promises.unlink(doc.eventLogPath);
|
|
fs.promises.unlink(doc.timelinePath);
|
|
}
|
|
} else if (type === responseType) {
|
|
const toDelete = await db.find<Response>(type, query);
|
|
for (const doc of toDelete) {
|
|
fs.promises.unlink(doc.bodyPath);
|
|
fs.promises.unlink(doc.timelinePath);
|
|
}
|
|
}
|
|
|
|
// Also delete legacy responses here or else the user will be confused as to
|
|
// why some responses are still showing in the UI.
|
|
await db.removeWhere(type, query);
|
|
}
|
|
|
|
/**
|
|
* Delete every locally stored response and collection-runner result, across every project
|
|
* and collection.
|
|
*
|
|
* Backs the global "Clear All Response History" action: these documents (and the timeline /
|
|
* event-log files they point at) can hold confidential values that were revealed while
|
|
* `hideSecretValuesInPreviewAndConsole` was off, so the on-disk files have to go too, not
|
|
* just the database rows. None of these types sync, so this only affects this machine.
|
|
*/
|
|
export async function removeAllResponseHistory() {
|
|
// Only a file that is already gone is ignored. Anything else (a locked or read-only file)
|
|
// means a confidential body or timeline is still on disk, so it has to reach the caller
|
|
// rather than be reported as a successful clear.
|
|
const unlinkIfExists = (path?: string) =>
|
|
path
|
|
? fs.promises.unlink(path).catch((err: NodeJS.ErrnoException) => {
|
|
if (err.code !== 'ENOENT') {
|
|
throw err;
|
|
}
|
|
})
|
|
: Promise.resolve();
|
|
|
|
for (const type of [
|
|
responseType,
|
|
models.webSocketResponse.type,
|
|
models.socketIOResponse.type,
|
|
models.mcpResponse.type,
|
|
models.runnerTestResult.type,
|
|
] as const) {
|
|
const docs = await db.find<Response | WebSocketResponse | SocketIOResponse | McpResponse | RunnerTestResult>(
|
|
type,
|
|
{},
|
|
);
|
|
|
|
if (docs.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
await Promise.all(
|
|
docs.map(doc => {
|
|
// Runner results hold no files of their own - they point at the responses above.
|
|
if (models.runnerTestResult.isRunnerTestResult(doc)) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return isResponse(doc)
|
|
? Promise.all([unlinkIfExists(doc.bodyPath), unlinkIfExists(doc.timelinePath)])
|
|
: Promise.all([unlinkIfExists(doc.eventLogPath), unlinkIfExists(doc.timelinePath)]);
|
|
}),
|
|
);
|
|
|
|
// None of these types have descendants, so a batch remove is enough and - unlike
|
|
// removeWhere - it does not walk the tree once per document.
|
|
await db.batchModifyDocs({ remove: docs });
|
|
}
|
|
}
|
|
|
|
export function removeResponse(response: Response | WebSocketResponse | SocketIOResponse | McpResponse) {
|
|
if (
|
|
models.webSocketResponse.isWebSocketResponse(response) ||
|
|
models.socketIOResponse.isSocketIOResponse(response) ||
|
|
models.mcpResponse.isMcpResponse(response)
|
|
) {
|
|
fs.promises.unlink(response.eventLogPath);
|
|
fs.promises.unlink(response.timelinePath);
|
|
} else if (isResponse(response)) {
|
|
fs.promises.unlink(response.bodyPath);
|
|
fs.promises.unlink(response.timelinePath);
|
|
}
|
|
return db.remove(response);
|
|
}
|
|
|
|
export const readCurlResponse = async (options: { bodyPath?: string; bodyCompression?: Compression }) => {
|
|
const readFailureMsg = '[main/curlBridgeAPI] failed to read response body message';
|
|
const bodyBufferOrErrMsg = await getResponseBodyBuffer(options, readFailureMsg);
|
|
// TODO(jackkav): simplify the fail msg and reuse in other getBodyBuffer renderer calls
|
|
|
|
if (!bodyBufferOrErrMsg) {
|
|
return { body: '', error: readFailureMsg };
|
|
} else if (typeof bodyBufferOrErrMsg === 'string') {
|
|
if (bodyBufferOrErrMsg === readFailureMsg) {
|
|
return { body: '', error: readFailureMsg };
|
|
}
|
|
return { body: '', error: `unknown error in loading response body: ${bodyBufferOrErrMsg}` };
|
|
}
|
|
|
|
return { body: bodyBufferOrErrMsg.toString('utf8'), error: '' };
|
|
};
|
|
|
|
export async function getResponseTimeline(response: Response, showBody?: boolean): Promise<ResponseTimelineEntry[]> {
|
|
const { timelinePath, bodyPath } = response;
|
|
|
|
const errorEntry: ResponseTimelineEntry[] = response.error
|
|
? [{ name: 'Text', timestamp: Date.now(), value: response.error }]
|
|
: [];
|
|
|
|
if (!timelinePath) {
|
|
return errorEntry;
|
|
}
|
|
|
|
try {
|
|
const rawBuffer = await fs.promises.readFile(timelinePath);
|
|
const timelineString = rawBuffer.toString();
|
|
const timeline = deserializeNDJSON(timelineString);
|
|
|
|
const body: ResponseTimelineEntry[] = showBody
|
|
? [
|
|
{
|
|
name: 'DataOut',
|
|
timestamp: Date.now(),
|
|
value: (await fs.promises.readFile(bodyPath)).toString(),
|
|
},
|
|
]
|
|
: [];
|
|
const output = [...timeline, ...body, ...errorEntry];
|
|
return output;
|
|
} catch (err) {
|
|
console.warn('Failed to read response body', err.message);
|
|
return errorEntry;
|
|
}
|
|
}
|
|
|
|
export const getResponseBodyBuffer = async (
|
|
response?: { bodyPath?: string; bodyCompression?: Compression },
|
|
readFailureValue?: string,
|
|
): Promise<Buffer | string> => {
|
|
if (!response?.bodyPath) {
|
|
// No body, so return empty Buffer
|
|
return Buffer.alloc(0);
|
|
}
|
|
try {
|
|
// TODO: unpick this read buffer so it can be used as a simple string reader
|
|
const rawBuffer = await fs.promises.readFile(response?.bodyPath);
|
|
if (response?.bodyCompression === 'zip') {
|
|
return new Promise((resolve, reject) =>
|
|
zlib.gunzip(rawBuffer, (err, buffer) => (err ? reject(err) : resolve(buffer))),
|
|
);
|
|
}
|
|
|
|
return rawBuffer;
|
|
} catch (err) {
|
|
console.warn('Failed to read response body', err.message);
|
|
return readFailureValue === undefined ? Buffer.alloc(0) : readFailureValue;
|
|
}
|
|
};
|