Skip to content

Commit 0849ad0

Browse files
committed
Handle gRPC compressed payloads + headers are sent to formatters + improve Protobuf/gRPC tests coverage
1 parent 93372c5 commit 0849ad0

File tree

11 files changed

+299
-93
lines changed

11 files changed

+299
-93
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/send/sent-response-body.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class SentResponseBodyCard extends React.Component<ExpandableCardProps &
121121
<ContentViewer
122122
contentId={message.id}
123123
editorNode={this.props.editorNode}
124-
rawContentType={lastHeader(message.headers['content-type'])}
124+
headers={message.headers}
125125
contentType={decodedContentType}
126126
expanded={!!expanded}
127127
cache={message.cache}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class HttpBodyCard extends React.Component<ExpandableCardProps & {
121121
<ContentViewer
122122
contentId={`${message.id}-${direction}`}
123123
editorNode={this.props.editorNode}
124-
rawContentType={lastHeader(message.headers['content-type'])}
124+
headers={message.headers}
125125
contentType={decodedContentType}
126126
schema={apiBodySchema}
127127
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: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const getBaseContentType = (mimeType: string | undefined) => {
2121
return type + '/' + combinedSubTypes;
2222
}
2323

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

114114
'application/grpc+proto': 'grpc-proto', // Used in GRPC requests (protobuf but with special headers)
115+
'application/grpc+protobuf': 'grpc-proto',
116+
'application/grpc-proto': 'grpc-proto',
117+
'application/grpc-protobuf': 'grpc-proto',
115118

116119
'application/octet-stream': 'raw'
117120
} as const;
@@ -180,10 +183,6 @@ export function getCompatibleTypes(
180183
types.add('xml');
181184
}
182185

183-
if (!types.has('grpc-proto') && rawContentType === 'application/grpc') {
184-
types.add('grpc-proto')
185-
}
186-
187186
if (
188187
body &&
189188
isProbablyProtobuf(body) &&
@@ -205,7 +204,7 @@ export function getCompatibleTypes(
205204
body &&
206205
body.length > 0 &&
207206
body.length % 4 === 0 && // Multiple of 4 bytes
208-
body.length < 1000 * 100 && // < 100 KB of content
207+
body.length < 100_000 && // < 100 KB of content
209208
body.every(isValidBase64Byte)
210209
) {
211210
types.add('base64');

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)