Skip to content

Commit c635fd2

Browse files
committed
Show full errors and raw encoded data on body cards, if decoding fails
1 parent a7f3f1c commit c635fd2

File tree

1 file changed

+120
-10
lines changed

1 file changed

+120
-10
lines changed

src/components/view/http/http-body-card.tsx

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import * as portals from 'react-reverse-portal';
77

88
import { ExchangeMessage, HtkResponse, HtkRequest } from '../../../types';
99
import { styled } from '../../../styles';
10+
import { WarningIcon } from '../../../icons';
11+
1012
import { lastHeader } from '../../../util';
1113
import { saveFile } from '../../../util/ui';
14+
import { ErrorLike } from '../../../util/error';
1215

1316
import { ViewableContentType, getCompatibleTypes, getContentEditorName } from '../../../model/events/content-types';
1417
import { getReadableSize } from '../../../model/events/bodies';
@@ -21,6 +24,7 @@ import { CollapsingButtons } from '../../common/collapsing-buttons';
2124
import { Pill, PillSelector } from '../../common/pill';
2225
import { ExpandShrinkButton } from '../../common/expand-shrink-button';
2326
import { IconButton } from '../../common/icon-button';
27+
import { Content, ContentMonoValue } from '../../common/text-content';
2428

2529
import { LoadingCard } from '../loading-card';
2630
import { ContentViewer } from '../../editor/content-viewer';
@@ -59,6 +63,32 @@ function getFilename(url: string, message: HtkResponse | HtkRequest): string | u
5963
if (urlBaseName?.includes(".")) return urlBaseName;
6064
}
6165

