packages/dubbo-fastify/src/fastify-dubbo-plugin.ts (100 lines of code) (raw):

// Copyright 2021-2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { JsonValue } from "@bufbuild/protobuf"; import { Code, ConnectError, createConnectRouter } from "apache-dubbo"; import type { ConnectRouter, ConnectRouterOptions } from "apache-dubbo"; import * as protoConnect from "apache-dubbo/protocol-triple"; import * as protoGrpcWeb from "apache-dubbo/protocol-grpc-web"; import * as protoGrpc from "apache-dubbo/protocol-grpc"; import { compressionBrotli, compressionGzip, universalRequestFromNodeRequest, universalResponseToNodeResponse, } from "apache-dubbo-node"; import type { FastifyInstance } from "fastify/types/instance"; interface FastifyConnectPluginOptions extends ConnectRouterOptions { /** * Route definitions. We recommend the following pattern: * * Create a file `connect.ts` with a default export such as this: * * ```ts * import {ConnectRouter} from "apache-dubbo"; * * export default (router: ConnectRouter) => { * router.service(ElizaService, {}); * } * ``` * * Then pass this function here. */ routes?: (router: ConnectRouter) => void; } /** * Plug your Connect routes into a Fastify server. */ export function fastifyConnectPlugin( instance: FastifyInstance, opts: FastifyConnectPluginOptions, done: (err?: Error) => void ) { if (opts.routes === undefined) { done(); return; } if (opts.acceptCompression === undefined) { opts.acceptCompression = [compressionGzip, compressionBrotli]; } const router = createConnectRouter(opts); opts.routes(router); const uHandlers = router.handlers; if (uHandlers.length == 0) { done(); return; } // we can override all content type parsers (including application/json) in // this plugin without affecting outer scope addNoopContentTypeParsers(instance); for (const uHandler of uHandlers) { instance.all( uHandler.requestPath, {}, async function handleFastifyRequest(req, reply) { try { const uRes = await uHandler( universalRequestFromNodeRequest( req.raw, req.body as JsonValue | undefined ) ); // Fastify maintains response headers on the reply object and only moves them to // the raw response during reply.send, but we are not using reply.send with this plugin. // So we need to manually copy them to the raw response before handing off to vanilla Node. for (const [key, value] of Object.entries(reply.getHeaders())) { if (value !== undefined) { reply.raw.setHeader(key, value); } } await universalResponseToNodeResponse(uRes, reply.raw); } catch (e) { if (ConnectError.from(e).code == Code.Aborted) { return; } // eslint-disable-next-line no-console console.error( `handler for rpc ${uHandler.method.name} of ${uHandler.service.typeName} failed`, e ); } } ); } done(); } /** * Registers fastify content type parsers that do nothing for all content-types * known to Connect. */ function addNoopContentTypeParsers(instance: FastifyInstance): void { instance.addContentTypeParser( [ protoConnect.contentTypeUnaryJson, protoConnect.contentTypeStreamJson, protoGrpcWeb.contentTypeProto, protoGrpcWeb.contentTypeJson, protoGrpc.contentTypeProto, protoGrpc.contentTypeJson, ], noopContentTypeParser ); instance.addContentTypeParser( protoGrpc.contentTypeRegExp, noopContentTypeParser ); instance.addContentTypeParser( protoGrpcWeb.contentTypeRegExp, noopContentTypeParser ); instance.addContentTypeParser( protoConnect.contentTypeRegExp, noopContentTypeParser ); } function noopContentTypeParser( _req: unknown, _payload: unknown, done: (err: null, body?: undefined) => void ) { done(null, undefined); }