From 41a2fd6379642f3224e40f81cdeec20549fc999e Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Thu, 24 Feb 2022 14:38:38 +0000 Subject: [PATCH] Update response-timeline-viewer to show response body. (#4346) Co-authored-by: Dimitri Mitropoulos --- packages/insomnia-app/app/common/misc.ts | 11 +++++ packages/insomnia-app/app/models/response.ts | 45 +++++++++++-------- packages/insomnia-app/app/network/network.ts | 29 ++++-------- .../components/editors/auth/o-auth-2-auth.tsx | 12 ++--- .../modals/response-debug-modal.tsx | 8 +++- .../app/ui/components/panes/response-pane.tsx | 2 +- .../viewers/response-timeline-viewer.tsx | 28 +++++++----- 7 files changed, 77 insertions(+), 58 deletions(-) diff --git a/packages/insomnia-app/app/common/misc.ts b/packages/insomnia-app/app/common/misc.ts index 966979097..0a4d4160a 100644 --- a/packages/insomnia-app/app/common/misc.ts +++ b/packages/insomnia-app/app/common/misc.ts @@ -466,3 +466,14 @@ export function isNotNullOrUndefined( } export const kebabCase = (value: string) => value.replace(/ /g, '-'); + +// Because node-libcurl changed some names that we used in the timeline +export const LIBCURL_DEBUG_MIGRATION_MAP = { + HeaderIn: 'HEADER_IN', + DataIn: 'DATA_IN', + SslDataIn: 'SSL_DATA_IN', + HeaderOut: 'HEADER_OUT', + DataOut: 'DATA_OUT', + SslDataOut: 'SSL_DATA_OUT', + Text: 'TEXT', +} as const; diff --git a/packages/insomnia-app/app/models/response.ts b/packages/insomnia-app/app/models/response.ts index 5aafe66c4..1b217a38d 100644 --- a/packages/insomnia-app/app/models/response.ts +++ b/packages/insomnia-app/app/models/response.ts @@ -3,10 +3,12 @@ import fs from 'fs'; import mkdirp from 'mkdirp'; import path from 'path'; import { Readable } from 'stream'; +import { ValueOf } from 'type-fest'; import zlib from 'zlib'; import { database as db, Query } from '../common/database'; import { getDataDirectory } from '../common/electron-helpers'; +import { LIBCURL_DEBUG_MIGRATION_MAP } from '../common/misc'; import type { BaseModel } from './index'; import * as models from './index'; @@ -26,7 +28,7 @@ export interface ResponseHeader { } export interface ResponseTimelineEntry { - name: string; + name: ValueOf; timestamp: number; value: string; } @@ -228,8 +230,30 @@ export const getBodyBuffer = ( readFailureValue, ); -export function getTimeline(response: Response) { - return getTimelineFromPath(response.timelinePath || ''); +export function getTimeline(response: Response, showBody?: boolean) { + const { timelinePath, bodyPath } = response; + + // No body, so return empty Buffer + if (!timelinePath) { + return []; + } + + try { + const rawBuffer = fs.readFileSync(timelinePath); + const timeline = JSON.parse(rawBuffer.toString()) as ResponseTimelineEntry[]; + const body: ResponseTimelineEntry[] = showBody ? [ + { + name: LIBCURL_DEBUG_MIGRATION_MAP.DataOut, + timestamp: Date.now(), + value: fs.readFileSync(bodyPath).toString(), + }, + ] : []; + const output = [...timeline, ...body]; + return output; + } catch (err) { + console.warn('Failed to read response body', err.message); + return []; + } } function getBodyStreamFromPath( @@ -282,21 +306,6 @@ function getBodyBufferFromPath( } } -function getTimelineFromPath(timelinePath: string) { - // No body, so return empty Buffer - if (!timelinePath) { - return []; - } - - try { - const rawBuffer = fs.readFileSync(timelinePath); - return JSON.parse(rawBuffer.toString()) as ResponseTimelineEntry[]; - } catch (err) { - console.warn('Failed to read response body', err.message); - return []; - } -} - async function migrateBodyToFileSystem(doc: Response) { if (doc.hasOwnProperty('body') && doc._id && !doc.bodyPath) { // @ts-expect-error -- TSCONVERSION previously doc.body and doc.encoding did exist but are now removed, and if they exist we want to migrate away from them diff --git a/packages/insomnia-app/app/network/network.ts b/packages/insomnia-app/app/network/network.ts index 297337d33..0c66c3dbc 100644 --- a/packages/insomnia-app/app/network/network.ts +++ b/packages/insomnia-app/app/network/network.ts @@ -48,6 +48,7 @@ import { hasAuthHeader, hasContentTypeHeader, hasUserAgentHeader, + LIBCURL_DEBUG_MIGRATION_MAP, waitForStreamToFinish, } from '../common/misc'; import type { ExtraRenderInfo, RenderedRequest } from '../common/render'; @@ -96,18 +97,6 @@ const MAX_DELAY_TIME = 1000; // Special header value that will prevent the header being sent const DISABLE_HEADER_VALUE = '__Di$aB13d__'; -// Because node-libcurl changed some names that we used in the timeline -const LIBCURL_DEBUG_MIGRATION_MAP = { - HeaderIn: 'HEADER_IN', - DataIn: 'DATA_IN', - SslDataIn: 'SSL_DATA_IN', - HeaderOut: 'HEADER_OUT', - DataOut: 'DATA_OUT', - SslDataOut: 'SSL_DATA_OUT', - Text: 'TEXT', - '': '', -}; - const cancelRequestFunctionMap = {}; let lastUserInteraction = Date.now(); @@ -163,17 +152,15 @@ export async function _actuallySend( return new Promise(async resolve => { const timeline: ResponseTimelineEntry[] = []; - function addTimeline(name, value) { + const addTimelineItem = (name: ResponseTimelineEntry['name']) => (value: string) => { timeline.push({ name, value, timestamp: Date.now(), }); - } + }; - function addTimelineText(value) { - addTimeline('TEXT', value); - } + const addTimelineText = addTimelineItem(LIBCURL_DEBUG_MIGRATION_MAP.Text); // Initialize the curl handle const curl = new Curl(); @@ -310,6 +297,7 @@ export async function _actuallySend( const content = contentBuffer.toString('utf8'); const rawName = Object.keys(CurlInfoDebug).find(k => CurlInfoDebug[k] === infoType) || ''; const name = LIBCURL_DEBUG_MIGRATION_MAP[rawName] || rawName; + const addToTimeline = addTimelineItem(name); if (infoType === CurlInfoDebug.SslDataIn || infoType === CurlInfoDebug.SslDataOut) { return 0; @@ -320,9 +308,9 @@ export async function _actuallySend( if (contentBuffer.length === 0) { // Sometimes this happens, but I'm not sure why. Just ignore it. } else if (contentBuffer.length / 1024 < settings.maxTimelineDataSizeKB) { - addTimeline(name, content); + addToTimeline(content); } else { - addTimeline(name, `(${describeByteSize(contentBuffer.length)} hidden)`); + addToTimeline(`(${describeByteSize(contentBuffer.length)} hidden)`); } return 0; @@ -338,9 +326,10 @@ export async function _actuallySend( return 0; } - addTimeline(name, content); + addToTimeline(content); return 0; // Must be here }); + // Set the headers (to be modified as we go) const headers = clone(renderedRequest.headers); // Set the URL, including the query parameters diff --git a/packages/insomnia-app/app/ui/components/editors/auth/o-auth-2-auth.tsx b/packages/insomnia-app/app/ui/components/editors/auth/o-auth-2-auth.tsx index 4d0f01a32..84524238f 100644 --- a/packages/insomnia-app/app/ui/components/editors/auth/o-auth-2-auth.tsx +++ b/packages/insomnia-app/app/ui/components/editors/auth/o-auth-2-auth.tsx @@ -1,3 +1,4 @@ +import { Button } from 'insomnia-components'; import React, { ChangeEvent, FC, ReactNode, useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; @@ -378,17 +379,18 @@ const OAuth2Error: FC = () => { showModal(ResponseDebugModal, { responseId: token.xResponseId, + showBody: true, }); }; const debugButton = token?.xResponseId ? ( - + Response Timeline + ) : null; const errorUriButton = token?.errorUri ? ( @@ -407,8 +409,8 @@ const OAuth2Error: FC = () => {

{errorDescription || 'no description provided'} {errorUriButton} - {debugButton}

+ {debugButton} ); } diff --git a/packages/insomnia-app/app/ui/components/modals/response-debug-modal.tsx b/packages/insomnia-app/app/ui/components/modals/response-debug-modal.tsx index 4ed28ecc0..6132af801 100644 --- a/packages/insomnia-app/app/ui/components/modals/response-debug-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/response-debug-modal.tsx @@ -11,6 +11,7 @@ import { ModalHeader } from '../base/modal-header'; interface State { response: Response | null; + showBody?: boolean; title: string | null; } @@ -20,6 +21,7 @@ export class ResponseDebugModal extends PureComponent<{}, State> { state: State = { response: null, + showBody: false, title: '', }; @@ -31,19 +33,20 @@ export class ResponseDebugModal extends PureComponent<{}, State> { this.modal?.hide(); } - async show(options: { responseId?: string; response?: Response; title?: string }) { + async show(options: { responseId?: string; response?: Response; title?: string; showBody?: boolean }) { const response = options.response ? options.response : await models.response.getById(options.responseId || 'n/a'); this.setState({ response, title: options.title || null, + showBody: options.showBody, }); this.modal?.show(); } render() { - const { response, title } = this.state; + const { response, title, showBody } = this.state; return ( {title || 'Response Timeline'} @@ -57,6 +60,7 @@ export class ResponseDebugModal extends PureComponent<{}, State> { {response ? ( ) : (
No response found
diff --git a/packages/insomnia-app/app/ui/components/panes/response-pane.tsx b/packages/insomnia-app/app/ui/components/panes/response-pane.tsx index fee78edad..bb465f138 100644 --- a/packages/insomnia-app/app/ui/components/panes/response-pane.tsx +++ b/packages/insomnia-app/app/ui/components/panes/response-pane.tsx @@ -134,7 +134,7 @@ export class ResponsePane extends PureComponent { return; } - const timeline = await models.response.getTimeline(response); + const timeline = models.response.getTimeline(response); const headers = timeline .filter(v => v.name === 'HEADER_IN') .map(v => v.value) diff --git a/packages/insomnia-app/app/ui/components/viewers/response-timeline-viewer.tsx b/packages/insomnia-app/app/ui/components/viewers/response-timeline-viewer.tsx index f91ec233c..fcd1b1fda 100644 --- a/packages/insomnia-app/app/ui/components/viewers/response-timeline-viewer.tsx +++ b/packages/insomnia-app/app/ui/components/viewers/response-timeline-viewer.tsx @@ -1,16 +1,18 @@ import React, { PureComponent } from 'react'; import { clickLink } from '../../../common/electron-helpers'; +import { LIBCURL_DEBUG_MIGRATION_MAP } from '../../../common/misc'; import * as models from '../../../models'; -import { Response } from '../../../models/response'; +import { Response, ResponseTimelineEntry } from '../../../models/response'; import { CodeEditor } from '../codemirror/code-editor'; interface Props { + showBody?: boolean; response: Response; } interface State { - timeline: any[]; + timeline: ResponseTimelineEntry[]; timelineKey: string; } @@ -33,45 +35,46 @@ export class ResponseTimelineViewer extends PureComponent { } async refreshTimeline() { - const { response } = this.props; - const timeline = await models.response.getTimeline(response); + const { response, showBody } = this.props; + const timeline = models.response.getTimeline(response, showBody); + this.setState({ timeline, timelineKey: response._id, }); } - renderRow(row, i, all) { + renderRow(row: ResponseTimelineEntry, i: number, all: ResponseTimelineEntry[]) { const { name, value } = row; const previousName = i > 0 ? all[i - 1].name : ''; let prefix: string | null = null; switch (name) { - case 'HEADER_IN': + case LIBCURL_DEBUG_MIGRATION_MAP.HeaderIn: prefix = '< '; break; - case 'DATA_IN': + case LIBCURL_DEBUG_MIGRATION_MAP.DataIn: prefix = '| '; break; - case 'SSL_DATA_IN': + case LIBCURL_DEBUG_MIGRATION_MAP.SslDataIn: prefix = '<< '; break; - case 'HEADER_OUT': + case LIBCURL_DEBUG_MIGRATION_MAP.HeaderOut: prefix = '> '; break; - case 'DATA_OUT': + case LIBCURL_DEBUG_MIGRATION_MAP.DataOut: prefix = '| '; break; - case 'SSL_DATA_OUT': + case LIBCURL_DEBUG_MIGRATION_MAP.SslDataOut: prefix = '>> '; break; - case 'TEXT': + case LIBCURL_DEBUG_MIGRATION_MAP.Text: prefix = '* '; break; @@ -99,6 +102,7 @@ export class ResponseTimelineViewer extends PureComponent { .filter(r => r !== null) .join('\n') .trim(); + return (