Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: child loader redirect overrides parent loader error #11805

Closed
stuartkeith opened this issue Jul 16, 2024 · 7 comments
Closed

[Bug]: child loader redirect overrides parent loader error #11805

stuartkeith opened this issue Jul 16, 2024 · 7 comments
Labels

Comments

@stuartkeith
Copy link

stuartkeith commented Jul 16, 2024

What version of React Router are you using?

6.24.1

Steps to Reproduce

Set up a data router with a parent loader that throws an exception and a child loader that throws a redirect.

An example is here: https://stackblitz.com/edit/github-ypg2nu-lgqn1u?file=src%2Fapp.tsx

The / route has a loader with throw new Response(null, { status: 400 });.

The index: true route underneath that has a loader with throw redirect('/bar');.

Start the app as / as normal.

Expected Behavior

The user sees the error boundary with the 400 request as the error and the current path as /, since the loader that threw the error is higher in the route hierarchy than the child. The redirect is ignored.

Actual Behavior

The user sees a mix of behaviour - the error is set to the 400 request from the parent loader, but they've been redirected to /bar (the child loader).

I had a look into what was going on. findRedirect finds the lowest redirect and then shortcircuits with that redirect, and seems to be run before any errors are handled. That would explain why the redirect is taking priority over the error.

I tried changing this to only return a redirect if there weren't any errors, and then also changed processRouteLoaderData to ignore any redirects instead of throwing.

However that caused the "reloads all routes when a loader during an actionReload redirects" submissions test to fail, and I had trouble figuring out why it was expecting what it was. The fact that the 400 error code is getting through even though the redirect shortcircuits everything seemed to indicate this was a bit trickier to fix than I thought.

See #11357 for more information.

@brophdawg11
Copy link
Contributor

The user sees the error boundary with the 400 request as the error and the current path as /

I'm curious why you would expect to remain on the same URL with an error boundary?

  • The parent route is saying "I can't load data for /"
  • But the more-specific child route is saying "we don't want to show the user /, we actually want to show them /bar"

So why would we stay on / just to show an error UX, instead of going to /bar because (1) that is where we want to end up and (2) we have another chance of loading successfully?

The user sees a mix of behaviour - the error is set to the 400 request from the parent loader, but they've been redirected to /bar (the child loader).

This sounds like a potential bug - I don't think the error should persist if we redirected, so we can look into that.

@stuartkeith
Copy link
Author

stuartkeith commented Jul 16, 2024

I'm curious why you would expect to remain on the same URL with an error boundary?

Maybe my example was too minimal, the real world use case was more something like /parentEntity/:id/childEntity/:id.

The parent loader is responsible for checking everything related to fetching the parent entity (permissions checks, handling non-200 responses for that entity in a certain way etc), maybe it might want to throw a particular error so the error boundary can show a "parent not found" or "you do not have access to this parent" message.

The child entity loader is independent of that. Maybe it has a redirect so childEntity/123 is redirected to childEntity/123/latest or some other default subroute, or it has its own checks, but it doesn't really need to worry about the parent.

If I go to /parentEntity/doesNotExist/childEntity/123, and the parent loader encounters a 404 and throws a particular error, I wouldn't expect to end up at /parentEntity/doesNotExist/childEntity/123/latest. Especially if the user doesn't have permissions to access that parent entity, the "latest" redirect feels like it's leaking information or taking the user somewhere they shouldn't be.

  • The parent route is saying "I can't load data for /"
  • But the more-specific child route is saying "we don't want to show the user /, we actually want to show them /bar"
    So why would we stay on / just to show an error UX, instead of going to /bar because (1) that is where we want to end up and (2) we have another chance of loading successfully?

I think this isn't consistent with how exceptions are handled generally. If a parent route throws exception X and the child route throws exception Y the parent's exception is always chosen over the child's. Whereas with redirects, the child gets an opportunity to override the parent. So the hierarchy is no longer one way, the child can override the parent's behaviour.

I think the redirect being "where we want to end up" is not necessarily the case if the parent loader has some business logic effectively blocking off its subroutes to particular users, and "another chance of loading successfully" doesn't apply there either, as the request won't succeed in any subroute.

The comment on #11357 (comment) also seems to imply redirects don't work this way in the non-data router.

@brophdawg11
Copy link
Contributor

It sounds like what you really want is middleware so you can make these types of top-down decisions in a sequential manner. Loaders running in parallel introduces this type of "who wins" decision point - and the current design is to prioritize redirects over errors.

@brophdawg11
Copy link
Contributor

The user sees a mix of behaviour - the error is set to the 400 request from the parent loader, but they've been redirected to /bar (the child loader).

I looked into this and the error is not sticking around after all. . You throw a 400 but you are seeing a net-new 404 error thrown by the router because no /bar route was defined. If you add a path:'bar' route then the error is properly cleared and you land on the /bar route with no errors.

@brophdawg11
Copy link
Contributor

I am going to close this out since this is all functioning as designed and I believe the solution for your use case is middleware, which should hopefully be coming early in v7.

@brophdawg11 brophdawg11 closed this as not planned Won't fix, can't repro, duplicate, stale Jul 17, 2024
@stuartkeith
Copy link
Author

The user sees a mix of behaviour - the error is set to the 400 request from the parent loader, but they've been redirected to /bar (the child loader).

I looked into this and the error is not sticking around after all. . You throw a 400 but you are seeing a net-new 404 error thrown by the router because no /bar route was defined. If you add a path:'bar' route then the error is properly cleared and you land on the /bar route with no errors.

That's strange, I was definitely hitting this but now I can't replicate it (I have a screenshot in #11357 (reply in thread), although the demo worked differently then). But confirming I am not seeing that behaviour now.

@stuartkeith
Copy link
Author

I am going to close this out since this is all functioning as designed and I believe the solution for your use case is middleware, which should hopefully be coming early in v7.

OK, fair enough. Thanks for getting back to me and taking the time to look into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants