@@ -7,8 +7,11 @@ import * as portals from 'react-reverse-portal';
7
7
8
8
import { ExchangeMessage , HtkResponse , HtkRequest } from '../../../types' ;
9
9
import { styled } from '../../../styles' ;
10
+ import { WarningIcon } from '../../../icons' ;
11
+
10
12
import { lastHeader } from '../../../util' ;
11
13
import { saveFile } from '../../../util/ui' ;
14
+ import { ErrorLike } from '../../../util/error' ;
12
15
13
16
import { ViewableContentType , getCompatibleTypes , getContentEditorName } from '../../../model/events/content-types' ;
14
17
import { getReadableSize } from '../../../model/events/bodies' ;
@@ -21,6 +24,7 @@ import { CollapsingButtons } from '../../common/collapsing-buttons';
21
24
import { Pill , PillSelector } from '../../common/pill' ;
22
25
import { ExpandShrinkButton } from '../../common/expand-shrink-button' ;
23
26
import { IconButton } from '../../common/icon-button' ;
27
+ import { Content , ContentMonoValue } from '../../common/text-content' ;
24
28
25
29
import { LoadingCard } from '../loading-card' ;
26
30
import { ContentViewer } from '../../editor/content-viewer' ;
@@ -59,6 +63,32 @@ function getFilename(url: string, message: HtkResponse | HtkRequest): string | u
59
63
if ( urlBaseName ?. includes ( "." ) ) return urlBaseName ;
60
64
}
61
65
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
+
62
92
@observer
63
93
export class HttpBodyCard extends React . Component < {
64
94
title : string ,
@@ -117,13 +147,15 @@ export class HttpBodyCard extends React.Component<{
117
147
lastHeader ( message . headers [ 'content-type' ] ) ,
118
148
message . body
119
149
) ;
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 ;
122
153
123
154
const decodedBody = message . body . decoded ;
124
155
125
- return decodedBody ?
126
- < CollapsibleCard
156
+ if ( decodedBody ) {
157
+ // We have successfully decoded the body content, show it:
158
+ return < CollapsibleCard
127
159
direction = { direction }
128
160
collapsed = { collapsed }
129
161
onCollapseToggled = { onCollapseToggled }
@@ -154,7 +186,7 @@ export class HttpBodyCard extends React.Component<{
154
186
< Pill > { getReadableSize ( decodedBody . byteLength ) } </ Pill >
155
187
< PillSelector < ViewableContentType >
156
188
onChange = { this . setContentType }
157
- value = { contentType }
189
+ value = { decodedContentType }
158
190
options = { compatibleContentTypes }
159
191
nameFormatter = { getContentEditorName }
160
192
/>
@@ -167,17 +199,94 @@ export class HttpBodyCard extends React.Component<{
167
199
contentId = { `${ message . id } -${ direction } ` }
168
200
editorNode = { this . props . editorNode }
169
201
rawContentType = { lastHeader ( message . headers [ 'content-type' ] ) }
170
- contentType = { contentType }
202
+ contentType = { decodedContentType }
171
203
schema = { apiBodySchema }
172
204
expanded = { expanded }
173
205
cache = { message . cache }
174
206
>
175
207
{ decodedBody }
176
208
</ ContentViewer >
177
209
</ 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
181
290
direction = { direction }
182
291
collapsed = { collapsed }
183
292
onCollapseToggled = { onCollapseToggled }
@@ -187,7 +296,7 @@ export class HttpBodyCard extends React.Component<{
187
296
< header >
188
297
< PillSelector < ViewableContentType >
189
298
onChange = { this . setContentType }
190
- value = { contentType }
299
+ value = { decodedContentType }
191
300
options = { compatibleContentTypes }
192
301
nameFormatter = { getContentEditorName }
193
302
/>
@@ -196,6 +305,7 @@ export class HttpBodyCard extends React.Component<{
196
305
</ CollapsibleCardHeading >
197
306
</ header >
198
307
</ LoadingCard > ;
308
+ }
199
309
}
200
310
201
311
}
0 commit comments