Skip to content

Commit 3d713b3

Browse files
committed
Handle gRPC compressed payloads + headers are sent to formatters + improve Protobuf/gRPC tests coverage
1 parent b3e28aa commit 3d713b3

File tree

12 files changed

+352
-98
lines changed

12 files changed

+352
-98
lines changed

src/components/editor/content-viewer.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { observer } from 'mobx-react';
55
import { SchemaObject } from 'openapi3-ts';
66
import * as portals from 'react-reverse-portal';
77

8+
import { Headers } from '../../types';
89
import { styled } from '../../styles';
910
import { ObservablePromise, isObservablePromise } from '../../util/observable';
1011
import { asError, unreachableCheck } from '../../util/error';
@@ -22,7 +23,7 @@ interface ContentViewerProps {
2223
children: Buffer | string;
2324
schema?: SchemaObject;
2425
expanded: boolean;
25-
rawContentType?: string;
26+
headers?: Headers;
2627
contentType: ViewableContentType;
2728
editorNode: portals.HtmlPortalNode<typeof SelfSizedEditor | typeof ContainerSizedEditor>;
2829
cache: Map<Symbol, unknown>;
@@ -199,7 +200,7 @@ export class ContentViewer extends React.Component<ContentViewerProps> {
199200
return <FormatterContainer expanded={this.props.expanded}>
200201
<formatterConfig.Component
201202
content={this.contentBuffer}
202-
rawContentType={this.props.rawContentType}
203+
headers={this.props.headers}
203204
/>
204205
</FormatterContainer>;
205206
}

src/components/editor/monaco.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async function loadMonacoEditor(retries = 5): Promise<void> {
6262
id: 'protobuf-decoding-header',
6363
command: {
6464
id: '', // No actual command defined here
65-
title: "Automatically decoded from raw Protobuf data",
65+
title: "Automatically decoded from raw Protobuf/gRPC data",
6666
},
6767
},
6868
],

src/components/send/sent-response-body.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ export class SentResponseBodyCard extends React.Component<ExpandableCardProps &
8181
? getCompatibleTypes(
8282
message.contentType,
8383
lastHeader(message.headers['content-type']),
84-
message.body
84+
message.body,
85+
message.headers,
8586
)
8687
: ['text'] as const;
8788

