Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for user provided context in handlers #835

Merged
merged 3 commits into from
Oct 2, 2023
Merged
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
17 changes: 15 additions & 2 deletions packages/connect-express/src/express-connect-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

import type { JsonValue } from "@bufbuild/protobuf";
import { createConnectRouter, Code, ConnectError } from "@connectrpc/connect";
import type { ConnectRouter, ConnectRouterOptions } from "@connectrpc/connect";
import type {
ConnectRouter,
ConnectRouterOptions,
ContextValues,
} from "@connectrpc/connect";
import type { UniversalHandler } from "@connectrpc/connect/protocol";
import {
compressionBrotli,
Expand Down Expand Up @@ -48,6 +52,11 @@ interface ExpressConnectMiddlewareOptions extends ConnectRouterOptions {
* Note that many gRPC client implementations do not allow for prefixes.
*/
requestPathPrefix?: string;
/**
* Context values to extract from the request. These values are passed to
* the handlers.
*/
contextValues?: (req: express.Request) => ContextValues;
}

/**
Expand Down Expand Up @@ -76,7 +85,11 @@ export function expressConnectMiddleware(
if (!uHandler) {
return next();
}
const uReq = universalRequestFromNodeRequest(req, getPreparsedBody(req));
const uReq = universalRequestFromNodeRequest(
req,
getPreparsedBody(req),
options.contextValues?.(req),
);
uHandler(uReq)
.then((uRes) => universalResponseToNodeResponse(uRes, res))
.catch((reason) => {
Expand Down
13 changes: 12 additions & 1 deletion packages/connect-fastify/src/fastify-connect-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

import type { JsonValue } from "@bufbuild/protobuf";
import { Code, ConnectError, createConnectRouter } from "@connectrpc/connect";
import type { ConnectRouter, ConnectRouterOptions } from "@connectrpc/connect";
import type {
ConnectRouter,
ConnectRouterOptions,
ContextValues,
} from "@connectrpc/connect";
import * as protoConnect from "@connectrpc/connect/protocol-connect";
import * as protoGrpcWeb from "@connectrpc/connect/protocol-grpc-web";
import * as protoGrpc from "@connectrpc/connect/protocol-grpc";
Expand All @@ -25,6 +29,7 @@ import {
universalResponseToNodeResponse,
} from "@connectrpc/connect-node";
import type { FastifyInstance } from "fastify/types/instance";
import type { FastifyRequest } from "fastify/types/request";

interface FastifyConnectPluginOptions extends ConnectRouterOptions {
/**
Expand All @@ -43,6 +48,11 @@ interface FastifyConnectPluginOptions extends ConnectRouterOptions {
* Then pass this function here.
*/
routes?: (router: ConnectRouter) => void;
/**
* Context values to extract from the request. These values are passed to
* the handlers.
*/
contextValues?: (req: FastifyRequest) => ContextValues;
}

/**
Expand Down Expand Up @@ -83,6 +93,7 @@ export function fastifyConnectPlugin(
universalRequestFromNodeRequest(
req.raw,
req.body as JsonValue | undefined,
opts.contextValues?.(req),
),
);
// Fastify maintains response headers on the reply object and only moves them to
Expand Down
17 changes: 15 additions & 2 deletions packages/connect-next/src/connect-nextjs-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
// limitations under the License.

import { createConnectRouter } from "@connectrpc/connect";
import type { ConnectRouter, ConnectRouterOptions } from "@connectrpc/connect";
import type {
ConnectRouter,
ConnectRouterOptions,
ContextValues,
} from "@connectrpc/connect";
import type { UniversalHandler } from "@connectrpc/connect/protocol";
import {
compressionBrotli,
Expand Down Expand Up @@ -56,6 +60,11 @@ interface NextJsApiRouterOptions extends ConnectRouterOptions {
* This is `/api` by default for Next.js.
*/
prefix?: string;
/**
* Context values to extract from the request. These values are passed to
* the handlers.
*/
contextValues?: (req: NextApiRequest) => ContextValues;
}

