Closed ThomasBouasli closed 7 months ago
I also faced this exact issue, as my static pages were not working and it took me some time to figure out the TRPCProvider wrapped around the whole app was the culprit, as headers()
are passed from there to client component.
+1.
This has been bothering me for quite a long time. I really don't know if it is possible, but would love it if it was
I found that the only way to do this is to use
export const dynamic = "force-static"
But it sucks having to do it like this
@dBianchii I don't know how I didn't thought of this tbh. It's bad but better than ditching TRPC. One thing that I'm curious about is how partial pre-rendering will affect this, the content will load faster than the layout, which will probably look weird.
@dBianchii I don't know how I didn't thought of this tbh. It's bad but better than ditching TRPC. One thing that I'm curious about is how partial pre-rendering will affect this, the content will load faster than the layout, which will probably look weird.
It does not work with PPR. If you go to canary and turn it on, building with next build
completely fails.
I think T3 should definitely fix this. I am not sure if it is something that needs to wait TRPC v11
That sucks. I do thinks it's more of an TRPC problem than anything else.
@dBianchii I don't know how I didn't thought of this tbh. It's bad but better than ditching TRPC. One thing that I'm curious about is how partial pre-rendering will affect this, the content will load faster than the layout, which will probably look weird.
It does not work with PPR. If you go to canary and turn it on, building with
next build
completely fails.I think T3 should definitely fix this. I am not sure if it is something that needs to wait TRPC v11
I just remove the cookies
function from context and not passing it to the Provider. With app router, we can easily call the cookies function in middleware when needed
Also, got PPR working with unstable_noStore
function.
@raymclee Can you share what you did in your middleware? I don't get how you're passing cookies to the TRPC route with this method.
I eliminated the cookies
function entirely from the root layout. I believe it's unnecessary in that context since its sole purpose was to be passed to the client component, which instantiates the trpcClient
for React, and then we pass it as headers. If we look closely, it becomes evident that we don't actually need these headers to be transmitted from unstable_httpBatchStreamLink
. In every browser-to-server request, all the required headers, including the HTTP cookies, are automatically transmitted from the browser. Therefore, if we only need to pass some additional headers, we can simply use new Headers()
and pass them, as demonstrated in the following manner.
// react.tsx file
// in trpcClient instantiation using clientApi.createClient
unstable_httpBatchStreamLink({
url: getUrl(),
headers() {
// const heads = new Map(props.headers); REMOVED THIS!
const heads = new Map(new Headers());
heads.set("x-trpc-source", "react"); // setting some custom headers
return Object.fromEntries(heads);
},
})
However I could be completely wrong about my assumption of the usage of headers()
in this unstable_httpBatchStreamLink
function, and maybe that headers()
were serving something which I am missing.
But this workaround works for me with proper auth.
I did the same a few days ago; I didn't send the cookies to the provider through next/headers
because this causes the page to opt-in to SSR. Instead, I passed window.document.cookie
directly.
You can also opt-out of SSR by using export const dynamic = "force-static"
on each page or in the layout. However, removing the import { cookies } from "next/headers"
does the trick as well.
Also, I encountered problems using tRPC within generateStaticParams()
. I needed to create another createTRPCProxyClient()
that does not use cookies in createContext()
. Even though the request does not require authentication, if it tries to read cookies within createTRPCContext()
, it does not work and throws:
Invariant: Method expects to have requestAsyncStorage, none available
To simplify, I divided my procedures/appRouter into public and authenticated. This way, I can use the public ones freely on static pages.
@raymclee Can you share what you did in your middleware? I don't get how you're passing cookies to the TRPC route with this method.
like this
const isAuthed = t.middleware(async (opts) => {
const { ctx } = opts;
const cookie = cookies().get("token")?.value;
const session = await getSession(cookie);
if (!session) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return opts.next({ ctx: { session} });
});
I think you could even call it inside procedure
I did the same a few days ago; I didn't send the cookies to the provider through
next/headers
because this causes the page to opt-in to SSR. Instead, I passedwindow.document.cookie
directly.You can also opt-out of SSR by using
export const dynamic = "force-static"
on each page or in the layout. However, removing theimport { cookies } from "next/headers"
does the trick as well.Also, I encountered problems using tRPC within
generateStaticParams()
. I needed to create anothercreateTRPCProxyClient()
that does not use cookies increateContext()
. Even though the request does not require authentication, if it tries to read cookies withincreateTRPCContext()
, it does not work and throws:Invariant: Method expects to have requestAsyncStorage, none available
To simplify, I divided my procedures/appRouter into public and authenticated. This way, I can use the public ones freely on static pages.
This sounds like a great solution. Could you share an example with us?
@juliusmarminge @c-ehrlich , do you have any opinion on these issues?
I did the same a few days ago; I didn't send the cookies to the provider through
next/headers
because this causes the page to opt-in to SSR. Instead, I passedwindow.document.cookie
directly. You can also opt-out of SSR by usingexport const dynamic = "force-static"
on each page or in the layout. However, removing theimport { cookies } from "next/headers"
does the trick as well. Also, I encountered problems using tRPC withingenerateStaticParams()
. I needed to create anothercreateTRPCProxyClient()
that does not use cookies increateContext()
. Even though the request does not require authentication, if it tries to read cookies withincreateTRPCContext()
, it does not work and throws:Invariant: Method expects to have requestAsyncStorage, none available
To simplify, I divided my procedures/appRouter into public and authenticated. This way, I can use the public ones freely on static pages.
This sounds like a great solution. Could you share an example with us?
// server/api/trpc.ts
export async function createTRPCContext(
opts: { headers: Headers },
args?: { unauthenticated: boolean },
) {
if (args?.unauthenticated) {
return { db, ...opts, session: null };
}
const cookieStore = cookies();
const session = await getSession(cookieStore);
return { db, ...opts, session };
}
// server/api/root.ts
export const appRouter = router({...});
export const publicRouter = router({...});
// lib/trpc/server.ts
const createContext = cache(() => {
return createTRPCContext({
headers: new Headers({
cookie: cookies().toString(),
"x-trpc-source": "rsc",
}),
});
});
const createPublicContext = cache(() => {
return createTRPCContext(
{ headers: new Headers({ cookie: "", "x-trpc-source": "rsc" }) },
{ unauthenticated: true },
);
});
const serverApp = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
...,
() =>
({ op }) =>
observable((observer) => {
createContext() // authenticated context
.then((ctx) => {
return callProcedure({
procedures: appRouter._def.procedures, // authenticated procedures
...
});
}),
...
}),
],
});
const serverPublic = createTRPCProxyClient<PublicRouter>({
transformer: superjson,
links: [
...,
() =>
({ op }) =>
observable((observer) => {
createPublicContext() // public context
.then((ctx) => {
return callProcedure({
procedures: publicRouter._def.procedures, // public procedures
...
});
})
...
}),
],
});
export const server = {
app: serverApp,
public: serverPublic,
};
// app/post/page.tsx
const post = await server.app.post.get.query(); // authenticated
const info = await server.public.info.get.query(); // public
I think I can still refine the implementation, but it works very well for those who want to separate procedures into public and authenticated ones.
I believe that @raymclee solution is also good, moving access to cookies only to the middleware, you can skip all the steps I took and maybe have just one appRouter, I think it can simplify the implementation, but I don't know if there is any disadvantage in this approach.
Thank you @danieljpgo
I was interested in this thread based on principle...
...but I just got derailed on a side-project because it's impossible to create a dynamic route and use generateStaticParams()
As reported by @danieljpgo :
⨯ Failed to generate static paths for /[dashboard]:
Error [TRPCClientError]: Invariant: cookies() expects to have requestAsyncStorage, none available.
at TRPCClientError.from (webpack-internal:///(rsc)/./node_modules/@trpc/client/dist/TRPCClientError-0de4d231.mjs:41:16)
at eval (webpack-internal:///(rsc)/./node_modules/@trpc/client/dist/index.mjs:69:85)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
type: 'TRPCClientError'
}
This is the issue preventing static generation https://github.com/vercel/next.js/issues/60009
I eliminated the cookies function entirely from the root layout. I believe it's unnecessary in that context since its sole purpose was to be passed to the client component, which instantiates the trpcClient for React, and then we pass it as headers. If we look closely, it becomes evident that we don't actually need these headers to be transmitted from unstable_httpBatchStreamLink. In every browser-to-server request, all the required headers, including the HTTP cookies, are automatically transmitted from the browser. Therefore, if we only need to pass some additional headers, we can simply use new Headers() and pass them, as demonstrated in the following manner.
This is not the case when the client components are SSRed, then we need the headers passed from the server component. This should be possible using a Promise, which will never be consumed if you don't fetch anything on that page. I filed the above issue as I want to get this working too...
are there any updates or solutions to this issue? btw:
@dBianchii I don't know how I didn't thought of this tbh. It's bad but better than ditching TRPC. One thing that I'm curious about is how partial pre-rendering will affect this, the content will load faster than the layout, which will probably look weird.
It does not work with PPR. If you go to canary and turn it on, building with
next build
completely fails. I think T3 should definitely fix this. I am not sure if it is something that needs to wait TRPC v11I just remove the
cookies
function from context and not passing it to the Provider. With app router, we can easily call the cookies function in middleware when neededAlso, got PPR working with
unstable_noStore
function.
even if I remove the cookies
function from the layout it still shows the same error and all pages become SSR, do you maybe have your code for layout.tsx
and trpc/react.tsx
@raymclee ?
I just now realized that simple deletion of trpc/server.ts
resolves this problem entirely. If you don't use trpc within server components, and instead treat it as a client-only method of communication, no server context is needed.
Am I missing something here?
Is your feature request related to a problem? Please describe.
Since TRPCReactProvider is being render in the root layout and has cookies passing down to it, it counts as an dynamic function and opts out of the Full Route Cache.
Describe the solution you'd like to see
A way for the default Full Route Cache to be used and have TRPCProvider as a top level provider, I don't know if this is possible though.
Describe alternate solutions
Maybe address that as a comment so it's clearer? I had trouble figuring out why my static pages weren't actually static.
Additional information
No response