Skip to content

Commit

Permalink
Add docs for handling timeouts on the server (#156)
Browse files Browse the repository at this point in the history
This adds docs to the Connect for Node.js section for handling timeouts
on the server-side. In addition, it updates the Server Plugins page to
include the up-to-date options allowed.

---------

Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Timo Stamm <[email protected]>
Signed-off-by: dependabot[bot] <[email protected]>
Signed-off-by: Carol Gunby <[email protected]>
Signed-off-by: hirasawayuki <[email protected]>
Signed-off-by: Nick Snyder <[email protected]>
Signed-off-by: Philip K. Warren <[email protected]>
Signed-off-by: Dan Rice <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Timo Stamm <[email protected]>
Co-authored-by: Stefan VanBuren <[email protected]>
Co-authored-by: Derek Perez <[email protected]>
Co-authored-by: Nick Snyder <[email protected]>
Co-authored-by: Carol Gunby <[email protected]>
Co-authored-by: Yuki Hirasawa <[email protected]>
Co-authored-by: Philip K. Warren <[email protected]>
Co-authored-by: Dan Rice <[email protected]>
Co-authored-by: SimulShift <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
  • Loading branch information
11 people committed Sep 30, 2024
1 parent 645eebd commit 034044c
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 12 deletions.
6 changes: 3 additions & 3 deletions docs/node/interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { connectNodeAdapter } from "@connectrpc/connect-node";
import type { Interceptor } from "@connectrpc/connect";

const logger: Interceptor = (next) => async (req) => {
console.log(`recevied message on ${req.url}`);
console.log(`received message on ${req.url}`);
return await next(req);
};

Expand All @@ -39,7 +39,7 @@ To intercept responses, we simply look at the return value of `next()`:

```ts
const logger: Interceptor = (next) => async (req) => {
console.log(`recevied message on ${req.url}`);
console.log(`received message on ${req.url}`);
const res = await next(req);
if (!res.stream) {
console.log("message:", res.message);
Expand Down Expand Up @@ -231,4 +231,4 @@ await server.listen({
});
```

The request passed to the `contextValues` function is different for each server plugin, please refer to the documentation for the server plugin you are using.
The request passed to the `contextValues` function is different for each server plugin, please refer to the documentation for the server plugin you are using.
31 changes: 23 additions & 8 deletions docs/node/server-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ http.createServer(
).listen(8080);
```

The function accepts all common options, and the following additional
The function accepts all [common options](#common-options), and the following additional
ones:

- `fallback?: NodeHandlerFn`<br/>
Expand Down Expand Up @@ -75,7 +75,13 @@ await server.listen({
});
```

The plugin accepts all common options, and the following additional ones:
The plugin accepts all [common options](#common-options), and the following additional ones:
- `shutdownTimeoutMs?: number`<br/>
If set, the server will wait for the specified duration before aborting any
in-flight requests once [`fastify.close`](https://fastify.dev/docs/latest/Reference/Server/#close) is called.
- `shutdownError?: unknown`<br/>
The reason to use when shutdown occurs. Note that if this is a `ConnectError` it will
be sent to the client.
- `contextValues?: (req: FastifyRequest) => ContextValues`<br/>
A function that returns a set of context values for each request. The
context values are passed to the service implementation. See
Expand Down Expand Up @@ -112,9 +118,14 @@ This file is a Next.js [catch-all API route](https://nextjs.org/docs/routing/dyn
serve your Connect RPCs with the `/api` prefix. Make sure to include the `/api` prefix in the `baseUrl` option for
your client transport.

The middleware accepts all common options, and the following additional
The middleware accepts all [common options](#common-options), and the following additional
one:

- `prefix?: string`<br/>
Serve all handlers under this prefix. For example, the prefix "/something"
will serve the RPC foo.FooService/Bar under "/something/foo.FooService/Bar".
By default, this is `/api` for Next.js.<br/>
Note that many gRPC client implementations do not allow for prefixes.
- `contextValues?: (req: NextApiRequest) => ContextValues`<br/>
A function that returns a set of context values for each request. The
context values are passed to the service implementation. See
Expand Down Expand Up @@ -150,13 +161,13 @@ app.use(expressConnectMiddleware({
http.createServer(app).listen(8080);
```

The middleware accepts all common options, and the following additional
The middleware accepts all [common options](#common-options), and the following additional
one:

- `requestPathPrefix?: string`<br/>
Serve all handlers under this prefix. For example, the prefix "/something"
will serve the RPC foo.FooService/Bar under "/something/foo.FooService/Bar".
Note that many gRPC client implementations do not allow for prefixes.
Serve all handlers under this prefix. For example, the prefix "/something"
will serve the RPC foo.FooService/Bar under "/something/foo.FooService/Bar".
Note that many gRPC client implementations do not allow for prefixes.
- `contextValues?: (req: express.Request) => ContextValues`<br/>
A function that returns a set of context values for each request. The
context values are passed to the service implementation. See
Expand All @@ -176,10 +187,14 @@ All adapters take a set of common options:
- `routes: (router: ConnectRouter) => void`<br/>
The adapter will call this function, and lets you register your services.<br/>
See [Implementing services](./implementing-services.md) for an example.
- `maxTimeoutMs?: number`<br/>
The maximum value for [timeouts](./timeouts) that clients may specify.
If a client requests a timeout that is greater than `maxTimeoutMs`,
the server responds with the error code `invalid_argument`.
- `connect?: boolean`<br/>
Whether to enable the Connect protocol for your routes. Enabled by default.
- `grpcWeb?: boolean`<br/>
Whether to enable the gRPC protocol for your routes. Enabled by default.
Whether to enable the gRPC-web protocol for your routes. Enabled by default.
- `grpc?: boolean`<br/>
Whether to enable the gRPC protocol for your routes. Enabled by default.
- `interceptors?: Interceptor[]`<br/>
Expand Down
2 changes: 1 addition & 1 deletion docs/node/testing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Testing
sidebar_position: 6
sidebar_position: 7
---

When writing tests for your Connect for Node.js application, your approach will
Expand Down
84 changes: 84 additions & 0 deletions docs/node/timeouts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: Timeouts
sidebar_position: 8
---

Timeouts can be used to limit the time a server may take to process a response.
In Connect-ES, timeout values are set by the client via the `timeoutMs` option
when issuing a requests. If handling the response takes longer than the timeout,
they will respond with the error code `deadline_exceeded`. In gRPC, the concept
is also known as [deadlines](https://grpc.io/docs/guides/deadlines/).

## Using `HandlerContext`

Servers can interact with this timeout via the [handler context](https://connectrpc.com/docs/node/implementing-services#context).
Depending on your needs, there are a few ways to approach it:

The first way is via an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
on the context. Using this signal, route handlers can then tell if the timeout specified
by the client was reached and abort their processes accordingly. The `AbortSignal` can be found
via the property name `signal`.

The signal can be passed to other functions or used to gracefully stop processes when the timeout is reached.
Using `signal` works for any operation you might want to call as long as the API supports it.

```ts
import type { HandlerContext } from "@bufbuild/connect";

const say = async (req: SayRequest, ctx: HandlerContext) => {

ctx.signal.aborted; // true if timed out
ctx.signal.reason; // an error with code deadline_exceeded if timed out

// raises an error with code deadline_exceeded if timed out
ctx.signal.throwIfAborted();

// the AbortSignal can be passed to other functions
await longRunning(ctx.signal);

return new SayResponse({sentence: `You said: ${req.sentence}`});
};
```

A second way to interact with the timeout value is via the `timeoutMs()` function
on the handler context. If the current request has a timeout, this function
returns the remaining time.

Using the `timeoutMs()` function is preferable when invoking upstream RPC calls
because it is more efficient and robust - you have a guarantee that the peer is
aware of the deadline, regardless of network issues. In gRPC, the concept is also
known as [deadline propagation](https://grpc.io/docs/guides/deadlines/#deadline-propagation).

```ts
import type { HandlerContext } from "@bufbuild/connect";

const say = async (req: SayRequest, ctx: HandlerContext) => {

// If a timeout was set on the call to this service, the timeoutMs() method
// returns the remaining time in milliseconds.

// Passing the value to an upstream client call propagates the timeout.
await upstreamClient.someCall({}, { timeoutMs: ctx.timeoutMs() });

return new SayResponse({sentence: `You said: ${req.sentence}`});
};
```

In addition, to server-side support for timeouts, there is also a related option on `ConnectRouter`
that helps constraining timeout values: `maxTimeoutMs`. For an explanation of this option,
see the docs on [Server Plugins](server-plugins#common-options)

Also note that while this page discusses timeouts in the context of a server, Connect-ES clients
honor timeout values and will raise a `ConnectError` with code `DeadlineExceeded`. Even if a connection
becomes unresponsive, the client call will still abort at the configured timeout.

```ts
try {
// If this call takes more than 200 milliseconds, it is canceled
await client.say({sentence: "Hello"}, { timeoutMs: 200 });
} catch (err) {
if (err instanceof ConnectError && err.code === Code.DeadlineExceeded) {
// handle the timeout error
}
}
```

0 comments on commit 034044c

Please sign in to comment.