UNPKG

@protobuf-ts/twirp-transport

Version:

Twirp transport for clients generated by the protoc plugin "protobuf-ts".

127 lines (126 loc) 6.49 kB
import { createTwirpRequestHeader, parseMetadataFromResponseHeaders, parseTwirpErrorResponse } from "./twirp-format"; import { Deferred, mergeRpcOptions, RpcError, UnaryCall } from "@protobuf-ts/runtime-rpc"; import { lowerCamelCase } from "@protobuf-ts/runtime"; import { TwirpErrorCode } from "./twitch-twirp-error-code"; /** * Implements the Twirp protocol, supporting JSON or binary format on * the wire. Uses the fetch API to do the HTTP requests. * * See https://twitchtv.github.io/twirp/docs/spec_v5.html */ export class TwirpFetchTransport { constructor(options) { this.defaultOptions = options; } mergeOptions(options) { return mergeRpcOptions(this.defaultOptions, options); } unary(method, input, options) { var _a, _b, _c; let opt = options, url = this.makeUrl(method, opt), fetchInit = (_a = opt.fetchInit) !== null && _a !== void 0 ? _a : {}, requestBody = opt.sendJson ? method.I.toJsonString(input, opt.jsonOptions) : method.I.toBinary(input, opt.binaryOptions), defHeader = new Deferred(), defMessage = new Deferred(), defStatus = new Deferred(), defTrailer = new Deferred(); globalThis.fetch(url, Object.assign(Object.assign({}, fetchInit), { method: 'POST', headers: createTwirpRequestHeader(new globalThis.Headers(), !!opt.sendJson, opt.meta), body: requestBody, signal: (_b = options.abort) !== null && _b !== void 0 ? _b : null // node-fetch@3.0.0-beta.9 rejects `undefined` })) .then(fetchResponse => { defHeader.resolve(parseMetadataFromResponseHeaders(fetchResponse.headers)); // 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 RpcError(`fetch response type ${fetchResponse.type}`, TwirpErrorCode[TwirpErrorCode.unknown]); } if (!fetchResponse.ok) { return fetchResponse.json().then(value => { throw parseTwirpErrorResponse(value); }, () => { throw new RpcError('received HTTP ' + fetchResponse.status + ', unable to read response body as json', TwirpErrorCode[TwirpErrorCode.internal]); }); } if (opt.sendJson) { return fetchResponse.json().then(value => method.O.fromJson(value, opt.jsonOptions), () => { throw new RpcError('unable to read response body as json', TwirpErrorCode[TwirpErrorCode.dataloss]); }); } return fetchResponse.arrayBuffer().then(value => method.O.fromBinary(new Uint8Array(value), opt.binaryOptions), () => { throw new RpcError('unable to read response body', TwirpErrorCode[TwirpErrorCode.dataloss]); }); }, (reason) => { // failed to fetch, aborted, wrong url or network problem if (reason instanceof Error && reason.name === 'AbortError') throw new RpcError(reason.message, TwirpErrorCode[TwirpErrorCode.cancelled]); throw new RpcError(reason instanceof Error ? reason.message : reason); }) .then(message => { defMessage.resolve(message); defStatus.resolve({ code: 'OK', detail: '' }); defTrailer.resolve({}); }) .catch((reason) => { // RpcErrors are thrown by us, everything else is an internal error let error = reason instanceof RpcError ? reason : new RpcError(reason instanceof Error ? reason.message : reason, TwirpErrorCode[TwirpErrorCode.internal]); error.methodName = method.name; error.serviceName = method.service.typeName; defHeader.rejectPending(error); defMessage.rejectPending(error); defStatus.rejectPending(error); defTrailer.rejectPending(error); }); return new UnaryCall(method, (_c = opt.meta) !== null && _c !== void 0 ? _c : {}, input, defHeader.promise, defMessage.promise, defStatus.promise, defTrailer.promise); } /** * Create an URI for a RPC call. * * Takes the `baseUrl` option and appends: * - slash "/" * - package name * - dot "." * - service name * - slash "/" * - method name * * If the service was declared without a package, the package name and dot * are omitted. * * The method name is CamelCased just as it would be in Go, unless the * option `useProtoMethodName` is `true`. */ makeUrl(method, options) { let base = options.baseUrl; if (base.endsWith('/')) base = base.substring(0, base.length - 1); let methodName = method.name; if (options.useProtoMethodName !== true) { methodName = lowerCamelCase(methodName); methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1); } return `${base}/${method.service.typeName}/${methodName}`; } clientStreaming(method /*, options: RpcOptions*/) { const e = new RpcError('Client streaming is not supported by Twirp', TwirpErrorCode[TwirpErrorCode.unimplemented]); e.methodName = method.name; e.serviceName = method.service.typeName; throw e; } duplex(method /*, options: RpcOptions*/) { const e = new RpcError('Duplex streaming is not supported by Twirp', TwirpErrorCode[TwirpErrorCode.unimplemented]); e.methodName = method.name; e.serviceName = method.service.typeName; throw e; } serverStreaming(method /*, input: I, options?: RpcOptions*/) { const e = new RpcError('Server streaming is not supported by Twirp', TwirpErrorCode[TwirpErrorCode.unimplemented]); e.methodName = method.name; e.serviceName = method.service.typeName; throw e; } }