@@ -121,7 +122,7 @@ export class SentResponseBodyCard extends React.Component<ExpandableCardProps &
121122
<ContentViewer
122123
contentId={message.id}
123124
editorNode={this.props.editorNode}
124-
rawContentType={lastHeader(message.headers['content-type'])}
125+
headers={message.headers}
125126
contentType={decodedContentType}
126127
expanded={!!expanded}
127128
cache={message.cache}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ export class HttpBodyCard extends React.Component<ExpandableCardProps & {
8282
const compatibleContentTypes = getCompatibleTypes(
8383
message.contentType,
8484
lastHeader(message.headers['content-type']),
85-
message.body
85+
message.body,
86+
message.headers,
8687
);
8788
const decodedContentType = compatibleContentTypes.includes(this.selectedContentType!)
8889
? this.selectedContentType!
@@ -121,7 +122,7 @@ export class HttpBodyCard extends React.Component<ExpandableCardProps & {
121122
<ContentViewer
122123
contentId={`${message.id}-${direction}`}
123124
editorNode={this.props.editorNode}
124-
rawContentType={lastHeader(message.headers['content-type'])}
125+
headers={message.headers}
125126
contentType={decodedContentType}
126127
schema={apiBodySchema}
127128
expanded={!!expanded}

src/model/events/body-formatting.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Headers } from '../../types';
12
import { styled } from '../../styles';
23

34
import { ViewableContentType } from '../events/content-types';
@@ -13,12 +14,12 @@ export interface EditorFormatter {
1314
language: string;
1415
cacheKey: Symbol;
1516
isEditApplicable: boolean; // Can you apply this manually during editing to format an input?
16-
render(content: Buffer): string | ObservablePromise<string>;
17+
render(content: Buffer, headers?: Headers): string | ObservablePromise<string>;
1718
}
1819

1920
type FormatComponentProps = {
2021
content: Buffer;
21-
rawContentType: string | undefined;
22+
headers?: Headers;
2223
};
2324

2425
type FormatComponent = React.ComponentType<FormatComponentProps>;
@@ -35,17 +36,17 @@ export function isEditorFormatter(input: any): input is EditorFormatter {
3536
}
3637

3738
const buildAsyncRenderer = (formatKey: WorkerFormatterKey) =>
38-
(input: Buffer) => observablePromise(
39-
formatBufferAsync(input, formatKey)
39+
(input: Buffer, headers?: Headers) => observablePromise(
40+
formatBufferAsync(input, formatKey, headers)
4041
);
4142

4243
export const Formatters: { [key in ViewableContentType]: Formatter } = {
4344
raw: {
4445
language: 'text',
4546
cacheKey: Symbol('raw'),
4647
isEditApplicable: false,
47-
render: (input: Buffer) => {
48-
if (input.byteLength < 2000) {
48+
render: (input: Buffer, headers?: Headers) => {
49+
if (input.byteLength < 2_000) {
4950
try {
5051
// For short-ish inputs, we return synchronously - conveniently this avoids
5152
// showing the loading spinner that churns the layout in short content cases.
@@ -55,7 +56,7 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = {
5556
}
5657
} else {
5758
return observablePromise(
58-
formatBufferAsync(input, 'raw')
59+
formatBufferAsync(input, 'raw', headers)
5960
);
6061
}
6162
}
@@ -64,7 +65,7 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = {
6465
language: 'text',
6566
cacheKey: Symbol('text'),
6667
isEditApplicable: false,
67-
render: (input: Buffer) => {
68+
render: (input: Buffer, headers?: Headers) => {
6869
return bufferToString(input);
6970
}
7071
},
@@ -102,24 +103,24 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = {
102103
language: 'json',
103104
cacheKey: Symbol('json'),
104105
isEditApplicable: true,
105-
render: (input: Buffer) => {
106-
if (input.byteLength < 10000) {
106+
render: (input: Buffer, headers?: Headers) => {
107+
if (input.byteLength < 10_000) {
107108
const inputAsString = bufferToString(input);
108109

109110
try {
110111
// For short-ish inputs, we return synchronously - conveniently this avoids
111112
// showing the loading spinner that churns the layout in short content cases.
112113
return JSON.stringify(
113114
JSON.parse(inputAsString),
114-
null, 2);
115+
null, 2);
115116
// ^ Same logic as in UI-worker-formatter
116117
} catch (e) {
117118
// Fallback to showing the raw un-formatted JSON:
118119
return inputAsString;
119120
}
120121
} else {
121122
return observablePromise(
122-
formatBufferAsync(input, 'json')
123+
formatBufferAsync(input, 'json', headers)
123124
);
124125
}
125126
}

src/model/events/content-types.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import * as _ from 'lodash';
2-
import { MessageBody } from '../../types';
2+
3+
import { Headers, MessageBody } from '../../types';
34
import {
45
isProbablyProtobuf,
5-
isValidProtobuf
6+
isValidProtobuf,
7+
isProbablyGrpcProto,
8+
isValidGrpcProto,
69
} from '../../util/protobuf';
710

811
// Simplify a mime type as much as we can, without throwing any errors
@@ -21,7 +24,7 @@ export const getBaseContentType = (mimeType: string | undefined) => {
2124
return type + '/' + combinedSubTypes;
2225
}
2326

24-
// Otherwise, wr collect a list of types from most specific to most generic: [svg, xml] for image/svg+xml
27+
// Otherwise, we collect a list of types from most specific to most generic: [svg, xml] for image/svg+xml
2528
// and then look through in order to see if there are any matches here:
2629
const subTypes = combinedSubTypes.split('+');
2730
const possibleTypes = subTypes.map(st => type + '/' + st);
@@ -112,6 +115,9 @@ const mimeTypeToContentTypeMap: { [mimeType: string]: ViewableContentType } = {
112115
'application/x-protobuffer': 'protobuf', // Commonly seen in Google apps
113116

114117
'application/grpc+proto': 'grpc-proto', // Used in GRPC requests (protobuf but with special headers)
118+
'application/grpc+protobuf': 'grpc-proto',
119+
'application/grpc-proto': 'grpc-proto',
120+
'application/grpc-protobuf': 'grpc-proto',
115121

116122
'application/octet-stream': 'raw'
117123
} as const;
@@ -169,7 +175,8 @@ function isValidURLSafeBase64Byte(byte: number) {
169175
export function getCompatibleTypes(
170176
contentType: ViewableContentType,
171177
rawContentType: string | undefined,
172-
body: MessageBody | Buffer | undefined
178+
body: MessageBody | Buffer | undefined,
179+
headers?: Headers,
173180
): ViewableContentType[] {
174181
let types = new Set([contentType]);
175182

@@ -190,22 +197,29 @@ export function getCompatibleTypes(
190197
types.add('xml');
191198
}
192199

193-
if (!types.has('grpc-proto') && rawContentType === 'application/grpc') {
194-
types.add('grpc-proto')
195-
}
196-
197200
if (
198201
body &&
199-
isProbablyProtobuf(body) &&
200202
!types.has('protobuf') &&
201203
!types.has('grpc-proto') &&
204+
isProbablyProtobuf(body) &&
202205
// If it's probably unmarked protobuf, and it's a manageable size, try
203206
// parsing it just to check:
204207
(body.length < 100_000 && isValidProtobuf(body))
205208
) {
206209
types.add('protobuf');
207210
}
208211

212+
if (
213+
body &&
214+
!types.has('grpc-proto') &&
215+
isProbablyGrpcProto(body, headers ?? {}) &&
216+
// If it's probably unmarked gRPC, and it's a manageable size, try
217+
// parsing it just to check:
218+
(body.length < 100_000 && isValidGrpcProto(body, headers ?? {}))
219+
) {
220+
types.add('grpc-proto');
221+
}
222+
209223
// SVGs can always be shown as XML
210224
if (rawContentType && rawContentType.startsWith('image/svg')) {
211225
types.add('xml');

src/services/ui-worker-api.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {
2121
ParseCertResponse
2222
} from './ui-worker';
2323

24-
import { Omit } from '../types';
24+
import { Headers, Omit } from '../types';
2525
import type { ApiMetadata, ApiSpec } from '../model/api/api-interfaces';
2626
import { WorkerFormatterKey } from './ui-worker-formatters';
2727

@@ -149,10 +149,11 @@ export async function parseCert(buffer: ArrayBuffer) {
149149
})).result;
150150
}
151151

152-
export async function formatBufferAsync(buffer: ArrayBuffer, format: WorkerFormatterKey) {
152+
export async function formatBufferAsync(buffer: ArrayBuffer, format: WorkerFormatterKey, headers?: Headers) {
153153
return (await callApi<FormatRequest, FormatResponse>({
154154
type: 'format',
155155
buffer,
156-
format
156+
format,
157+
headers,
157158
})).formatted;
158159
}

src/services/ui-worker-formatters.ts

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from 'js-beautify/js/lib/beautifier';
66
import * as beautifyXml from 'xml-beautifier';
77

8+
import { Headers } from '../types';
89
import { bufferToHex, bufferToString, getReadableSize } from '../util/buffer';
910
import { parseRawProtobuf, extractProtobufFromGrpc } from '../util/protobuf';
1011

@@ -13,10 +14,25 @@ const FIVE_MB = 1024 * 1024 * 5;
1314

1415
export type WorkerFormatterKey = keyof typeof WorkerFormatters;
1516

16-
export function formatBuffer(buffer: ArrayBuffer, format: WorkerFormatterKey): string {
17-
return WorkerFormatters[format](Buffer.from(buffer));
17+
export function formatBuffer(buffer: ArrayBuffer, format: WorkerFormatterKey, headers?: Headers): string {
18+
return WorkerFormatters[format](Buffer.from(buffer), headers);
1819
}
1920

21+
const prettyProtobufView = (data: any) => JSON.stringify(data, (_key, value) => {
22+
// Buffers have toJSON defined, so arrive here in JSONified form:
23+
if (value.type === 'Buffer' && Array.isArray(value.data)) {
24+
const buffer = Buffer.from(value.data);
25+
26+
return {
27+
"Type": `Buffer (${getReadableSize(buffer)})`,
28+
"As string": bufferToString(buffer, 'detect-encoding'),
29+
"As hex": bufferToHex(buffer)
30+
}
31+
} else {
32+
return value;
33+
}
34+
}, 2);
35+
2036
// A subset of all possible formatters (those allowed by body-formatting), which require
2137
// non-trivial processing, and therefore need to be processed async.
2238
const WorkerFormatters = {
@@ -76,44 +92,15 @@ const WorkerFormatters = {
7692
});
7793
},
7894
protobuf: (content: Buffer) => {
79-
const data = parseRawProtobuf(content, {
80-
prefix: ''
81-
});
82-
83-
return JSON.stringify(data, (_key, value) => {
84-
// Buffers have toJSON defined, so arrive here in JSONified form:
85-
if (value.type === 'Buffer' && Array.isArray(value.data)) {
86-
const buffer = Buffer.from(value.data);
87-
88-
return {
89-
"Type": `Buffer (${getReadableSize(buffer)})`,
90-
"As string": bufferToString(buffer, 'detect-encoding'),
91-
"As hex": bufferToHex(buffer)
92-
}
93-
} else {
94-
return value;
95-
}
96-
}, 2);
95+
const data = parseRawProtobuf(content, { prefix: '' });
96+
return prettyProtobufView(data);
9797
},
98-
'grpc-proto': (content: Buffer) => {
99-
const protobufMessages = extractProtobufFromGrpc(content);
98+
'grpc-proto': (content: Buffer, headers?: Headers) => {
99+
const protobufMessages = extractProtobufFromGrpc(content, headers ?? {});
100100

101101
let data = protobufMessages.map((msg) => parseRawProtobuf(msg, { prefix: '' }));
102102
if (data.length === 1) data = data[0];
103103

104-
return JSON.stringify(data, (_key, value) => {
105-
// Buffers have toJSON defined, so arrive here in JSONified form:
106-
if (value.type === 'Buffer' && Array.isArray(value.data)) {
107-
const buffer = Buffer.from(value.data);
108-
109-
return {
110-
"Type": `Buffer (${getReadableSize(buffer)})`,
111-
"As string": bufferToString(buffer, 'detect-encoding'),
112-
"As hex": bufferToHex(buffer)
113-
}
114-
} else {
115-
return value;
116-
}
117-
}, 2);
104+
return prettyProtobufView(data);
118105
}
119106
} as const;

src/services/ui-worker.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from 'http-encoding';
1414
import { OpenAPIObject } from 'openapi-directory';
1515

16+
import { Headers } from '../types';
1617
import { ApiMetadata, ApiSpec } from '../model/api/api-interfaces';
1718
import { buildOpenApiMetadata, buildOpenRpcMetadata } from '../model/api/build-api-metadata';
1819
import { parseCert, ParsedCertificate, validatePKCS12, ValidationResult } from '../model/crypto';
@@ -91,6 +92,7 @@ export interface FormatRequest extends Message {
9192
type: 'format';
9293
buffer: ArrayBuffer;
9394
format: WorkerFormatterKey;
95+
headers?: Headers;
9496
}
9597

9698
export interface FormatResponse extends Message {
@@ -217,7 +219,7 @@ ctx.addEventListener('message', async (event: { data: BackgroundRequest }) => {
217219
break;
218220

219221
case 'format':
220-
const formatted = formatBuffer(event.data.buffer, event.data.format);
222+
const formatted = formatBuffer(event.data.buffer, event.data.format, event.data.headers);
221223
ctx.postMessage({ id: event.data.id, formatted });
222224
break;
223225

0 commit comments

Comments
 (0)