Skip to content

Commit

Permalink
Merge pull request #54 from timostamm/fix-grpcweb-envoy
Browse files Browse the repository at this point in the history
Support grpc-web-text format with envoy.
  • Loading branch information
timostamm authored Dec 20, 2020
2 parents 14a9784 + dea2e0e commit ee2ade2
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 29 deletions.
54 changes: 38 additions & 16 deletions packages/grpcweb-transport/src/grpc-web-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export function createGrpcWebRequestHeader(headers: Headers, format: GrpcWebForm
}
// set standard headers (possibly overwriting meta)
headers.set('Content-Type', format === "text" ? "application/grpc-web-text" : "application/grpc-web+proto");
if (format == "text") {
// The client library should indicate to the server via the "Accept" header that
// the response stream needs to be text encoded e.g. when XHR is used or due to
// security policies with XHR
headers.set("Accept", "application/grpc-web-text");
}
headers.set('X-Grpc-Web', "1");
if (userAgent)
headers.set("X-User-Agent", userAgent);
Expand Down Expand Up @@ -60,9 +66,9 @@ 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, string | undefined, RpcMetadata];
export function readGrpcWebResponseHeader(headers: HttpHeaders, httpStatus: number, httpStatusText: string): [GrpcStatusCode, string | undefined, RpcMetadata];
export function readGrpcWebResponseHeader(headersOrFetchResponse: HttpHeaders | Response, httpStatus?: number, httpStatusText?: string): [GrpcStatusCode, string | undefined, RpcMetadata] {
export function readGrpcWebResponseHeader(fetchResponse: Response): [GrpcStatusCode, string | undefined, RpcMetadata, GrpcWebFormat];
export function readGrpcWebResponseHeader(headers: HttpHeaders, httpStatus: number, httpStatusText: string): [GrpcStatusCode, string | undefined, RpcMetadata, GrpcWebFormat];
export function readGrpcWebResponseHeader(headersOrFetchResponse: HttpHeaders | Response, httpStatus?: number, httpStatusText?: string): [GrpcStatusCode, string | undefined, RpcMetadata, GrpcWebFormat] {
if (arguments.length === 1) {
let fetchResponse = headersOrFetchResponse as Response;
switch (fetchResponse.type) {
Expand All @@ -72,8 +78,6 @@ export function readGrpcWebResponseHeader(headersOrFetchResponse: HttpHeaders |
// see https://developer.mozilla.org/en-US/docs/Web/API/Response/type
throw new RpcError(`fetch response type ${fetchResponse.type}`, GrpcStatusCode[GrpcStatusCode.UNKNOWN]);
}
if (!fetchResponse.body)
throw new RpcError('premature end of response', GrpcStatusCode[GrpcStatusCode.DATA_LOSS]);
return readGrpcWebResponseHeader(
fetchHeadersToHttp(fetchResponse.headers),
fetchResponse.status,
Expand All @@ -83,14 +87,14 @@ export function readGrpcWebResponseHeader(headersOrFetchResponse: HttpHeaders |
let
headers = headersOrFetchResponse as HttpHeaders,
httpOk = httpStatus! >= 200 && httpStatus! < 300,
responseMeta = parseMetadataFromHttpHeaders(headers),
[statusCode, statusDetail] = parseStatusFromHttpHeaders(headers);
responseMeta = parseMetadata(headers),
[statusCode, statusDetail] = parseStatus(headers);

if (statusCode === GrpcStatusCode.OK && !httpOk) {
statusCode = grpcStatusCodeFromHttp(httpStatus!);
statusCode = httpStatusToGrpc(httpStatus!);
statusDetail = httpStatusText;
}
return [statusCode, statusDetail, responseMeta];
return [statusCode, statusDetail, responseMeta, parseFormat(headers)];
}


Expand All @@ -104,9 +108,9 @@ export function readGrpcWebResponseHeader(headersOrFetchResponse: HttpHeaders |
*/
export function readGrpcWebResponseTrailer(data: Uint8Array): [GrpcStatusCode, string | undefined, RpcMetadata] {
let
headers = parseTrailerToHttpHeaders(data),
[code, detail] = parseStatusFromHttpHeaders(headers),
meta = parseMetadataFromHttpHeaders(headers);
headers = parseTrailer(data),
[code, detail] = parseStatus(headers),
meta = parseMetadata(headers);
return [code, detail, meta];
}

Expand Down Expand Up @@ -207,8 +211,26 @@ function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
}


// returns format from response header, throws if unknown
function parseFormat(headers: HttpHeaders): GrpcWebFormat {
let ct = headers['content-type'];
switch (ct) {
case "application/grpc-web-text":
return "text";
case "application/grpc-web":
// the receiver should assume the default is "+proto" when the message format is missing in Content-Type (as "application/grpc-web")
case "application/grpc-web+proto":
return "binary";
case undefined:
throw new RpcError("missing response content type", GrpcStatusCode[GrpcStatusCode.INTERNAL]);
default:
throw new RpcError("unexpected response content type: " + ct, GrpcStatusCode[GrpcStatusCode.INTERNAL]);
}
}


// returns error code on parse failure, uses OK as default code
function parseStatusFromHttpHeaders(headers: HttpHeaders): [GrpcStatusCode, string | undefined] {
function parseStatus(headers: HttpHeaders): [GrpcStatusCode, string | undefined] {
let code = GrpcStatusCode.OK,
message: string | undefined;
let m = headers['grpc-message'];
Expand All @@ -228,7 +250,7 @@ function parseStatusFromHttpHeaders(headers: HttpHeaders): [GrpcStatusCode, stri


// skips grpc-web headers
function parseMetadataFromHttpHeaders(headers: HttpHeaders): RpcMetadata {
function parseMetadata(headers: HttpHeaders): RpcMetadata {
let meta: RpcMetadata = {};
for (let [k, v] of Object.entries(headers))
switch (k) {
Expand All @@ -244,7 +266,7 @@ function parseMetadataFromHttpHeaders(headers: HttpHeaders): RpcMetadata {


// parse trailer data (ASCII) to our headers rep
function parseTrailerToHttpHeaders(trailerData: Uint8Array): HttpHeaders {
function parseTrailer(trailerData: Uint8Array): HttpHeaders {
let headers: HttpHeaders = {};
for (let chunk of String.fromCharCode.apply(String, trailerData as unknown as number[]).trim().split("\r\n")) {
let [key, value] = chunk.split(":", 2);
Expand Down Expand Up @@ -278,7 +300,7 @@ function fetchHeadersToHttp(fetchHeaders: Headers): HttpHeaders {


// internal
function grpcStatusCodeFromHttp(httpStatus: number): GrpcStatusCode {
function httpStatusToGrpc(httpStatus: number): GrpcStatusCode {
switch (httpStatus) {
case 200:
return GrpcStatusCode.OK;
Expand Down
40 changes: 27 additions & 13 deletions packages/grpcweb-transport/src/grpc-web-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,28 @@ export class GrpcWebFetchTransport implements RpcTransport {
signal: options.abort
})
.then(fetchResponse => {
let [code, detail, meta] = readGrpcWebResponseHeader(fetchResponse);
let [code, detail, meta, ] = readGrpcWebResponseHeader(fetchResponse);
defHeader.resolve(meta);
if (code !== GrpcStatusCode.OK)
throw new RpcError(detail ?? GrpcStatusCode[code], GrpcStatusCode[code], meta);
return fetchResponse.body!;
return fetchResponse;

}, reason => {
// failed to fetch, aborted, wrong url or network problem
// failed to parse header
if (reason instanceof RpcError)
return Promise.reject(reason);
// aborted
if (reason instanceof Error && reason.name === 'AbortError')
throw new RpcError(reason.message, GrpcStatusCode[GrpcStatusCode.CANCELLED]);
// failed to fetch, wrong url or network problem
throw new RpcError(reason instanceof Error ? reason.message : reason);
})

.then(responseBody => {
return readGrpcWebResponseBody(responseBody, format, (type, data) => {
.then(fetchResponse => {
if (!fetchResponse.body)
throw new RpcError('missing response body', GrpcStatusCode[GrpcStatusCode.INTERNAL]);
let [, , , responseFormat] = readGrpcWebResponseHeader(fetchResponse);
return readGrpcWebResponseBody(fetchResponse.body!, responseFormat, (type, data) => {
switch (type) {
case GrpcWebFrame.DATA:
responseStream.notifyMessage(
Expand Down Expand Up @@ -196,21 +203,28 @@ export class GrpcWebFetchTransport implements RpcTransport {
signal: options.abort
})
.then(fetchResponse => {
let [statusCode, statusDetail, responseMeta] = readGrpcWebResponseHeader(fetchResponse);
defHeader.resolve(responseMeta);
if (statusCode !== GrpcStatusCode.OK)
throw new RpcError(statusDetail ?? GrpcStatusCode[statusCode], GrpcStatusCode[statusCode], responseMeta);
return fetchResponse.body!;
let [code, detail, meta, ] = readGrpcWebResponseHeader(fetchResponse);
defHeader.resolve(meta);
if (code !== GrpcStatusCode.OK)
throw new RpcError(detail ?? GrpcStatusCode[code], GrpcStatusCode[code], meta);
return fetchResponse;

}, reason => {
// failed to fetch, aborted, wrong url or network problem
// failed to parse header
if (reason instanceof RpcError)
return Promise.reject(reason);
// aborted
if (reason instanceof Error && reason.name === 'AbortError')
throw new RpcError(reason.message, GrpcStatusCode[GrpcStatusCode.CANCELLED]);
// failed to fetch, wrong url or network problem
throw new RpcError(reason instanceof Error ? reason.message : reason);
})

.then(responseBody => {
return readGrpcWebResponseBody(responseBody, format, (type, data) => {
.then(fetchResponse => {
if (!fetchResponse.body)
throw new RpcError('missing response body', GrpcStatusCode[GrpcStatusCode.INTERNAL]);
let [, , , responseFormat] = readGrpcWebResponseHeader(fetchResponse);
return readGrpcWebResponseBody(fetchResponse.body!, responseFormat, (type, data) => {
switch (type) {
case GrpcWebFrame.DATA:
if (defMessage.state === DeferredState.RESOLVED)
Expand Down

0 comments on commit ee2ade2

Please sign in to comment.