Skip to content

Commit

Permalink
Add support for server side interceptors (#982)
Browse files Browse the repository at this point in the history
  • Loading branch information
srikrsna-buf authored Jan 23, 2024
1 parent 5f05173 commit fd34d07
Show file tree
Hide file tree
Showing 12 changed files with 553 additions and 36 deletions.
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 | 117,695 b | 51,217 b | 13,730 b |
| connect | 117,734 b | 51,217 b | 13,697 b |
| grpc-web | 415,212 b | 300,936 b | 53,420 b |
1 change: 1 addition & 0 deletions packages/connect/src/implementation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe("createHandlerContext()", function () {
method: TestService.methods.unary,
protocolName: "foo",
requestMethod: "GET",
url: "https://example.com/foo",
};

describe("signal", function () {
Expand Down
8 changes: 7 additions & 1 deletion packages/connect/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import type {
MessageType,
MethodIdempotency,
MethodInfo,
MethodKind,
PartialMessage,
ServiceType,
} from "@bufbuild/protobuf";
import { MethodKind } from "@bufbuild/protobuf";
import { ConnectError } from "./connect-error.js";
import { Code } from "./code.js";
import {
Expand Down Expand Up @@ -124,6 +124,11 @@ export interface HandlerContext {
* Per RPC context values that can be used to pass data to handlers.
*/
readonly values: ContextValues;

/**
* The URL received by the server.
*/
readonly url: string;
}

/**
Expand All @@ -134,6 +139,7 @@ interface HandlerContextInit {
method: MethodInfo;
protocolName: string;
requestMethod: string;
url: string;
timeoutMs?: number;
shutdownSignal?: AbortSignal;
requestSignal?: AbortSignal;
Expand Down
30 changes: 28 additions & 2 deletions packages/connect/src/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
import type { ContextValues } from "./context-values.js";

/**
* An interceptor can add logic to clients, similar to the decorators
* An interceptor can add logic to clients or servers, similar to the decorators
* or middleware you may have seen in other libraries. Interceptors may
* mutate the request and response, catch errors and retry/recover, emit
* logs, or do nearly everything else.
Expand All @@ -40,6 +40,10 @@ import type { ContextValues } from "./context-values.js";
* actual HTTP request is run by the transport. The response then comes back
* through all layers and is returned to the client.
*
* Similarly, a request received by a server goes through the outermost layer
* first. In the center, the actual HTTP request is received by the handler. The
* response then comes back through all layers and is returned to the client.
*
* To implement that layering, Interceptors are functions that wrap a call
* invocation. In an array of interceptors, the interceptor at the end of
* the array is applied first.
Expand Down Expand Up @@ -147,7 +151,8 @@ interface RequestCommon<I extends Message<I>, O extends Message<O>> {
readonly method: MethodInfo<I, O>;

/**
* The URL the request is going to hit.
* The URL the request is going to hit for the clients or the
* URL received by the server.
*/
readonly url: string;

Expand Down Expand Up @@ -195,3 +200,24 @@ interface ResponseCommon<I extends Message<I>, O extends Message<O>> {
*/
readonly trailer: Headers;
}

/**
* applyInterceptors takes the given UnaryFn or ServerStreamingFn, and wraps
* it with each of the given interceptors, returning a new UnaryFn or
* ServerStreamingFn.
*/
export function applyInterceptors<T>(
next: T,
interceptors: Interceptor[] | undefined,
): T {
return (
(interceptors
?.concat()
.reverse()
.reduce(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
(n, i) => i(n),
next as any, // eslint-disable-line @typescript-eslint/no-explicit-any
) as T) ?? next
);
}
10 changes: 9 additions & 1 deletion packages/connect/src/protocol-connect/handler-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ function createUnaryHandler<I extends Message<I>, O extends Message<O>>(
shutdownSignal: opt.shutdownSignal,
requestSignal: req.signal,
requestHeader: req.header,
url: req.url,
responseHeader: {
[headerContentType]: type.binary
? contentTypeUnaryProto
Expand Down Expand Up @@ -241,7 +242,12 @@ function createUnaryHandler<I extends Message<I>, O extends Message<O>>(
serialization,
reqBody,
);
const output = await invokeUnaryImplementation(spec, context, input);
const output = await invokeUnaryImplementation(
spec,
context,
input,
opt.interceptors,
);
body = serialization.getO(type.binary).serialize(output);
} catch (e) {
let error: ConnectError | undefined;
Expand Down Expand Up @@ -375,6 +381,7 @@ function createStreamHandler<I extends Message<I>, O extends Message<O>>(
shutdownSignal: opt.shutdownSignal,
requestSignal: req.signal,
requestHeader: req.header,
url: req.url,
responseHeader: {
[headerContentType]: type.binary
? contentTypeStreamProto
Expand Down Expand Up @@ -422,6 +429,7 @@ function createStreamHandler<I extends Message<I>, O extends Message<O>>(
const it = transformInvokeImplementation<I, O>(
spec,
context,
opt.interceptors,
)(inputIt)[Symbol.asyncIterator]();
const outputIt = pipe(
// We wrap the iterator in an async iterator to ensure that the
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/protocol-grpc-web/handler-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ function createHandler<I extends Message<I>, O extends Message<O>>(
shutdownSignal: opt.shutdownSignal,
requestSignal: req.signal,
requestHeader: req.header,
url: req.url,
responseHeader: {
[headerContentType]: type.binary ? contentTypeProto : contentTypeJson,
},
Expand Down Expand Up @@ -173,6 +174,7 @@ function createHandler<I extends Message<I>, O extends Message<O>>(
const it = transformInvokeImplementation<I, O>(
spec,
context,
opt.interceptors,
)(inputIt)[Symbol.asyncIterator]();
const outputIt = pipe(
// We wrap the iterator in an async iterator to ensure that the
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/protocol-grpc/handler-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ function createHandler<I extends Message<I>, O extends Message<O>>(
shutdownSignal: opt.shutdownSignal,
requestSignal: req.signal,
requestHeader: req.header,
url: req.url,
responseHeader: {
[headerContentType]: type.binary ? contentTypeProto : contentTypeJson,
},
Expand Down Expand Up @@ -159,6 +160,7 @@ function createHandler<I extends Message<I>, O extends Message<O>>(
const it = transformInvokeImplementation<I, O>(
spec,
context,
opt.interceptors,
)(inputIt)[Symbol.asyncIterator]();
const outputIt = pipe(
// We wrap the iterator in an async iterator to ensure that the
Expand Down
Loading

0 comments on commit fd34d07

Please sign in to comment.