You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Expected behavior: Interceptors are called during transport creation, and the resulting handler is reused for every request.
Actual Behaviour: Interceptors are called for every request.
The consequence of this behaviour is that it prevents interceptors from operating across multiple requests in a safe way. See the additional context section for an example.
To Reproduce
letnumInterceptorCalls=0;letnumHandlerCalls=0;constinterceptor: Interceptor=(next)=>{numInterceptorCalls++;returnasync(request)=>{numHandlerCalls++;returnnext(request);};};consttransport=createRouterTransport((router)=>{router.service(GreetingService,{greet: ({ name })=>({greeting: `Hello, ${name}!`}),});},{transport: {interceptors: [interceptor],},},);constclient=createClient(GreetingService,transport);awaitclient.greet({name: "Sun"});awaitclient.greet({name: "Moon"});awaitclient.greet({name: "World"});expect(numHandlerCalls).toBe(3);expect(numInterceptorCalls).toBe(1);// Actual value: 3
Environment
@connectrpc/connect-web version: 2.0.1
@connectrpc/connect-node version: 2.0.1
Frontend framework and version: react@19, next@15
Node.js version: (for example, 23.2.0)
Browser and version: (for example, Google Chrome Version 133.0.6943.127 (Official Build) (arm64))
Additional context
Let's say I wanted to create an interceptor which deduplicates identical requests made to idempotent methods using React's cache function. My expectation was that I could create a request cache inside the interceptor, where I have access to the next handler. The request cache would check each request to see if it matched a previous request and, if it does, produce the cached response.
Note
For brevity, this example only checks the method and request message. It ignores headers, context values, and abort signals.
importassertfrom"node:assert";import{equals}from"@bufbuild/protobuf";import{MethodOptions_IdempotencyLevel}from"@bufbuild/protobuf/wkt";import{Interceptor,UnaryRequest,UnaryResponse}from"@connectrpc/connect";import{cache}from"react";exportconstreactCacheInterceptor: Interceptor=(next)=>async(request)=>{consthandler=getCachedHandler(next);constresponse=awaithandler(request);returnresponse;};constgetCachedHandler=cache<Interceptor>((next)=>{constcache=newMap<UnaryRequest,Promise<UnaryResponse>>();return(request)=>{constcanCache=request.method.methodKind==="unary"&&[MethodOptions_IdempotencyLevel.IDEMPOTENT,MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS,].includes(request.method.idempotency);if(!canCache){returnnext(request);}// TypeScript doesn't know that the method is unary.assert(request.stream===false);for(const[cachedRequest,cachedResponsePromise]ofcache.entries()){if(request.method===cachedRequest.method&&equals(request.method.input,request.message,cachedRequest.message)){returncachedResponsePromise;}}constresponsePromise=next(request).then((response)=>{// TypeScript doesn't know that the method is unary.assert(response.stream===false);returnresponse;});cache.set(request,responsePromise);returnresponsePromise;};});
Unfortunately, this doesn't work. As the interceptor is called for each request, the request cache is created for each request too. As the next argument is also unique for each request, storing caches in a WeakMap and looking them up inside the handler doesn't work either.
There are a few possible workarounds:
Use a global request cache. This is unsafe. If the same interceptor is used in multiple transports, then responses can leak between transports.
Create a custom transport that calls interceptors once.
Create a custom client that dedupes requests before sending them to the transport.
The text was updated successfully, but these errors were encountered:
Describe the bug
The consequence of this behaviour is that it prevents interceptors from operating across multiple requests in a safe way. See the additional context section for an example.
To Reproduce
Environment
23.2.0
)Google Chrome Version 133.0.6943.127 (Official Build) (arm64)
)Additional context
Let's say I wanted to create an interceptor which deduplicates identical requests made to idempotent methods using React's
cache
function. My expectation was that I could create a request cache inside the interceptor, where I have access to thenext
handler. The request cache would check each request to see if it matched a previous request and, if it does, produce the cached response.Note
For brevity, this example only checks the method and request message. It ignores headers, context values, and abort signals.
Unfortunately, this doesn't work. As the interceptor is called for each request, the request cache is created for each request too. As the
next
argument is also unique for each request, storing caches in a WeakMap and looking them up inside the handler doesn't work either.There are a few possible workarounds:
The text was updated successfully, but these errors were encountered: