@protobuf-ts/grpcweb-transport
Version:
gRPC-web transport for clients generated by the protoc plugin "protobuf-ts"
325 lines (324 loc) • 14.3 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.readGrpcWebResponseBody = exports.GrpcWebFrame = exports.readGrpcWebResponseTrailer = exports.readGrpcWebResponseHeader = exports.createGrpcWebRequestBody = exports.createGrpcWebRequestHeader = void 0;
const runtime_1 = require("@protobuf-ts/runtime");
const runtime_rpc_1 = require("@protobuf-ts/runtime-rpc");
const goog_grpc_status_code_1 = require("./goog-grpc-status-code");
/**
* Create fetch API headers for a grpc-web request.
*/
function createGrpcWebRequestHeader(headers, format, timeout, meta, userAgent) {
// add meta as headers
if (meta) {
for (let [k, v] of Object.entries(meta)) {
if (typeof v == "string")
headers.append(k, v);
else
for (let i of v)
headers.append(k, i);
}
}
// 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);
if (typeof timeout === "number") {
if (timeout <= 0) {
// we raise an error ourselves because header "grpc-timeout" must be a positive integer
throw new runtime_rpc_1.RpcError(`timeout ${timeout} ms exceeded`, goog_grpc_status_code_1.GrpcStatusCode[goog_grpc_status_code_1.GrpcStatusCode.DEADLINE_EXCEEDED]);
}
headers.set('grpc-timeout', `${timeout}m`);
}
else if (timeout) {
const deadline = timeout.getTime();
const now = Date.now();
if (deadline <= now) {
// we raise an error ourselves because header "grpc-timeout" must be a positive integer
throw new runtime_rpc_1.RpcError(`deadline ${timeout} exceeded`, goog_grpc_status_code_1.GrpcStatusCode[goog_grpc_status_code_1.GrpcStatusCode.DEADLINE_EXCEEDED]);
}
headers.set('grpc-timeout', `${deadline - now}m`);
}
return headers;
}
exports.createGrpcWebRequestHeader = createGrpcWebRequestHeader;
function createGrpcWebRequestBody(message, format) {
let body = new Uint8Array(5 + message.length); // we need 5 bytes for frame type + message length
body[0] = GrpcWebFrame.DATA; // first byte is frame type
// 4 bytes message length
for (let msgLen = message.length, i = 4; i > 0; i--) {
body[i] = (msgLen % 256);
msgLen >>>= 8;
}
body.set(message, 5); // reset is message
return format === "binary" ? body : runtime_1.base64encode(body);
}
exports.createGrpcWebRequestBody = createGrpcWebRequestBody;
function readGrpcWebResponseHeader(headersOrFetchResponse, httpStatus, httpStatusText) {
if (arguments.length === 1) {
let fetchResponse = headersOrFetchResponse;
// Cloudflare Workers throw when the type property of a fetch response
// is accessed, so wrap access with try/catch. See:
// * https://developers.cloudflare.com/workers/runtime-apis/response/#properties
// * https://github.com/cloudflare/miniflare/blob/72f046e/packages/core/src/standards/http.ts#L646
let responseType;
try {
responseType = fetchResponse.type;
}
catch (_a) { }
switch (responseType) {
case "error":
case "opaque":
case "opaqueredirect":
// see https://developer.mozilla.org/en-US/docs/Web/API/Response/type
throw new runtime_rpc_1.RpcError(`fetch response type ${fetchResponse.type}`, goog_grpc_status_code_1.GrpcStatusCode[goog_grpc_status_code_1.GrpcStatusCode.UNKNOWN]);
}
return readGrpcWebResponseHeader(fetchHeadersToHttp(fetchResponse.headers), fetchResponse.status, fetchResponse.statusText);
}
let headers = headersOrFetchResponse, httpOk = httpStatus >= 200 && httpStatus < 300, responseMeta = parseMetadata(headers), [statusCode, statusDetail] = parseStatus(headers);
if ((statusCode === undefined || statusCode === goog_grpc_status_code_1.GrpcStatusCode.OK) && !httpOk) {
statusCode = httpStatusToGrpc(httpStatus);
statusDetail = httpStatusText;
}
return [statusCode, statusDetail, responseMeta];
}
exports.readGrpcWebResponseHeader = readGrpcWebResponseHeader;
/**
* Parses a grpc status (code and optional text) and meta data from response
* trailers.
*
* Response trailers are expected as a byte array, but are actually just an
* ASCII string with HTTP headers. Just pass the data of a grpc-web trailer
* frame.
*/
function readGrpcWebResponseTrailer(data) {
let headers = parseTrailer(data), [code, detail] = parseStatus(headers), meta = parseMetadata(headers);
return [code !== null && code !== void 0 ? code : goog_grpc_status_code_1.GrpcStatusCode.OK, detail, meta];
}
exports.readGrpcWebResponseTrailer = readGrpcWebResponseTrailer;
/**
* A grpc-frame type. Can be used to determine type of frame emitted by
* `readGrpcWebResponseBody()`.
*/
var GrpcWebFrame;
(function (GrpcWebFrame) {
GrpcWebFrame[GrpcWebFrame["DATA"] = 0] = "DATA";
GrpcWebFrame[GrpcWebFrame["TRAILER"] = 128] = "TRAILER";
})(GrpcWebFrame = exports.GrpcWebFrame || (exports.GrpcWebFrame = {}));
/**
* Parses a grpc-web response (unary or server streaming) from a fetch API
* stream.
*
* Emits grpc-web frames.
*
* The returned promise resolves when the response is complete.
*/
function readGrpcWebResponseBody(stream, contentType, onFrame) {
return __awaiter(this, void 0, void 0, function* () {
let streamReader, base64queue = "", byteQueue = new Uint8Array(0), format = parseFormat(contentType);
// allows to read streams from the 'node-fetch' polyfill which uses
// node.js ReadableStream instead of the what-wg streams api ReadableStream
if (isReadableStream(stream)) {
let whatWgReadableStream = stream.getReader();
streamReader = {
next: () => whatWgReadableStream.read()
};
}
else {
streamReader = stream[Symbol.asyncIterator]();
}
while (true) {
let result = yield streamReader.next();
if (result.value !== undefined) {
if (format === "text") {
// the statements below just decode base64 and append to `bytesUnread`
// add incoming base64 to queue
for (let i = 0; i < result.value.length; i++)
base64queue += String.fromCharCode(result.value[i]);
// if the base64 queue is not a multiple of 4,
// we have to wait for more data
let safeLen = base64queue.length - base64queue.length % 4;
if (safeLen === 0)
continue;
// decode safe chunk of base64 and add to byte queue
byteQueue = concatBytes(byteQueue, runtime_1.base64decode(base64queue.substring(0, safeLen)));
base64queue = base64queue.substring(safeLen);
}
else {
byteQueue = concatBytes(byteQueue, result.value);
}
// read all fully available data frames
while (byteQueue.length >= 5 && byteQueue[0] === GrpcWebFrame.DATA) {
let msgLen = 0;
for (let i = 1; i < 5; i++)
msgLen = (msgLen << 8) + byteQueue[i];
if (byteQueue.length - 5 >= msgLen) {
// we have the entire message
onFrame(GrpcWebFrame.DATA, byteQueue.subarray(5, 5 + msgLen));
byteQueue = byteQueue.subarray(5 + msgLen);
}
else
break; // wait for more data
}
}
// exit, but emit trailer if exists
if (result.done) {
if (byteQueue.length === 0)
break;
if (byteQueue[0] !== GrpcWebFrame.TRAILER || byteQueue.length < 5)
throw new runtime_rpc_1.RpcError("premature EOF", goog_grpc_status_code_1.GrpcStatusCode[goog_grpc_status_code_1.GrpcStatusCode.DATA_LOSS]);
onFrame(GrpcWebFrame.TRAILER, byteQueue.subarray(5));
break;
}
}
});
}
exports.readGrpcWebResponseBody = readGrpcWebResponseBody;
// internal
const isReadableStream = (s) => {
return typeof s.getReader == "function";
};
// internal
function concatBytes(a, b) {
let n = new Uint8Array(a.length + b.length);
n.set(a);
n.set(b, a.length);
return n;
}
// determines format from response "content-type" value.
// throws if value is unknown or missing.
function parseFormat(contentType) {
// > the sender *should* always specify the message format, e.g. +proto, +json
//
// > the receiver should assume the default is "+proto" when the message format is
// > missing in Content-Type (as "application/grpc-web")
//
// see https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
switch (contentType) {
case "application/grpc-web-text":
case "application/grpc-web-text+proto":
return "text";
case "application/grpc-web":
case "application/grpc-web+proto":
return "binary";
case undefined:
case null:
throw new runtime_rpc_1.RpcError("missing response content type", goog_grpc_status_code_1.GrpcStatusCode[goog_grpc_status_code_1.GrpcStatusCode.INTERNAL]);
default:
throw new runtime_rpc_1.RpcError("unexpected response content type: " + contentType, goog_grpc_status_code_1.GrpcStatusCode[goog_grpc_status_code_1.GrpcStatusCode.INTERNAL]);
}
}
// returns error code on parse failure
function parseStatus(headers) {
let code, message;
let m = headers['grpc-message'];
if (m !== undefined) {
if (Array.isArray(m))
return [goog_grpc_status_code_1.GrpcStatusCode.INTERNAL, "invalid grpc-web message"];
message = m;
}
let s = headers['grpc-status'];
if (s !== undefined) {
if (Array.isArray(s))
return [goog_grpc_status_code_1.GrpcStatusCode.INTERNAL, "invalid grpc-web status"];
code = parseInt(s, 10);
if (goog_grpc_status_code_1.GrpcStatusCode[code] === undefined)
return [goog_grpc_status_code_1.GrpcStatusCode.INTERNAL, "invalid grpc-web status"];
}
return [code, message];
}
// skips grpc-web headers
function parseMetadata(headers) {
let meta = {};
for (let [k, v] of Object.entries(headers))
switch (k) {
case 'grpc-message':
case 'grpc-status':
case 'content-type':
break;
default:
meta[k] = v;
}
return meta;
}
// parse trailer data (ASCII) to our headers rep
function parseTrailer(trailerData) {
let headers = {};
for (let chunk of String.fromCharCode.apply(String, trailerData).trim().split("\r\n")) {
if (chunk == "")
continue;
let [key, ...val] = chunk.split(":");
const value = val.join(":").trim();
key = key.trim();
let e = headers[key];
if (typeof e == "string")
headers[key] = [e, value];
else if (Array.isArray(e))
e.push(value);
else
headers[key] = value;
}
return headers;
}
// fetch API to our headers rep
function fetchHeadersToHttp(fetchHeaders) {
let headers = {};
fetchHeaders.forEach((value, key) => {
let e = headers[key];
if (typeof e == "string")
headers[key] = [e, value];
else if (Array.isArray(e))
e.push(value);
else
headers[key] = value;
});
return headers;
}
// internal
function httpStatusToGrpc(httpStatus) {
switch (httpStatus) {
case 200:
return goog_grpc_status_code_1.GrpcStatusCode.OK;
case 400:
return goog_grpc_status_code_1.GrpcStatusCode.INVALID_ARGUMENT;
case 401:
return goog_grpc_status_code_1.GrpcStatusCode.UNAUTHENTICATED;
case 403:
return goog_grpc_status_code_1.GrpcStatusCode.PERMISSION_DENIED;
case 404:
return goog_grpc_status_code_1.GrpcStatusCode.NOT_FOUND;
case 409:
return goog_grpc_status_code_1.GrpcStatusCode.ABORTED;
case 412:
return goog_grpc_status_code_1.GrpcStatusCode.FAILED_PRECONDITION;
case 429:
return goog_grpc_status_code_1.GrpcStatusCode.RESOURCE_EXHAUSTED;
case 499:
return goog_grpc_status_code_1.GrpcStatusCode.CANCELLED;
case 500:
return goog_grpc_status_code_1.GrpcStatusCode.UNKNOWN;
case 501:
return goog_grpc_status_code_1.GrpcStatusCode.UNIMPLEMENTED;
case 503:
return goog_grpc_status_code_1.GrpcStatusCode.UNAVAILABLE;
case 504:
return goog_grpc_status_code_1.GrpcStatusCode.DEADLINE_EXCEEDED;
default:
return goog_grpc_status_code_1.GrpcStatusCode.UNKNOWN;
}
}
;