michaelschufi
michaelschufiā€¢4w ago

How do I use the rsc-rq-prefetch example with a protected procedure?

Thank you for this implementation! It looks sooo promising šŸ˜€ However, I ran into an issue when trying it out: https://github.com/trpc/trpc/blob/next/examples/.experimental/next-app-dir/src/app/rsc-rq-prefetch/page.tsx If we have a protected procedure instead of a public one, the server is not authenticated during SSR and an error is thrown. How can we solve this? Can we just ignore the error and will it still work?
GitHub
trpc/examples/.experimental/next-app-dir/src/app/rsc-rq-prefetch/pa...
šŸ§™ā€ā™€ļø Move Fast and Break Nothing. End-to-end typesafe APIs made easy. - trpc/trpc
12 Replies
testsubject1137
testsubject1137ā€¢4w ago
I think something changed between rc.403 and current RC versions. I switched from npm to pnpm today and it updated me to the latest RC and this broke. It started trying to run the client query on the server which caused my console to go nuts with a huge error output. Rolling back to 403 and it's working again.
michaelschufi
michaelschufiā€¢4w ago
oh my god, yep. that was the issue .... sigh, I was so confused Is there a changelog of some kind so I know if the PR is even released in rc.403?
testsubject1137
testsubject1137ā€¢4w ago
My guess is this is related: https://github.com/trpc/trpc/pull/5810
GitHub
fix(react-query): return same Proxy-object to avoid infinite recu...
Closes #5808 šŸŽÆ Changes This is a bit of a mindfuck. āœ… Checklist I have followed the steps listed in the Contributing guide. If necessary, I have added documentation related to the changes made. ...
michaelschufi
michaelschufiā€¢3w ago
Hi @testsubject1137 Have you had some success with more recent versions in the meantime? Even with rc.403, I'm getting an error in my isAuthed trpc middleware.
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.auth.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' });
}

return next({
ctx: {
auth: ctx.auth,
},
});
});
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.auth.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' });
}

return next({
ctx: {
auth: ctx.auth,
},
});
});
since, as you've said, the server is running the client component query during ssr.
testsubject1137
testsubject1137ā€¢3w ago
This is how I'm doing it, and it's working on rc.403.
export const publicProcedure = t.procedure;

export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

return next({
ctx: {
session: {
...ctx.session,
user: ctx.session.user,
},
},
});
});
export const publicProcedure = t.procedure;

export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

return next({
ctx: {
session: {
...ctx.session,
user: ctx.session.user,
},
},
});
});
michaelschufi
michaelschufiā€¢3w ago
Hmm, does your trpc client component client run during SSR? I'm experiencing strange behaviour... My next.js receives 3 trpc requests for the following page: foo/page.tsx
import { HydrateClient, trpc } from '@/app/api/trpc/client/rsc';
import { Foo } from '../../foo';

const FooPage = async () => {
void trpc.foo.prefetch();

return (
<HydrateClient>
<Foo />
</HydrateClient>
);
};

export default FooPage;
import { HydrateClient, trpc } from '@/app/api/trpc/client/rsc';
import { Foo } from '../../foo';

const FooPage = async () => {
void trpc.foo.prefetch();

return (
<HydrateClient>
<Foo />
</HydrateClient>
);
};

export default FooPage;
Server Logs
[MIDDLEWARE]: http://localhost:3000/foo null
createContextCached []
isAuthed { 'x-trpc-source': 'react-query-ssr', 'ctx.auth.userId': 'missing' }
a [TRPCClientError]: Not authenticated
at ...
(error info of the previous posted error)
}
isAuthed { 'x-trpc-source': 'rsc', 'ctx.auth.userId': 'exists' }
isAuthed { 'x-trpc-source': 'react-query', 'ctx.auth.userId': 'exists' }
[MIDDLEWARE]: http://localhost:3000/foo null
createContextCached []
isAuthed { 'x-trpc-source': 'react-query-ssr', 'ctx.auth.userId': 'missing' }
a [TRPCClientError]: Not authenticated
at ...
(error info of the previous posted error)
}
isAuthed { 'x-trpc-source': 'rsc', 'ctx.auth.userId': 'exists' }
isAuthed { 'x-trpc-source': 'react-query', 'ctx.auth.userId': 'exists' }
rsc.ts (the RSC query client)
const createContextCached = cache(
async (...args: unknown[]): Promise<Context> => {
console.log('createContextCached', args);
const _headers = new Headers(headers());
_headers.set('x-trpc-source', 'rsc');

const authObj = auth();

return {
req: undefined,
headers: Object.fromEntries(_headers),
auth: authObj,
identity: await getIdentity(authObj),
};
},
);

/**
* Create a stable getter for the query client that
* will return the same client during the same request.
*/
const getQueryClient = cache(createQueryClient);
const caller = createCallerFactory(appRouter)(createContextCached);

export const { trpc, HydrateClient } = createHydrationHelpers<typeof appRouter>(
caller,
getQueryClient,
);
const createContextCached = cache(
async (...args: unknown[]): Promise<Context> => {
console.log('createContextCached', args);
const _headers = new Headers(headers());
_headers.set('x-trpc-source', 'rsc');

const authObj = auth();

return {
req: undefined,
headers: Object.fromEntries(_headers),
auth: authObj,
identity: await getIdentity(authObj),
};
},
);