66+
const ErrorBanner = styled(Content)<{ direction: 'left' | 'right' }>`
67+
${p => p.direction === 'left'
68+
? 'margin: 0 -20px 0 -15px;'
69+
: 'margin: 0 -15px 0 -20px;'
70+
}
71+
72+
padding: 10px 30px 0;
73+
74+
font-size: ${p => p.theme.textSize};
75+
color: ${p => p.theme.mainColor};
76+
background-color: ${p => p.theme.warningBackground};
77+
border-top: solid 1px ${p => p.theme.containerBorder};
78+
79+
svg {
80+
margin-left: 0;
81+
}
82+
`;
83+
84+
const ErrorMessage = styled(ContentMonoValue)`
85+
padding: 0;
86+
margin: 10px 0;
87+
`;
88+
89+
// A selection of content types you might want to try out, to explore your encoded data:
90+
const ENCODED_DATA_CONTENT_TYPES = ['text', 'raw', 'base64', 'image'] as const;
91+
6292
@observer
6393
export class HttpBodyCard extends React.Component<{
6494
title: string,
@@ -117,13 +147,15 @@ export class HttpBodyCard extends React.Component<{
117147
lastHeader(message.headers['content-type']),
118148
message.body
119149
);
120-
const contentType = _.includes(compatibleContentTypes, this.selectedContentType) ?
121-
this.selectedContentType! : message.contentType;
150+
const decodedContentType = _.includes(compatibleContentTypes, this.selectedContentType)
151+
? this.selectedContentType!
152+
: message.contentType;
122153

123154
const decodedBody = message.body.decoded;
124155

125-
return decodedBody ?
126-
<CollapsibleCard
156+
if (decodedBody) {
157+
// We have successfully decoded the body content, show it:
158+
return <CollapsibleCard
127159
direction={direction}
128160
collapsed={collapsed}
129161
onCollapseToggled={onCollapseToggled}
@@ -154,7 +186,7 @@ export class HttpBodyCard extends React.Component<{
154186
<Pill>{ getReadableSize(decodedBody.byteLength) }</Pill>
155187
<PillSelector<ViewableContentType>
156188
onChange={this.setContentType}
157-
value={contentType}
189+
value={decodedContentType}
158190
options={compatibleContentTypes}
159191
nameFormatter={getContentEditorName}
160192
/>
@@ -167,17 +199,94 @@ export class HttpBodyCard extends React.Component<{
167199
contentId={`${message.id}-${direction}`}
168200
editorNode={this.props.editorNode}
169201
rawContentType={lastHeader(message.headers['content-type'])}
170-
contentType={contentType}
202+
contentType={decodedContentType}
171203
schema={apiBodySchema}
172204
expanded={expanded}
173205
cache={message.cache}
174206
>
175207
{decodedBody}
176208
</ContentViewer>
177209
</EditorCardContent>
178-
</CollapsibleCard>
179-
:
180-
<LoadingCard
210+
</CollapsibleCard>;
211+
} else if (!decodedBody && message.body.decodingError) {
212+
// We have failed to decode the body content! Show the error & raw encoded data instead:
213+
const error = message.body.decodingError as ErrorLike;
214+
const encodedBody = Buffer.isBuffer(message.body.encoded)
215+
? message.body.encoded
216+
: undefined;
217+
218+
const encodedDataContentType = _.includes(ENCODED_DATA_CONTENT_TYPES, this.selectedContentType)
219+
? this.selectedContentType!
220+
: 'text';
221+
222+
return <CollapsibleCard
223+
direction={direction}
224+
collapsed={collapsed}
225+
onCollapseToggled={onCollapseToggled}
226+
expanded={expanded}
227+
>
228+
<header>
229+
{ encodedBody && <>
230+
<CollapsingButtons>
231+
<ExpandShrinkButton
232+
expanded={expanded}
233+
onClick={onExpandToggled}
234+
/>
235+
<IconButton
236+
icon={['fas', 'download']}
237+
title={
238+
isPaidUser
239+
? "Save this body as a file"
240+
: "With Pro: Save this body as a file"
241+
}
242+
disabled={!isPaidUser}
243+
onClick={() => saveFile(
244+
getFilename(url, message) || "",
245+
'application/octet-stream', // Ignore content type, as it's encoded
246+
encodedBody
247+
)}
248+
/>
249+
</CollapsingButtons>
250+
<Pill>{ getReadableSize(encodedBody.byteLength) }</Pill>
251+
</> }
252+
<PillSelector<ViewableContentType>
253+
onChange={this.setContentType}
254+
value={encodedDataContentType}
255+
// A selection of maybe-useful decodings you can try out regardless:
256+
options={ENCODED_DATA_CONTENT_TYPES}
257+
nameFormatter={getContentEditorName}
258+
/>
259+
<CollapsibleCardHeading onCollapseToggled={onCollapseToggled}>
260+
{ title }
261+
</CollapsibleCardHeading>
262+
</header>
263+
<ErrorBanner direction={this.props.direction}>
264+
<p>
265+
<WarningIcon/> Body decoding failed for encoding '{message.headers['content-encoding']}' due to:
266+
</p>
267+
<ErrorMessage>{ error.code ? `${error.code}: ` : '' }{ error.message || error.toString() }</ErrorMessage>
268+
<p>
269+
This typically means either the <code>content-encoding</code> header is incorrect or unsupported, or the body
270+
was corrupted. The raw content (not decoded) is shown below.
271+
</p>
272+
</ErrorBanner>
273+
{ encodedBody &&
274+
<EditorCardContent>
275+
<ContentViewer
276+
contentId={`${message.id}-${direction}`}
277+
editorNode={this.props.editorNode}
278+
contentType={encodedDataContentType}
279+
expanded={expanded}
280+
cache={message.cache}
281+
>
282+
{ encodedBody }
283+
</ContentViewer>
284+
</EditorCardContent>
285+
}
286+
</CollapsibleCard>;
287+
} else {
288+
// No body content, but no error yet, show a loading spinner:
289+
return <LoadingCard
181290
direction={direction}
182291
collapsed={collapsed}
183292
onCollapseToggled={onCollapseToggled}
@@ -187,7 +296,7 @@ export class HttpBodyCard extends React.Component<{
187296
<header>
188297
<PillSelector<ViewableContentType>
189298
onChange={this.setContentType}
190-
value={contentType}
299+
value={decodedContentType}
191300
options={compatibleContentTypes}
192301
nameFormatter={getContentEditorName}
193302
/>
@@ -196,6 +305,7 @@ export class HttpBodyCard extends React.Component<{
196305
</CollapsibleCardHeading>
197306
</header>
198307
</LoadingCard>;
308+
}
199309
}
200310

201311
}

0 commit comments

Comments
 (0)