/**
Expand Down Expand Up @@ -84,7 +93,11 @@ export function nextJsApiRouter(options: NextJsApiRouterOptions): ApiRoute {
}
try {
const uRes = await uHandler(
universalRequestFromNodeRequest(req, req.body as JsonValue | undefined),
universalRequestFromNodeRequest(
req,
req.body as JsonValue | undefined,
options.contextValues?.(req),
),
);
await universalResponseToNodeResponse(uRes, res);
} catch (e) {
Expand Down
52 changes: 52 additions & 0 deletions packages/connect-node-test/src/node-readme.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import * as http2 from "http2";
import { Message, MethodKind, proto3 } from "@bufbuild/protobuf";
import type { PartialMessage } from "@bufbuild/protobuf";
import {
createContextKey,
createContextValues,
createPromiseClient,
createRouterTransport,
} from "@connectrpc/connect";
Expand Down Expand Up @@ -181,6 +183,56 @@ describe("node readme", function () {
server.close();
});

it("using context value", async function () {
let port = -1;

const kUser = createContextKey<string | undefined>(undefined);
function routes(router: ConnectRouter) {
router.rpc(
ElizaService,
ElizaService.methods.say,
async (req, { values }) => ({
sentence: `Hey ${values.get(kUser)}! You said: ${req.sentence}`,
}),
);
}

function startServer() {
return new Promise<http2.Http2Server>((resolve) => {
const handler = connectNodeAdapter({
routes,
contextValues: (req) =>
createContextValues().set(kUser, req.headers["x-user"]),
});
const server = http2.createServer(handler).listen(0, () => {
const a = server.address();
if (a !== null && typeof a !== "string") {
port = a.port;
}
resolve(server);
});
});
}

async function runClient() {
const transport = createGrpcTransport({
baseUrl: `http://localhost:${port}`,
httpVersion: "2",
});
const client = createPromiseClient(ElizaService, transport);
const res = await client.say(
{ sentence: "I feel happy." },
{ headers: { "x-user": "alice" } },
);
// console.log(res.sentence) // Hey alice! You said: I feel happy.
expect(res.sentence).toBe("Hey alice! You said: I feel happy.");
}

const server = await startServer();
await runClient();
server.close();
});

it("using writable iterable", async function () {
let port = -1;

Expand Down
17 changes: 15 additions & 2 deletions packages/connect-node/src/connect-node-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
// limitations under the License.

import { Code, ConnectError, createConnectRouter } from "@connectrpc/connect";
import type { ConnectRouter, ConnectRouterOptions } from "@connectrpc/connect";
import type {
ConnectRouter,
ConnectRouterOptions,
ContextValues,
} from "@connectrpc/connect";
import type { UniversalHandler } from "@connectrpc/connect/protocol";
import { uResponseNotFound } from "@connectrpc/connect/protocol";
import {
Expand Down Expand Up @@ -55,6 +59,11 @@ interface ConnectNodeAdapterOptions extends ConnectRouterOptions {
* Note that many gRPC client implementations do not allow for prefixes.
*/
requestPathPrefix?: string;
/**
* Context values to extract from the request. These values are passed to
* the handlers.
*/
contextValues?: (req: NodeServerRequest) => ContextValues;
}