/**
* Create a stable getter for the query client that
* will return the same client during the same request.
*/
const getQueryClient = cache(createQueryClient);
const caller = createCallerFactory(appRouter)(createContextCached);

export const { trpc, HydrateClient } = createHydrationHelpers<typeof appRouter>(
caller,
getQueryClient,
);
rcc.tsx
export const trpc = createTRPCReact<AppRouter>();

let clientQueryClientSingleton: QueryClient | undefined = undefined;
const getQueryClient = () => {
if (typeof window === 'undefined') {
// Server: always make a new query client
return createQueryClient();
} else {
// Browser: use singleton pattern to keep the same query client
return (clientQueryClientSingleton ??= createQueryClient());
}
};

export function Provider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();

const [trpcClient] = useState(() =>
trpc.createClient({
links: [
unstable_httpBatchStreamLink({
transformer,
url: getBaseUrl() + '/api/trpc',
headers: {
'x-trpc-source':
typeof window !== 'undefined' ? 'react-query' : 'react-query-ssr',
},
}),
],
}),
);

return ...
export const trpc = createTRPCReact<AppRouter>();

let clientQueryClientSingleton: QueryClient | undefined = undefined;
const getQueryClient = () => {
if (typeof window === 'undefined') {
// Server: always make a new query client
return createQueryClient();
} else {
// Browser: use singleton pattern to keep the same query client
return (clientQueryClientSingleton ??= createQueryClient());
}
};

export function Provider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();

const [trpcClient] = useState(() =>
trpc.createClient({
links: [
unstable_httpBatchStreamLink({
transformer,
url: getBaseUrl() + '/api/trpc',
headers: {
'x-trpc-source':
typeof window !== 'undefined' ? 'react-query' : 'react-query-ssr',
},
}),
],
}),
);

return ...
foo.tsx (Component with useSuspenseQuery)
'use client';

import { trpc } from '../api/trpc/client/rcc';

export function Foo() {
const [foo] = trpc.foo.useSuspenseQuery();

return <div>{JSON.stringify(foo)}</div>;
}
'use client';

import { trpc } from '../api/trpc/client/rcc';

export function Foo() {
const [foo] = trpc.foo.useSuspenseQuery();

return <div>{JSON.stringify(foo)}</div>;
}
So what I would expect is: The trpc handler should be hit 1-2 times instead of 3 times. - Once for the void trpc.foo.prefetch(); in page.tsx ('x-trpc-source': 'rsc') - Maybe once for the trpc.foo.useSuspenseQuery(); in foo.tsx ('x-trpc-source': 'react-query') in case the server prefetch failed. But it should never be called during SSR with the trpc client component in rcc.tsx ('x-trpc-source': 'react-query-ssr'). Are those assumptions correct?
testsubject1137
testsubject1137ā€¢3w ago
Do you have all of the packages locked to 403?
"@trpc/client": "11.0.0-rc.403",
"@trpc/next": "11.0.0-rc.403",
"@trpc/react-query": "11.0.0-rc.403",
"@trpc/server": "11.0.0-rc.403",
"@trpc/client": "11.0.0-rc.403",
"@trpc/next": "11.0.0-rc.403",
"@trpc/react-query": "11.0.0-rc.403",
"@trpc/server": "11.0.0-rc.403",
I know I had some unexpected behavior when I didn't. I have not tested to see how many/which calls tRPC is making, but my auth does work just fine. However, I don't know if it makes any difference, but mine is set up a little differently. Maybe this will help a little. /server/api/trpc.ts
export const createTRPCContext = async (opts: { headers: Headers }) => {
const db = getDatabase();
const user = await getUser();
const { ctx, env, cf } = getRequestContext();

return {
db,
user,
cf: {
...ctx,
env,
...cf,
},
...opts,
};
};

const t = initTRPC.context<typeof createTRPCContext>().create({
transformer,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});

export const createCallerFactory = t.createCallerFactory;

export const createTRPCRouter = t.router;

export const publicProcedure = t.procedure;

export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

return next({
ctx: {
...ctx,
// infers the `session` as non-nullable
user: ctx.user,
},
});
});
export const createTRPCContext = async (opts: { headers: Headers }) => {
const db = getDatabase();
const user = await getUser();
const { ctx, env, cf } = getRequestContext();

return {
db,
user,
cf: {
...ctx,
env,
...cf,
},
...opts,
};
};

const t = initTRPC.context<typeof createTRPCContext>().create({
transformer,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});

export const createCallerFactory = t.createCallerFactory;

export const createTRPCRouter = t.router;

export const publicProcedure = t.procedure;

export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

return next({
ctx: {
...ctx,
// infers the `session` as non-nullable
user: ctx.user,
},
});
});
/trpc/server.ts
const createContext = cache(() => {
const heads = new Headers(headers());
heads.set("x-trpc-source", "rsc");

return createTRPCContext({
headers: heads,
});
});

