Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/grpcweb-transport/spec/grpc-web-format.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,22 @@ describe('readGrpcWebResponse', () => {
expect(actual).toEqual([GrpcStatusCode.OK, 'ok', { foo: 'bar, baz, qux'}]);
});

it('does not decode percent-encoded grpc-message by default', function () {
const actual = readGrpcWebResponseHeader({ 'grpc-status': '2', 'grpc-message': 'error%20message' }, 200, '');
expect(actual).toEqual([GrpcStatusCode.UNKNOWN, 'error%20message', {}]);
});

it('decodes percent-encoded grpc-message when decodeMessage is true', function () {
const actual = readGrpcWebResponseHeader({ 'grpc-status': '2', 'grpc-message': 'error%20message%3A%20something%20failed' }, 200, '', true);
expect(actual).toEqual([GrpcStatusCode.UNKNOWN, 'error message: something failed', {}]);
});

it('falls back to raw value if grpc-message percent-decoding fails', function () {
// Invalid percent-encoding (% followed by non-hex chars)
const actual = readGrpcWebResponseHeader({ 'grpc-status': '2', 'grpc-message': 'invalid%ZZencoding' }, 200, '', true);
expect(actual).toEqual([GrpcStatusCode.UNKNOWN, 'invalid%ZZencoding', {}]);
});

it('translates non-OK HTTP responses into gRPC status and message', function () {
const statusMap = new Map([
[400, GrpcStatusCode.INVALID_ARGUMENT],
Expand Down Expand Up @@ -408,6 +424,30 @@ describe('readGrpcWebResponse', () => {
expect(trailer).toEqual([GrpcStatusCode.OK, message, {}])
});

it('does not decode percent-encoded grpc-message by default', function () {
const trailer = readGrpcWebResponseTrailer(trailerFromObject({
'grpc-status': GrpcStatusCode.INVALID_ARGUMENT,
'grpc-message': 'validation%20failed',
}));
expect(trailer).toEqual([GrpcStatusCode.INVALID_ARGUMENT, 'validation%20failed', {}])
});

it('decodes percent-encoded grpc-message when decodeMessage is true', function () {
const trailer = readGrpcWebResponseTrailer(trailerFromObject({
'grpc-status': GrpcStatusCode.INVALID_ARGUMENT,
'grpc-message': 'validation%20failed%3A%20field%20%22name%22%20is%20required',
}), true);
expect(trailer).toEqual([GrpcStatusCode.INVALID_ARGUMENT, 'validation failed: field "name" is required', {}])
});

it('falls back to raw value if grpc-message percent-decoding fails', function () {
const trailer = readGrpcWebResponseTrailer(trailerFromObject({
'grpc-status': GrpcStatusCode.INTERNAL,
'grpc-message': 'bad%GGencoding',
}), true);
expect(trailer).toEqual([GrpcStatusCode.INTERNAL, 'bad%GGencoding', {}])
});

it('adds other headers to meta', function () {
const someHeader = { foo: 'bar', baz: 'qux' };
const trailer = readGrpcWebResponseTrailer(trailerFromObject(someHeader));
Expand Down
36 changes: 24 additions & 12 deletions packages/grpcweb-transport/src/grpc-web-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ export function createGrpcWebRequestBody(message: Uint8Array, format: GrpcWebFor
* If given a fetch response, checks for fetch-specific error information
* ("type" property) and whether the "body" is null and throws a RpcError.
*/
export function readGrpcWebResponseHeader(fetchResponse: Response): [GrpcStatusCode | undefined, string | undefined, RpcMetadata];
export function readGrpcWebResponseHeader(headers: HttpHeaders, httpStatus: number, httpStatusText: string): [GrpcStatusCode | undefined, string | undefined, RpcMetadata];
export function readGrpcWebResponseHeader(headersOrFetchResponse: HttpHeaders | Response, httpStatus?: number, httpStatusText?: string): [GrpcStatusCode | undefined, string | undefined, RpcMetadata] {
if (arguments.length === 1) {
export function readGrpcWebResponseHeader(fetchResponse: Response, decodeMessage?: boolean): [GrpcStatusCode | undefined, string | undefined, RpcMetadata];
export function readGrpcWebResponseHeader(headers: HttpHeaders, httpStatus: number, httpStatusText: string, decodeMessage?: boolean): [GrpcStatusCode | undefined, string | undefined, RpcMetadata];
export function readGrpcWebResponseHeader(headersOrFetchResponse: HttpHeaders | Response, httpStatusOrDecode?: number | boolean, httpStatusText?: string, decodeMessage?: boolean): [GrpcStatusCode | undefined, string | undefined, RpcMetadata] {
// 1-2 args: Response overload; 3-4 args: HttpHeaders overload
if (arguments.length <= 2) {
let fetchResponse = headersOrFetchResponse as Response;
let decode = httpStatusOrDecode as boolean | undefined;

// Cloudflare Workers throw when the type property of a fetch response
// is accessed, so wrap access with try/catch. See:
Expand All @@ -104,17 +106,19 @@ export function readGrpcWebResponseHeader(headersOrFetchResponse: HttpHeaders |
return readGrpcWebResponseHeader(
fetchHeadersToHttp(fetchResponse.headers),
fetchResponse.status,
fetchResponse.statusText
fetchResponse.statusText,
decode
);
}
let
headers = headersOrFetchResponse as HttpHeaders,
httpOk = httpStatus! >= 200 && httpStatus! < 300,
httpStatus = httpStatusOrDecode as number,
httpOk = httpStatus >= 200 && httpStatus < 300,
responseMeta = parseMetadata(headers),
[statusCode, statusDetail] = parseStatus(headers);
[statusCode, statusDetail] = parseStatus(headers, decodeMessage);

if ((statusCode === undefined || statusCode === GrpcStatusCode.OK) && !httpOk) {
statusCode = httpStatusToGrpc(httpStatus!);
statusCode = httpStatusToGrpc(httpStatus);
statusDetail = httpStatusText;
}
return [statusCode, statusDetail, responseMeta];
Expand All @@ -129,10 +133,10 @@ export function readGrpcWebResponseHeader(headersOrFetchResponse: HttpHeaders |
* ASCII string with HTTP headers. Just pass the data of a grpc-web trailer
* frame.
*/
export function readGrpcWebResponseTrailer(data: Uint8Array): [GrpcStatusCode, string | undefined, RpcMetadata] {
export function readGrpcWebResponseTrailer(data: Uint8Array, decodeMessage?: boolean): [GrpcStatusCode, string | undefined, RpcMetadata] {
let
headers = parseTrailer(data),
[code, detail] = parseStatus(headers),
[code, detail] = parseStatus(headers, decodeMessage),
meta = parseMetadata(headers);
return [code ?? GrpcStatusCode.OK, detail, meta];
}
Expand Down Expand Up @@ -311,14 +315,22 @@ function parseFormat(contentType: string | undefined | null): GrpcWebFormat {


// returns error code on parse failure
function parseStatus(headers: HttpHeaders): [GrpcStatusCode | undefined, string | undefined] {
function parseStatus(headers: HttpHeaders, decodeMessage?: boolean): [GrpcStatusCode | undefined, string | undefined] {
let code: GrpcStatusCode | undefined,
message: string | undefined;
let m = headers['grpc-message'];
if (m !== undefined) {
if (Array.isArray(m))
return [GrpcStatusCode.INTERNAL, "invalid grpc-web message"];
message = m;
if (decodeMessage) {
try {
message = decodeURIComponent(m);
} catch {
message = m;
}
} else {
message = m;
}
}
let s = headers['grpc-status'];
if (s !== undefined) {
Expand Down
11 changes: 11 additions & 0 deletions packages/grpcweb-transport/src/grpc-web-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,15 @@ export interface GrpcWebOptions extends RpcOptions {
* A `fetch` function to use in place of `globalThis.fetch`
*/
fetch?: typeof fetch;

/**
* Decode percent-encoded grpc-message header values.
*
* Per the gRPC over HTTP/2 specification, the grpc-message header
* must be percent-encoded. When this option is true, the transport
* will decode the message using decodeURIComponent().
*
* Defaults to false for backwards compatibility.
*/
decodeGrpcMessage?: boolean;
}
10 changes: 6 additions & 4 deletions packages/grpcweb-transport/src/grpc-web-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export class GrpcWebFetchTransport implements RpcTransport {
format = opt.format ?? 'text',
fetch = opt.fetch ?? globalThis.fetch,
fetchInit = opt.fetchInit ?? {},
decodeGrpcMessage = opt.decodeGrpcMessage ?? false,
url = this.makeUrl(method, opt),
inputBytes = method.I.toBinary(input, opt.binaryOptions),
defHeader = new Deferred<RpcMetadata>(),
Expand All @@ -109,7 +110,7 @@ export class GrpcWebFetchTransport implements RpcTransport {
signal: options.abort ?? null // [email protected] rejects `undefined`
})
.then(fetchResponse => {
let [code, detail, meta] = readGrpcWebResponseHeader(fetchResponse);
let [code, detail, meta] = readGrpcWebResponseHeader(fetchResponse, decodeGrpcMessage);
defHeader.resolve(meta);
if (code != null && code !== GrpcStatusCode.OK)
throw new RpcError(detail ?? GrpcStatusCode[code], GrpcStatusCode[code], meta);
Expand All @@ -134,7 +135,7 @@ export class GrpcWebFetchTransport implements RpcTransport {
break;
case GrpcWebFrame.TRAILER:
let code, detail;
[code, detail, maybeTrailer] = readGrpcWebResponseTrailer(data);
[code, detail, maybeTrailer] = readGrpcWebResponseTrailer(data, decodeGrpcMessage);
maybeStatus = {
code: GrpcStatusCode[code],
detail: detail ?? GrpcStatusCode[code]
Expand Down Expand Up @@ -200,6 +201,7 @@ export class GrpcWebFetchTransport implements RpcTransport {
format = opt.format ?? 'text',
fetch = opt.fetch ?? globalThis.fetch,
fetchInit = opt.fetchInit ?? {},
decodeGrpcMessage = opt.decodeGrpcMessage ?? false,
url = this.makeUrl(method, opt),
inputBytes = method.I.toBinary(input, opt.binaryOptions),
defHeader = new Deferred<RpcMetadata>(),
Expand All @@ -218,7 +220,7 @@ export class GrpcWebFetchTransport implements RpcTransport {
signal: options.abort ?? null // [email protected] rejects `undefined`
})
.then(fetchResponse => {
let [code, detail, meta] = readGrpcWebResponseHeader(fetchResponse);
let [code, detail, meta] = readGrpcWebResponseHeader(fetchResponse, decodeGrpcMessage);
defHeader.resolve(meta);
if (code != null && code !== GrpcStatusCode.OK)
throw new RpcError(detail ?? GrpcStatusCode[code], GrpcStatusCode[code], meta);
Expand All @@ -242,7 +244,7 @@ export class GrpcWebFetchTransport implements RpcTransport {
break;
case GrpcWebFrame.TRAILER:
let code, detail;
[code, detail, maybeTrailer] = readGrpcWebResponseTrailer(data);
[code, detail, maybeTrailer] = readGrpcWebResponseTrailer(data, decodeGrpcMessage);
maybeStatus = {
code: GrpcStatusCode[code],
detail: detail ?? GrpcStatusCode[code]
Expand Down