Skip to content
This repository was archived by the owner on Aug 21, 2026. It is now read-only.
This repository was archived by the owner on Aug 21, 2026. It is now read-only.

First-class "client-side redirect" support #124

Closed

Description

@domenic

The scenario shown in #121 uses navigation.navigate(newURL, { replace: true }) in the middle of a navigate handler, to perform a "client-side redirect".

However, this is not optimal, as doing so makes the original navigation count as interrupted, and thus a "failure", with an "AbortError" DOMException as the failure reason. Notably it fires navigateerror on navigation, rejects navigation.transition.finished, and rejects the promise returned by navigation.navigate() if the navigation started that way.

There may be other downsides, e.g. the very-related discussion in #5 talks about the problems with losing same-siteness (for Sec-Same-Site) and form data. However, for purely client-side redirects I don't think those particular concerns apply. I guess maybe losing the userInitiated bit is in this vein, though.

We may want to contemplate a first-class redirect API, perhaps on the NavigateEvent object. The semantics would be something like a replace, but with the additional pieces:

  • navigation.transition.finished delays settling until the new navigation settles
  • navigation.navigate()'s return value delays settling until the new navigation settles
  • navigation.transition.rollback() during the new navigation rolls back both navigations.
  • event.signal for the original event does not fire abort.
  • Maybe we carry over event.userInitiated
  • Maybe we carry over event.info and event.formData unless told not to?
  • There is still a period where navigation.curentEntry is the intermediate page (e.g. if you're navigating to /b and want to redirect to /c, navigation.currentEntry will briefly be on /b before eventually replacing with /c). There's no way of getting around that.

Activity

  1. domenic commented on Jun 28, 2021

    @domenic
    CollaboratorAuthor

    I just realized something that ups the priority here.

    If you're synchronously trying to do the redirect, as in this example:

    e.transitionWhile((async () => {
      if (shouldRedirect()) {
        await navigation.navigate("/redirected"); // (*)
      } else {
        // ...
      }
    })());

    then you should not use { replace: true }, since at the time the (*) line is running, the navigation hasn't happened yet: we're still in the synchronous phase of event dispatch (despite being inside an async function).

    Whereas if you're asynchronously trying to do a redirect, as in this example

    e.transitionWhile((async () => {
      if (await shouldRedirect()) {               // Note the await
        await navigation.navigate("/redirected"); // (*)
      } else {
        // ...
      }
    })());

    then you do need to use { replace: true }, because by that point the navigation has gone through and you're on the page that needs to get replaced.

    This seems error-prone and fragile, so adding a first-class helper to smooth this over would be a good idea.

  2. added
    additionA proposed addition which could be added later without impacting the rest of the API
    on Aug 23, 2021
  3. domenic commented on Jan 12, 2022

    @domenic
    CollaboratorAuthor

    If you're synchronously trying to do the redirect, as in this example:

    This example also causes annoying uncaught exceptions, since the first navigation being canceled causes transitionWhile() to throw an exception (since it's called on a canceled event). See https://crbug.com/1224413.

  4. atscott commented on Apr 23, 2022

    @atscott
    Contributor

    A concrete use-case for this might be for SPA app initial navigation. When going directly to a guarded URL, that navigation might be rejected. If the application developer wants to deny the the navigation but doesn't provide a redirect, how should the framework's router handle this? Right now, this can result in "blank" screens on initial page load.

    Let's say the framework's router allows the application developer to specify a fallback to redirect to if the initial navigation fails, for example the home page. It would also be interested in tracking any redirects associated with that initial navigation so it doesn't eagerly navigate to the home page fallback when it should instead allow any associated redirects to complete.

    Edit: would the first class redirect support multiple redirects? i.e., navigate to a-> redirects to b -> redirects to c

  5. domenic commented on Apr 25, 2022

    @domenic
    CollaboratorAuthor

    It would also be interested in tracking any redirects associated with that initial navigation so it doesn't eagerly navigate to the home page fallback when it should instead allow any associated redirects to complete.

    I don't quite understand that part; which redirects exist in this setup, besides the one from a guarded URL to the fallback homepage URL?

    Edit: would the first class redirect support multiple redirects? i.e., navigate to a-> redirects to b -> redirects to c

    Yes, I think that's the ideal.


    To summarize my current thinking of what we could do for redirects: navigation.transition.redirect(url, { info }) would have the following impacts:

    • If the navigate event has not finished firing:
      • Cancel the navigate event (like preventDefault()), but without firing navigateerror or rejecting any promises or firing abort on the AbortSignal.
      • Start a new navigation, similar to navigation.navigate(url, { info }).
      • Update navigation.transition in place to track that navigation, instead of replacing the object like navigation.navigate() normally would. (This ensures anyone watching navigation.transition.finished gets notified at the right time.)
      • Make any { committed, finished } promises returned from any original navigation tie themselves to the new navigation.
    • Otherwise, if the navigate event has finished firing but we're still in the transition phase:
      • Start a new navigation, similar to navigation.navigate(url, { info, history: "replace" }). (Note the replace!!) Similarly don't fire navigateerror or reject any promises or fire abort, even though we are interrupting a transitioning navigation.
      • Update navigation.transition in place to track that navigation. I think this includes updating navigation.transition.navigationType (to replace) and navigation.transition.from (to the being-replaced intermediate entry).
      • Update any { finished } promise returned from the original navigation, similar to the above. (But we cannot update committed since it already settled.)
    • Otherwise, immediately reject. (This can only happen if you stored navigation.transition somewhere and called storedTransition.redirect(...) later; the transition property will get nulled out after the transition phase.)

    All of these steps should work if done more than once.

  6. atscott commented on Apr 25, 2022

    @atscott
    Contributor

    It would also be interested in tracking any redirects associated with that initial navigation so it doesn't eagerly navigate to the home page fallback when it should instead allow any associated redirects to complete.

    I don't quite understand that part; which redirects exist in this setup, besides the one from a guarded URL to the fallback homepage URL?

    @domenic The fallback URL would only want to be applied if the initial navigation totally failed. There might be user-defined redirects associated with that navigation and wouldn't want to perform the fallback navigation if a different redirect succeeded. I believe the properties of the navigation you just outlined would make this task easy.

  7. posva commented on Feb 26, 2025

    @posva
    Contributor

    I was playing around with the API and I noticed that event.respondWith() and event.redirect() do not exist yet. I believe that allowing to redirect a navigation is crucial for SPAs because it's a mechanism that already exists in navigation guards and because it would allow for SPA routers to listen to navigatesuccess and navigateerror events to determine when a navigation is done.
    Having a native way to redirect within a navigate event is the last missing piece for users to freely use the native browser API instead of having to use the router's API. I think this would be a huge win for this browser API. Nowadays, routers enforce users to go through routers' methods (or components like <RouterLink />) to ensure a consistent state (there is no way to consistently detect a history.pushState()) and since they control when history.pushState() is called, they allow redirecting within a navigation and it still counts as one single call to router.push() (or router.navigate()).
    By not being able to redirect within a navigation, we break this flow and one navigation can trigger multiple ones. Of course, this could also be the new expected behavior and routers could go this path and release with a breaking change.

    Related to #5, To me, rewriting and redirected are the same in the context of an SPA router, sometimes you change the URL, sometimes you add stuff to state or info

  8. domenic commented on Feb 26, 2025

    @domenic
    CollaboratorAuthor

    Did you see #279?

    respondWith() was replaced several times, and its current name is intercept().

  9. posva commented on Feb 26, 2025

    @posva
    Contributor

    I did see #279. Since it's not implemented and the issues are open I understood they are still in discussion and not sure to be added

    respondWith() was replaced several times, and its current name is intercept().

    Thanks! I wasn't able to follow all the changes 😅

  10. atscott commented on Feb 26, 2025

    @atscott
    Contributor

    @posva for sure, I ran into the same thing prototyping things a couple weeks ago. We were able to agree on the simplest case for redirect with uncommitted deferred navigations. I hadn’t considered redirecting with state or info though… I could see how that might be useful when all baseline browsers have the navigation api implemented and navigations no longer have to be initiated through the router APIs.

  11. posva commented on Feb 26, 2025

    @posva
    Contributor

    It's worth noting that I'm only considering uncommitted deferred navigations because it seems like everybody agreed that was the most common use case for SPA routers in #66 and also I thought it would be a requirement for redirects, otherwise the URL would change multiple times.

    The way things are outlined at #124 (comment) should work out well for the needs of Vue Router

  12. domenic commented on Feb 27, 2025

    @domenic
    CollaboratorAuthor

    #279 is our current plan for redirects, already implemented in Chromium behind a flag. I kept this open to track potential work on "rollbacks" (separate from redirects), but maybe that is confusing.

    Let me close this, and perhaps if you think there is something in the current design in the explainer that is not satisfactory, then you could open a specific issue with a suggestion? For example, it sounds like maybe we want to allow redirects to not only modify the URL, but also the info, and maybe the state? Writing that up in a clear issue with some sample use cases would be very helpful.

  13. posva commented on Feb 27, 2025

    @posva
    Contributor

    Thanks, I will open an issue regarding the info and state in a redirect.

    Regarding event.redirect() itself(), I tried it on the Dev version and it doesn't follow the: it doesn't trigger a new navigation like navigation.navigate() as in navigate is not triggered and cannot be intercepted again. It only allows to change the final URL that is committed and just once. In SPAs routers, when a navigation guard redirect, that new redirection goes through navigation guards again and can redirect as many times as needed. As mentioned in #124 (comment), I think not being able to handle redirect this way would mean discouraging the use of event.redirect() in intercepted navigation when they use an SPA router and limiting the extensibility of the Navigation API.

    Is this meant to be like this or are they plans to change it?

  14. domenic commented on Feb 27, 2025

    @domenic
    CollaboratorAuthor

    Please file new issues for any such concerns!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    additionA proposed addition which could be added later without impacting the rest of the API

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions