Fetch that works on both client and server and requires Authorization?
Answered
Sun bear posted this in #help-forum
Sun bearOP
Are there any patterns for writing fetch functions that work on both client and server when the request needs some sort of authorization header from a cookie?
On the client, this header just gets passed automatically by the browser so it doesn't need to be passed to "fetch", but if I want to make the same call on the server (with the same code) I need to pass the cookie value by reading from
On the client, this header just gets passed automatically by the browser so it doesn't need to be passed to "fetch", but if I want to make the same call on the server (with the same code) I need to pass the cookie value by reading from
cookies()
. I'd love to be able to conditionally call this when I know I'm on the server, but you can't even import the function in the client bundle without receiving errors. How can I write an "isomorphic" fetch function?Answered by Tomistoma
getAccessToken in this context is a server action. You would get build errors if you are importing headers or cookies from a non server action.
On the file you're importing the headers and cookies, make sure that's a server action.
On the file you're importing the headers and cookies, make sure that's a server action.
26 Replies
Tomistoma
What you can do is to create a fetcher in your lib then use that fetcher for your fetches. I used to do that with this code as an example:
You can build something similar like this though I am using next auth so I'm getting the access token from there rather than the cookie directly. I've also separated the url of client (browser calls) vs the server url because I have a specific reason to do that. If they're the same for your use case you can just have one fetcher.
import { getAccessToken } from "@/actions/auth";
const serverURL = process.env.NEXT_PUBLIC_BACKEND;
const clientURL = process.env.NEXT_PUBLIC_CLIENT_BACKEND;
export const fetcherServer = async (resource, options = {}) => {
const token = await getAccessToken();
const { method = "GET", headers = {}, ...restOptions } = options;
const response = await fetch(`${serverURL}/${resource}`, {
method,
headers: {
Authorization: `Bearer ${token}`,
...headers,
},
...restOptions,
});
return response;
};
export const fetcherClient = async (resource, init) => {
const token = await getAccessToken();
const response = await fetch(`${clientURL}/${resource}`, {
headers: {
Authorization: `Bearer ${token}`,
},
...init,
});
if (!response.ok) {
const error = new Error("An error occurred while fetching the data.");
error.info = await response.json();
error.status = response.status;
throw error;
}
return response.json();
};
You can build something similar like this though I am using next auth so I'm getting the access token from there rather than the cookie directly. I've also separated the url of client (browser calls) vs the server url because I have a specific reason to do that. If they're the same for your use case you can just have one fetcher.
Sun bearOP
thanks @Tomistoma , my issue is that it seems like even importing the "headers" or "cookies" functions in the client bundle causes build errors. In your example that would be implicitly happening by importing the "getAccessToken" method, since it reads cookies under the hood
Tomistoma
getAccessToken in this context is a server action. You would get build errors if you are importing headers or cookies from a non server action.
On the file you're importing the headers and cookies, make sure that's a server action.
On the file you're importing the headers and cookies, make sure that's a server action.
Answer
Sun bearOP
ah I see, that might work. The only thing that feels wonky there is that I wouldn't want the client code to be geting the access token itself, because the auth details in the cookies are used in a middleware proxy to get access serverside. So I think if I did this then the server action would basically just return nothing when called clientside and it would just be making a dummy call to the backend or something
or i guess it would just skip the call. But I'd also want some way to block calls to the server action from anywhere but the server
Tomistoma
Just create separate fetchers for both client and server. I did that for a different reason which is due to the Docker link not being recognized in the client since the browser calls it.
Sun bearOP
yeah the problem is im trying to write a unified fetcher that I can use in react query so that if a query is encountered on the serverside render then it can start the fetch there, while still being able to refresh and invalidate the same queries on the client
the server action approach works for kind of "tricking it" into letting the build happen even though the function is only ever intended to be called on the server, but now I just need to figure out how to lock it down so that the server action's endpoint can never be called on the client
Sun bearOP
i ended up solving this by just making an environment variable containing a "secret" string which my fetch client passes to the server action. Since its not a NEXT_PUBLIC env var its omitted from the client bundle so only the server knows it, then the action can check if the correct secret was passed in
Korat
Have you checked trpc?
What I think you might need is a BFF approach, where you use the nextjs server as a gateway between your client and your external server
Sun bearOP
what I have right now is basically a middleware reverse proxy between the client and my external API, so the clientside sends requests destined for that external API and they get proxied and auth'd through the nextjs middleware with auth0
it was previously a fully clientside app using nextjs pages router which i migrated to app router, so Im trying to layer on some serverside stuff in places where it makes sense. So I want to be able to render and prefetch data on the server using the same react query fetcher that the client uses so that the same component works when run in both places
it was previously a fully clientside app using nextjs pages router which i migrated to app router, so Im trying to layer on some serverside stuff in places where it makes sense. So I want to be able to render and prefetch data on the server using the same react query fetcher that the client uses so that the same component works when run in both places
basically theres way too much code here to do anything other than getting react query working on the server, can't really switch to a different data fetching system or anything
Korat
Not enough time to experiment ?
Sun bearOP
not really, its a production app with a team working on it so just trying to get some of the benefits of app router without a total rewrite
Korat
creating your own middleware is the most basic solution in this case, but you gotta handle all stuff yourself, another option would be trpc as mentioned
Sun bearOP
hmm actually the server action solution only works if the fetch is called outside of the "initial render" of the server components, otherwise you get an error
Korat
What do you mean by that
Sun bearOP
like in the example above
Rendering this will error because Next doesn't let you call a server action in the first render pass like this
export const MyComponent = async () => {
// this is a server action
const accessToken = await getAccessToken()
const data = await myFetcher(accessToken)
return <></>
}
Rendering this will error because Next doesn't let you call a server action in the first render pass like this
Korat
Why would you need to call a server action in a server component?
You already are in server, just do the fetch
You already are in server, just do the fetch
Sun bearOP
well the initial premise here is im trying to write a fetch system that works on both the server and the client so that it can be called in either place
to hit the same API
ultimately so that I can have a react query system that can both prefetch data on the server and then refetch it on the client later on using the same function:
https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#advanced-server-rendering
https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#advanced-server-rendering
Korat
Yeah, as I mentioned above, use trpc
Sun bearOP
we are already using react query, just trying to augment it with prefetching
Korat
trpc has a wrapper over react query