const caller = createCaller(createContext);

export const getQueryClient = cache(createQueryClient);

export const { HydrateClient, trpc: api } = createHydrationHelpers<AppRouter>(
caller,
getQueryClient,
);
const createContext = cache(() => {
const heads = new Headers(headers());
heads.set("x-trpc-source", "rsc");

return createTRPCContext({
headers: heads,
});
});

const caller = createCaller(createContext);

export const getQueryClient = cache(createQueryClient);

export const { HydrateClient, trpc: api } = createHydrationHelpers<AppRouter>(
caller,
getQueryClient,
);
/trpc/react.tsx
let clientQueryClientSingleton: QueryClient | undefined = undefined;

const getQueryClient = () => {
if (typeof window === "undefined") {
// Server: always make a new query client
return createQueryClient();
}
// Browser: use singleton pattern to keep the same query client
return (clientQueryClientSingleton ??= createQueryClient());
};

export const api = createTRPCReact<AppRouter>();

export function TRPCReactProvider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();

const [trpcClient] = useState(() =>
api.createClient({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
unstable_httpBatchStreamLink({
transformer,
url: getTRPCUrl(),
headers: () => {
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return headers;
},
}),
],
}),
);

return (
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{props.children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</api.Provider>
);
}
let clientQueryClientSingleton: QueryClient | undefined = undefined;

const getQueryClient = () => {
if (typeof window === "undefined") {
// Server: always make a new query client
return createQueryClient();
}
// Browser: use singleton pattern to keep the same query client
return (clientQueryClientSingleton ??= createQueryClient());
};

export const api = createTRPCReact<AppRouter>();

export function TRPCReactProvider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();

const [trpcClient] = useState(() =>
api.createClient({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
unstable_httpBatchStreamLink({
transformer,
url: getTRPCUrl(),
headers: () => {
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return headers;
},
}),
],
}),
);

return (
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{props.children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</api.Provider>
);
}
michaelschufi
michaelschufiā€¢3w ago
Yes, I have
"@tanstack/react-query": "^5.49.2",
"@tanstack/react-query-devtools": "^5.49.2",

"@trpc/client": "11.0.0-rc.403",
"@trpc/react-query": "11.0.0-rc.403",
"@trpc/server": "11.0.0-rc.403",
"@tanstack/react-query": "^5.49.2",
"@tanstack/react-query-devtools": "^5.49.2",

"@trpc/client": "11.0.0-rc.403",
"@trpc/react-query": "11.0.0-rc.403",
"@trpc/server": "11.0.0-rc.403",
What version of tanstack react-query do you use? Maybe that's the issue and not trpc itself... Since, I don't think trpc is deciding if it should run during SSR or not... If you do something like this
headers: {
'x-trpc-source':
typeof window !== 'undefined' ? 'react-query' : 'react-query-ssr',
},
headers: {
'x-trpc-source':
typeof window !== 'undefined' ? 'react-query' : 'react-query-ssr',
},
in the httpBatchStream, and also log the headers in the protected procedure middleware - does it get logged? Because I think my setup is wrongfully calling the useSuspense query on the server during SSR and, since the server during rendering is somehow not authed, it fails...
testsubject1137
testsubject1137ā€¢3w ago
I can test this for you tonight. @michaelschufi I actually just get one x-trpc-source: rsc call
michaelschufi
michaelschufiā€¢2w ago
Thank you! This helps a lot. Now I know that this is the issue šŸ™
testsubject1137
testsubject1137ā€¢2w ago
Let me know if there are any code samples I can share to help you narrow it down. Oh! "@tanstack/react-query": "^5.49.0",
michaelschufi
michaelschufiā€¢2w ago
i'll try upgrading soon to the newest rc anyways. because of #Can I use the "Streaming with Server Components" strategy with tRPC? seems to be working.
More Posts
how to handle or hydrate error throwed in prefetch with streaming?i tried next-app-dir / rsc-rq-prefetch example, but there have only success case, not failed. if i tProcedure passed as argument to function loses type safety.Hi there, I have a mystifying TS issue, where I lose type safety of my proc when I pass it as an argError "Subscriptions should use wsLink"I have made a stackoverflow post, hoping if I can get some help! https://stackoverflow.com/questionsWhy are `new QueryClient` and `trpc.createClient` run inside a component in the React setup?From <https://trpc.io/docs/client/react/setup>:```ts function App() { const [queryClient] = useStaHow are errors handled in batched requestsWe're using httpBatchLink, and have noticed that when multiple procedures calls are batched togetherFile upload with formdataI'm trying to upload a pdf file to R2 and save metadata to my postgres database (both through a trpcZod formatting ErrorI'm using Zod for validation and whenever I get a validation error I'd like to be able to get the erWhen a procedure returns an instance of a class, the superjson isApplicable doesnt detect itWhen a procedure returns an instance of a class, the superjson isApplicable doesnt detect it as thatValidating inputs and outputs only via typescriptHey everyone, I am trying to create an appRouter with `ts-morph` but I am having trouble with passinZod error not being formattedI'm using Zod for validation and whenever I get a validation error I'd like to be able to get the er