Redirect from Middleware
Hello, is it possible to redirect from a middleware function? It doesn't seem to be working correctly for me when calling a serverFn from the client when the middleware would throw the redirect. I am using
useServerFn
for the serverFn, but not sure if there's an equivalent hook that is needed for the middleware as well; currently just getting the following error in the console:
Below is the code from the middleware
37 Replies
afraid-scarlet•10mo ago
can you please create GitHub issue for this so we can track it?
optimistic-gold•7mo ago
Hello, did you find some solution how to respond to failing auth middleware? @alrightsure. Return 401 or 403 response with error message?
afraid-scarlet•7mo ago
was there an issue created?
optimistic-gold•7mo ago
I got answer how to return 401, thank you a lot for help
https://discord.com/channels/719702312431386674/1238170697650405547/1342341835837800478
However, this question how to redirect is still relevant. I would guess it should be possible to handle on client side.
afraid-scarlet•7mo ago
please create a minimal complete example and create a GitHub issue (if not existing yet)
fair-rose•7mo ago
Just wanted to share that this is something I've been trying to figure out as well.
I have authMiddleware that runs before every protected server function. It works fine and checks auth state, but it is unclear how best to handle the case where the user is unauthorized. The ideal (IMO) case would be an easy way to redirect to "/sign-in" directly from middleware, but this doesn't currently work (and maybe there's a good reason for that). I can
I have authMiddleware that runs before every protected server function. It works fine and checks auth state, but it is unclear how best to handle the case where the user is unauthorized. The ideal (IMO) case would be an easy way to redirect to "/sign-in" directly from middleware, but this doesn't currently work (and maybe there's a good reason for that). I can
throw json
or throw Error
but then the only thing I've been able to get working involves handling the error for every serverFunction (I guess maybe there's a helper function that could wrap them, but haven't gotten that far).
The version that I got working used try/catch on the server function call, then added an optional error prop to the return value, then in the component I have to check that error and navigate to /sign-in. Not really practical since I'd need to do that for every server function.
Definitely following and curious what others have found to work well.afraid-scarlet•7mo ago
I read about not being able to throw a redirect from Middleware but I think there is no GitHub issue created for that, right?
if not existing, can you please create an issue with a complete minimal example as a GitHub repo?
fair-rose•7mo ago
I'm not sure if I saw an Issue. I'd be okay if that wasn't allowed directly though.
And yeah, I can make an example.
The one thing I see in the docs is client middleware. I was thinking that if there was a way for client middleware to run on the response from the server function that would give me a single place to redirect on the client if the response had been an error. I haven't explored that yet since I assumed it would only run on the way out.
I will say that this isn't a huge issue since the routes are protected separately, and after a short cache they will end up redirecting to
sign-in
anyway. So it's sort of an edge case where the session was ended outside the current tab, then the user clicks something and gets a 401 from the function and hits the error boundary.
I was able to address my issue by using React Query to fetch the session client-side once in a while, with a short-ish staleTime. So if the user is logged out and tabs back to a logged in tab that query will be stale and refetch, and they'll redirect to sign-in without being able to click anything. So the 401 issue pretty much goes away for me and this is better anyway since an old logged-in tab won't show data when you navigate to it.
However, I'll still check to see if that GitHub Issue exists.
🎉 I was able to get it to work by having an outer client middleware running. If the response is an error we can handle writing a new window location right there if it's "Unauthorized".
The most common use-case I've seen is people wanting to go to /sign-in
when the middleware comes back with unauthorized (as with OP for this thread).
afraid-scarlet•7mo ago
I still think we should support throwing a redirect from any middleware
fair-rose•7mo ago
I've been working on an example repo but I'm realizing now that I can make things work with what we already have, even without a separate client middleware.
So let's say we have a server function with server middleware that throws a redirect.
- If called from
From a DX perspective, it seems like it'd be nice if I could automatically wrap every server function in useServerFn when possible - although I don't know the pros/cons of that (e.g., are there cases where I would not want to wrap them?).
For middleware, it seems like it is sort of working as expected right now within the same confines as server functions themselves. It also seems like it might be uncommon to be calling server functions outside of beforeLoad/loader or a component. If that's accurate then we're mainly left with the DX concern, since from a technical perspective we can make the redirects work for those cases.
beforeLoad
or loader
the redirect works, just as if the redirect was thrown from the server function (makes sense, it's a single function call so it shouldn't matter where the redirect error was thrown in the call stack).
- If called from a component, we can wrap the server function in useServerFn
and then the behavior is the same as beforeLoad/loader.
One issue I previously had was that I'm using React Query and my query options are defined outside a component so I wasn't able to use a wrapped version. However, I think I can modify the options to take an optional wrapped function and use that in things like useSuspenseQuery
in a component, but then just use the function directly if I'm in something like ensureQueryData
/fetchQuery
in a loader. Haven't tested that yet.
I think the biggest problem is a DX concern. For example, if I have a single global middleware for auth that includes a redirect, I now would need to make sure every server function in the app is always wrapped when called outside a beforeLoad/loader or it won't work as expected. That seems hard to enforce in larger projects/teams. This DX concern isn't just for middleware though. A similar DX issue exists with server functions too since the developer would have to "just know" that a particular function expects to be able to redirect.
It's unclear exactly what "problem" to raise in a GitHub Issue.From a DX perspective, it seems like it'd be nice if I could automatically wrap every server function in useServerFn when possible - although I don't know the pros/cons of that (e.g., are there cases where I would not want to wrap them?).
For middleware, it seems like it is sort of working as expected right now within the same confines as server functions themselves. It also seems like it might be uncommon to be calling server functions outside of beforeLoad/loader or a component. If that's accurate then we're mainly left with the DX concern, since from a technical perspective we can make the redirects work for those cases.
afraid-scarlet•7mo ago
are you using
@tanstack/react-router-with-query
?
this automatically handles redirects in functions called by queryfair-rose•7mo ago
Yeah, I have it set up like in the TanStack Start example w/ query. I'll have to take a look. Those cases may already be working. Thanks!
Okay, took a look. It does not seem to be working. I've confirmed my only query client is in the router, as shown in the Start example
* Many of my server functions were actions (create, delete, etc.) within useMutation hooks. So technically using react-query, but I found those needed to be wrapped in useServerFn. Not sure if this is expected.
* My page data also uses query. It's called in the loader with
* Using the server function directly: I can see the network requests in the browser devtools, including retries, and the responses all include the redirect response I expect. However, the app does not redirect. * Refactor the query to optionally use a wrapped server function (unwapped in loader, wrapped in component). Now redirects work. This is basically what the refactor is: Then in the component version: and no change in the loader:
ensureQueryData
and then in the component with useSuspenseQuery
. I tested this by setting a very short staleTime, logging out in another tab, and then switching tabs to trigger the refetch on the other tab.* Using the server function directly: I can see the network requests in the browser devtools, including retries, and the responses all include the redirect response I expect. However, the app does not redirect. * Refactor the query to optionally use a wrapped server function (unwapped in loader, wrapped in component). Now redirects work. This is basically what the refactor is: Then in the component version: and no change in the loader:
afraid-scarlet•7mo ago
cc @Ryan Gillie
fair-rose•7mo ago
To summarize everything above... Given a server function using server middleware that throws a redirect:
1. Calling it from beforeLoad/loader will handle the redirect.
2. Calling it in a component will handle the redirect if it's wrapped with useServerFn.
3. Calling it using React Query on the client will handle the redirect if it's wrapped with useServerFn.
1 and 2 are pretty much exactly what the docs say. 3 is consistent with 2 (function on client needs to be wrapped), although it seems like maybe it should be handled without needing to wrap it based on the message above?
afraid-scarlet•7mo ago
i was under the impression that react-router-with-query would handle this
https://github.com/TanStack/router/blob/main/packages/react-router-with-query/src/index.tsx#L109-L148
which version are you using?
fair-rose•7mo ago
Oh, nice! I didn't assume it was a brand new feature... figured it was like "how it always works" with router/query. I now see in the blame that Ryan just added that last week. My TanStack stuff is all 1.105 right now, the ancient days <checks notes> 11 days ago, lol. I'll get that updated and it seems like it'll work without the hack.
afraid-scarlet•7mo ago
yes, start really has a fast pace right now
hard to keep up 😄
fair-rose•7mo ago
Okay, so I just ran the same test again but without the wrapped server function, it does eventually redirect, but not until the end of the retries that Query does.
I'm using defaults for retries, so when I re-tab to the page with the stale query I see one network request immediately and it includes the redirect but nothing happens. Then 3 retries occur (all with redirect info in the response) and then the page finally redirects on the last one (4th total). So instead of instant like I was seeing in my wrapped version it takes 7 seconds. I also tried with retry: false and it redirected immediately.
Maybe a bug in the new feature? For a "normal" error we'd want to retry before treating it as an error, but for a "redirect" error we'd want to redirect right away I think.
I'm using defaults for retries, so when I re-tab to the page with the stale query I see one network request immediately and it includes the redirect but nothing happens. Then 3 retries occur (all with redirect info in the response) and then the page finally redirects on the last one (4th total). So instead of instant like I was seeing in my wrapped version it takes 7 seconds. I also tried with retry: false and it redirected immediately.
Maybe a bug in the new feature? For a "normal" error we'd want to retry before treating it as an error, but for a "redirect" error we'd want to redirect right away I think.
afraid-scarlet•7mo ago
sounds like it should not retry then
want to contribute that fix?
fair-rose•7mo ago
Yeah, I can take a look and see if I can figure out the guts. From a quick glance it looks like the new code is in
onError
and my assumption based on the results is that onError doesn't get called until after retries have finished. We'd need to run this check after each request instead of onError.optimistic-gold•7mo ago
What I miss
afraid-scarlet•7mo ago
retry behavior
not redirecting directly when retries are enabled
only after retry count is reached
optimistic-gold•7mo ago
Oh, yeah
Lemme look into that
Gonna cc @TkDodo 🔮 on this 😅
Because retries are done on the query and not the cache, so idk its possible to attach it to the
defaultOptions:{ queries }
because a query can just say retry: 3
and that gets overwritten
something like this work would, but again doing
would break itafraid-scarlet•7mo ago
seems like we are missing some hook in query here
optimistic-gold•7mo ago
Yeahhhhhh not sure what to do
My way will totally handle it in the default sense but not the overwritten
afraid-scarlet•7mo ago
lets wait for TkDodo to chime in
fair-rose•7mo ago
A workaround for now in my app is to set this in defaultOptions within
router.tsx
. But yeah, would break if I override it anywhere as Ryan mentions above.
In the docs I see that useQuery
returns a failureReason
that'll include the contents of the error
property while the retries are ongoing. I confirmed locally that the redirect info is available immediately as it continues to retry. That specific fact may not be helpful to fix our case, but it shows that the idea of exposing the individual errors as they are happening is already present in Query.optimistic-gold•7mo ago
I confirmed locally that the redirect info is available immediately as it continues to retryDo you mean like
const { error } = useQuery()
and error is a redirect?fair-rose•7mo ago
yeah, but it's
const { failureReason } = useQuery()
during retries. I did a console log and it printed out the same redirect object that comes back in the response when you throw redirect
.optimistic-gold•7mo ago
ahhhhhhhhhhhh okay gotcha
yeah lets see what tkdodo says, I'm very invested in this
Because I just noticed we also use retries in ours (still on react-router, soon™️ porting to tr and then ts) so this is something we'd have to deal with eventually
jesus I can't type, just got back from hockey 😅
rare-sapphire•7mo ago
What's the issue exactly? If you want to turn off retries when there's a redirect error, you have to implement the retry function. If someone sets retry:3, then they want 3 retries, period - even if there's a redirect error. I don't see a problem with that ?
afraid-scarlet•7mo ago
we wanted to only retry if a "real" error was thrown.
the understanding is that a redirect is not an error
rare-sapphire•7mo ago
yeah that's fine - you need to implement the
retry
function for that
this should probably happen automatically if handleRedirects
is turned on in react-router-with-query
?afraid-scarlet•7mo ago
but it won't catch this if someone specified retry:3 in a single query invocation
so ideally we could "preprocess" the "error"
rare-sapphire•7mo ago
The usual way would be try/catch in the queryFn. There is no additional transformation layer
afraid-scarlet•7mo ago
i know. hence the ask for another "hook" that would allow to generalize this handling
fair-rose•7mo ago
I opened an Issue to track this: https://github.com/TanStack/router/issues/3580
Demo StackBlitz with "issue" and "workaround" demos: https://stackblitz.com/edit/tanstack-router-89xcpvuk?file=app%2Froutes%2Fissue-demo.tsx