Client interceptor usage #652
-
I'm loving this project -- it fills so many of the gaps that tRPC left open. I've been poking at it for a few days and I have a question about interceptors on the client side. My intent here is to create a client factory that creates a prometheus histogram or similar to measure response times from the client. Initially I tried this using import { createORPCClient, onStart } from "@orpc/client";
/* other imports */
export function createClient(
baseUrl: string
): ContractRouterClient<typeof myServerContract> {
const link = new RPCLink({
clientInterceptors: [
onStart(async ({ next }) => {
const startTime = performance.now();
try {
return next();
} finally {
const duration = performance.now() - startTime;
observeRequestDuration(duration);
}
}),
],
url: `${baseUrl}/rpc`,
});
return createORPCClient(link);
}
const client = createClient('https://url');
const result = await client.myProcedure(); // Calls the server twice This behaved strangely -- the client was calling the server twice for each invocation. Then, I tried removing the export function createClient(
baseUrl: string
): ContractRouterClient<typeof myServerContract> {
const link = new RPCLink({
clientInterceptors: [
async ({ next }) => {
const startTime = performance.now();
try {
return next();
} finally {
const duration = performance.now() - startTime;
observeRequestDuration(duration);
}
},
],
url: `${baseUrl}/rpc`,
});
return createORPCClient(link);
} This one works as expected. I looked through all the playgrounds and docs and I saw examples of customizing the headers, fetch, and methods of the So what I'm wondering is if I'm using this right and it just isn't documented, or if this isn't meant to be done at all, or if I've just missed the documentation. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
You ran into a subtle but important behavior with orpc's onStart helper. The onStart function is designed to run your callback and then always call next() itself—so if your callback also calls next(), you end up triggering the next handler twice, which results in two requests to the server per call. This is why your first example caused double requests: your callback called next(), and then onStart called it again after your callback finished. The intended use for onStart is to perform side effects (like starting a timer or logging), but not to call or await next() inside your callback. If you need to wrap the request and measure duration, writing your own interceptor (as in your second example) is the right approach and matches the intended pattern. There isn’t any documentation or playground example covering this pitfall, so your approach and findings are spot on. If you want to use onStart, just do your side effect in the callback and let onStart handle the flow; for more control, use a custom interceptor as you did. References: To reply, just mention @dosu. How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other |
Beta Was this translation helpful? Give feedback.
-
@dosu You're a #hero. Tell you robot brethren about this for me. |
Beta Was this translation helpful? Give feedback.
You ran into a subtle but important behavior with orpc's onStart helper. The onStart function is designed to run your callback and then always call next() itself—so if your callback also calls next(), you end up triggering the next handler twice, which results in two requests to the server per call. This is why your first example caused double requests: your callback called next(), and then onStart called it again after your callback finished.
The intended use for onStart is to perform side effects (like starting a timer or logging), but not to call or await next() inside your callback. If you need to wrap the request and measure duration, writing your own interceptor (as in your second …