/**
Expand Down Expand Up @@ -85,7 +94,11 @@ export function connectNodeAdapter(
(options.fallback ?? fallback)(req, res);
return;
}
const uReq = universalRequestFromNodeRequest(req, undefined);
const uReq = universalRequestFromNodeRequest(
req,
undefined,
options.contextValues?.(req),
);
uHandler(uReq)
.then((uRes) => universalResponseToNodeResponse(uRes, res))
.catch((reason) => {
Expand Down
18 changes: 15 additions & 3 deletions packages/connect-node/src/node-universal-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ describe("universalRequestFromNodeRequest()", function () {
const server = useNodeServer(() => {
serverRequest = undefined;
return http2.createServer(function (request) {
serverRequest = universalRequestFromNodeRequest(request, undefined);
serverRequest = universalRequestFromNodeRequest(
request,
undefined,
undefined,
);
});
});
async function request(rstCode: number) {
Expand Down Expand Up @@ -173,7 +177,11 @@ describe("universalRequestFromNodeRequest()", function () {
requestTimeout: 0,
},
function (request) {
serverRequest = universalRequestFromNodeRequest(request, undefined);
serverRequest = universalRequestFromNodeRequest(
request,
undefined,
undefined,
);
},
);
// For some reason, the type definitions for ServerOptions do not include
Expand Down Expand Up @@ -259,7 +267,11 @@ describe("universalRequestFromNodeRequest()", function () {
requestTimeout: 0,
},
function (request, response) {
serverRequest = universalRequestFromNodeRequest(request, undefined);
serverRequest = universalRequestFromNodeRequest(
request,
undefined,
undefined,
);
response.writeHead(200);
response.end();
},
Expand Down
3 changes: 3 additions & 0 deletions packages/connect-node/src/node-universal-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
connectErrorFromH2ResetCode,
connectErrorFromNodeReason,
} from "./node-error.js";
import type { ContextValues } from "@connectrpc/connect";

/**
* NodeHandlerFn is compatible with http.RequestListener and its equivalent
Expand Down Expand Up @@ -73,6 +74,7 @@ export type NodeServerResponse = (
export function universalRequestFromNodeRequest(
nodeRequest: NodeServerRequest,
parsedJsonBody: JsonValue | undefined,
contextValues: ContextValues | undefined,
): UniversalServerRequest {
const encrypted =
"encrypted" in nodeRequest.socket && nodeRequest.socket.encrypted;
Expand Down Expand Up @@ -125,6 +127,7 @@ export function universalRequestFromNodeRequest(
header: nodeHeaderToWebHeader(nodeRequest.headers),
body,
signal: abortController.signal,
values: contextValues,
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/connect-web-bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ it like a web server would usually do.

| code generator | bundle size | minified | compressed |
|----------------|-------------------:|-----------------------:|---------------------:|
| connect | 113,658 b | 49,964 b | 13,487 b |
| connect | 113,658 b | 49,964 b | 13,486 b |
| grpc-web | 414,071 b | 300,352 b | 53,255 b |
51 changes: 51 additions & 0 deletions packages/connect/src/context-values.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2021-2023 The Connect Authors
//
// 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 { createContextKey, createContextValues } from "./context-values.js";

describe("ContextValues", function () {
it("should get default values", function () {
const contextValues = createContextValues();
const kString = createContextKey("default");
expect(contextValues.get(kString)).toBe(kString.defaultValue);
});
it("should set values", function () {
const contextValues = createContextValues();
const kString = createContextKey("default");
contextValues.set(kString, "foo");
expect(contextValues.get(kString)).toBe("foo");
});
it("should delete values", function () {
const contextValues = createContextValues();
const kString = createContextKey("default");
contextValues.set(kString, "foo");
contextValues.delete(kString);
expect(contextValues.get(kString)).toBe(kString.defaultValue);
});
it("should work with undefined values", function () {
const contextValues = createContextValues();
const kUndefined = createContextKey<string | undefined>("default");
expect(contextValues.get(kUndefined)).toBe(kUndefined.defaultValue);
contextValues.set(kUndefined, undefined);
expect(contextValues.get(kUndefined)).toBe(undefined);
});
it("should be properties on the type", function () {
const contextValues = createContextValues();
const kString = createContextKey("default", { description: "string" });
contextValues.set(kString, "foo");
expect(contextValues).toEqual(
jasmine.objectContaining({ [kString.id]: "foo" }),
);
});
});
Loading