I have a Nuxt3 project where I'd like to add new routes based on an API call to a database. For example, let's say a user navigates to /my-product-1. A route middleware will look into the database and if it finds an entry, it will return that a product page should be rendered (instead of a category page, for example).
This is what I came up with:
export default defineNuxtPlugin(() => {
const router = useRouter()
addRouteMiddleware('routing', async (to) => {
if (to.path == '/my-awesome-product') {
router.addRoute({
component: () => import('/pages/product.vue'),
name: to.path,
path: to.path
})
console.log(router.hasRoute(to.path)) // returns TRUE
}
}, { global: true })
})
To keep it simple, I excluded the API call from this example. The solution above works, but not on initial load of the route. The route is indeed added to the Vue Router (even on the first visit), however, when I go directly to that route, it shows a 404 and only if I don't reload the page on the client does it show the correct page when navigated to it for the second time.
I guess it has something to do with the router not being updated... I found the following example in a GitHub issue, however, I can't get it to work in Nuxt3 as (as far as I'm aware) it doesn't provide the next() method.
When I tried adding router.replace(to.path) below the router.addRoute line, I ended up in an infinite redirect loop.
// from https://github.com/vuejs/vue-router/issues/3660
// You need to trigger a redirect to resolve again so it includes the newly added
route:
let hasAdded = false;
router.beforeEach((to, from, next) => {
if (!hasAdded && to.path === "/route3") {
router.addRoute(
{
path: "/route3",
name: "route3",
component: () => import("#/views/Route3.vue")
}
);
hasAdded = true;
next('/route3');
return;
}
next();
});
How could I fix this issue, please?
Edit:
Based on a suggestion, I tried using navigateTo() as a replacement for the next() method from Vue Router. This, however, also doesn't work on the first navigation to the route.
let dynamicPages: { path: string, type: string }[] = []
export default defineNuxtRouteMiddleware((to, _from) => {
const router = useRouter()
router.addRoute({
path: to.path,
name: to.path,
component: () => import ('/pages/[[dynamic]]/product.vue')
})
if (!dynamicPages.some(route => route.path === to.path)) {
dynamicPages.push({
path: to.path,
type: 'product'
})
return navigateTo(to.fullPath)
}
})
I also came up with this code (which works like I wanted), however, I don't know whether it is the best solution.
export default defineNuxtPlugin(() => {
const router = useRouter()
let routes = []
router.beforeEach(async (to, _from, next) => {
const pageType = await getPageType(to.path) // api call
if (isDynamicPage(pageType)) {
router.addRoute({
path: to.path,
name: to.path,
component: () => import(`/pages/[[dynamic]]/product.vue`),
})
if (!routes.some(route => route.path === to.path)) {
routes.push({
path: to.path,
type: pageType,
})
next(to.fullPath)
return
}
}
next()
})
})
I suggest you use dynamic routing within /page directory structure - https://nuxt.com/docs/guide/directory-structure/pages#dynamic-routes
The [slug] concept is designed exactly for your usecase. You don't need to know all possible routes in advance. You just provide a placeholder and Nuxt will take care of resolving during runtime.
If you insist on resolving method called before each route change, the Nuxt's replacement for next() method you're looking for is navigateTo
https://nuxt.com/docs/api/utils/navigate-to
And I advise you to use route middleware and put your logic into /middleware/routeGuard.global.ts. It will be auto-executed upon every route resolving event. The file will contain:
export default defineNuxtRouteMiddleware((to, from) => {
// your route-resolving logic you wanna perform
if ( /* navigation should happen */ {
return navigateTo( /* your dynamic route */ )
}
// otherwise do nothing - code will flow and given to.path route will be resolved
})
EDIT: However, this would still need content inside /pages directory or some routes created via Vue Router. Because otherwise navigateTo will fail, as there would be no route to go.
Here is an example of one possible approach:
https://stackblitz.com/edit/github-8wz4sj
Based on pageType returned from API Nuxt route guard can dynamically re-route the original URL to a specific slug page.
I have followed a number of posts' guidance on Stack Overflow and other websites about setting up Vue routes to check for user authentication. The setup I now have does essentially work correctly (for each route, the user authentication status is checked, and redirected to the login page if necessary) but there is a small issue in that just before the user is redirected, the standard Nuxt/Vue 404 page screen flashes up momentarily.
This is an example of each route that requires user authentication that I have in router.js:
...
{
path: '/question/:id/comments',
name: 'comments',
component: page('Comments.vue'),
meta: {requiresAuth: true},
beforeEnter: (to, from, next) => {
guard(to, from, next);
}
},
...
And here is my guard() function that checks the user authentication and then redirects them with next() as required:
const guard = function(to, from, next) {
axios.get(process.env.apiUrl + 'check-auth').then(response => {
if( to.matched.some( record => record.meta.requiresAdmin ) ) {
if( response.data.is_moderator !== 1 ) {
next({ path: '/' });
} else {
next();
}
} else if( to.matched.some( record => record.meta.requiresAuth ) ) {
if( !response.data.id ) {
next({
path: '/login',
params: { nextUrl: to.fullPath }
});
} else {
next();
}
} else {
next();
}
});
};
Many articles online suggested using localStorage to check the user authentication but this doesn't work in router.js (presumably because its server side and not client side) but I got around this using a Laravel API call with Axios to check it instead.
If anyone can shed any light on why the 404 screen flashes up first before the redirect? Going to the routes directly works fine, so I am guessing it must be something to do with the next() method.
I have navigation guards working perfectly based on a userTp value, however whenever the user reloads a page it redirects them to name: 'login'.
I would like to prevent a route change on refresh. Is there a simple way to do this within navigation guards?
router.beforeEach(async (to, from, next) => {
if (to.meta.requireAuth) {
if (!store.state.auth.user) {
await store.dispatch(AUTH_REFRESH)
}
if (to.meta.userTp === store.state.auth.user.userTp) {
next()
window.scrollTo(0, 0)
} else {
next({ name: 'login' })
window.scrollTo(0, 0)
}
} else {
next()
window.scrollTo(0, 0)
}
})
EDIT:
I found a semi-workaround for this, but it's not perfect. It allows users to access pages if they know the correct page url path, but since my setup has a server-side userTp check before sending data it just shows the page template but without any data in it.
My temp solution:
(Added as the first if statement)
router.beforeEach(async (to, from, next) => {
if (to = from) {
if (!store.state.auth.user) {
await store.dispatch(AUTH_REFRESH)
to()
}
}
Why is vue-router giving me this error? To be clear, the login flow works as intended but I want to a) get rid of the errro and b) understand why the error is happening.
Error:
Uncaught (in promise) Error: Redirected from "/login" to "/" via a navigation guard.
Login flow
start logged out, but enter a URL that requires auth (i.e. anything besides "/login")
get redirected to "/login" (as expected).
login
successfully get redirected to starting Url from step #1, except with the above error.
Login action:
doLogin({ commit }, loginData) {
commit("loginStart");
axiosClient
.post("/jwt-auth/v1/token", {
username: loginData.username,
password: loginData.password,
})
.then((response) => {
commit("loginStop", null);
commit("setUserData", response.data);
this.categories = airtableQuery.getTable("Categories");
commit("setCategories", this.categories);
this.locations = airtableQuery.getTable("Locations");
commit("setLocations", this.locations);
router.push("/"); // push to site root after authentication
})
.catch((error) => {
console.log(error.response.data.message);
commit("loginStop", error.response.data.message);
commit("delUserData");
});
},
Router:
const routes = [
{
path: "/login",
name: "Login",
component: Login,
meta: { requiresAuth: false },
},
{
path: "/",
name: "Home",
component: Home,
meta: { requiresAuth: true },
},
];
let entryUrl = null;
router.beforeEach((to, from, next) => {
let localStorageUserData = JSON.parse(localStorage.getItem("userData"));
let storeUserData = state.getters.getUserData;
let userData = localStorageUserData || storeUserData;
let isAuthenticated = userData.token !== "" && userData.token !== undefined;
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!isAuthenticated) {
if (to.name !== "Login" && to.name !== "Home") {
entryUrl = to.fullPath;
}
next({ name: "Login" });
} else if (entryUrl) {
let url = entryUrl;
entryUrl = null;
next(url);
} else {
next();
}
} else {
next();
}
});
I spent hours debugging this and got to the following results for the ugly Uncaught (in promise) Error: Redirected when going from ... Error.
Note that the error is not for the "redirect". It's for the initial caller of the first navigation. Keep reading...
It's by design. Why?
Read this comment.
TL;DR: Let's say you are on page A, and click on a button to take you to page B (kinda like method: goToB() { router.push('/B'); } on page A). But there is a Navigation Guard for page B, that sends you to page C.
This error is a way for letting that goToB() function know that the router hasn't been able to fulfill the desired task, and the user hasn't landed on /B.
It's nasty, but informative
The biggest confusion here is that the redirect (landing on Page C) is, both:
an "expected" outcome to you, the architect of the system. But, at the same time,
an "unexpected" event to the caller of goToB in page A (i.e. router.push), who expects the router to go to page B.
That's why when it's popped as Error, it's confusing and frustrating to "you", who looks at the system entirely and thinks nothing is wrong or erroneous!
Urrrgh... So, what should I do?
Solution 1: Use router-link if you can
I ran into a case that <router-link> was working fine, but router.push was complaining. (I think router-link internally suppresses such errors.)
Solution 2.1: Individual suppress errors on each router.push call
The router.push function is returning a Promise (as it can be considered, or will be, an asynchronous job). All you need to do is to suppress any Error it might throw via
router.push('/B').catch(() => {});
// Add this: ^^^^^^^^^^^^^^^^
Solution 2.2: Augment Router.prototype.push to always suppress errors
If you think you have multiple instances of this, you can augment the push function on the prototype of the Router via the snippet on the same comment to apply this to all the router.push calls on the entire app.
The good news is it's giving you granularity level to choose which error you want to suppress (e.g. only NavigationFailureTypes.redirected ones, for example. The enum is here)
If you are on TypeScript, be my guest on the conversion and typing https://gist.github.com/eyedean/ce6ab6a5108a1bd19ace64382144b5b0 :)
Other tips:
Upgrade your vue-router! Your case might be solved by the time you read this. (As they have a plan to do so, apparently.)
Make sure you are not forking or reaching to dead-end in your Navigation Guards, if you have multiple ones. Follow them one by one and track them step by step. Note that, double redirecting is fine (thanks to this answer), you just need to be double careful!
I also got a playground here: https://codepen.io/eyedean/pen/MWjmKjV You can start mimicking this to your need to figure out where your problem happens in the first place.
The error message is getting updated in the next version of vue-router. The error will read:
Redirected when going from "/login" to "/" via a navigation guard
Somewhere in your code, after being redirected to "/login", you are redirecting back to "/". And vue-router is complaining about. You'll want to make sure you only have one redirect per navigation action.
I had a similar error, but for an onboarding redirect in .beforeEach, which was resolved by replacing in the .beforeEach conditional logic:
next({ name: "Onboarding" });
with
router.push({ path: 'Onboarding' });
This error is meant to inform the caller of $router.push that the navigation didn't go to where it was initially intended. If you expect a redirection you can safely ignore the error with the following code.
import VueRouter from 'vue-router'
const { isNavigationFailure, NavigationFailureType } = VueRouter
...
this.$router.push('/')
.catch((e) => {
if (!isNavigationFailure(e, NavigationFailureType.redirected)) {
Promise.reject(e)
}
}
See https://github.com/vuejs/vue-router/issues/2932 for a discussion regarding this issue.
If your redirect is after a call to router.push('/someroute) then you can catch this error as router.push() is a promise and you can attach a catch to it as below
$router.push('/somesecureroute')
.catch(error => {
console.info(error.message)
})
I have the same error. This error created by router.push("/"); row - it trying to say you that pushing to home was interrupted by redirection in navigation guard.
But actually, it's not an error because it is an expected behaviour.
I made ignoring of such errors by the following way:
const router = new VueRouter({
mode: 'history',
routes: _routes,
});
/**
* Do not throw an exception if push is rejected by redirection from navigation guard
*/
const originalPush = router.push;
router.push = function push(location, onResolve, onReject) {
if (onResolve || onReject) {
return originalPush.call(this, location, onResolve, onReject);
}
return originalPush.call(this, location).catch((err) => {
let reg = new RegExp('^Redirected when going from "[a-z_.\\/]+" to "[a-z_.\\/]+" via a navigation guard.$');
if (reg.test(err.message)) {
// If pushing interrupted because of redirection from navigation guard - ignore it.
return Promise.resolve(false);
}
// Otherwise throw error
return Promise.reject(err);
});
};
I had the same issue, i thought it was config problem but it was not
You can try this code
async doLogin({ commit, dispatch }, loginData) {
commit("loginStart");
let response = await axiosClient
.post("/jwt-auth/v1/token", {
username: loginData.username,
password: loginData.password,
})
return dispacth('attempt', response)
}
async attempt({ commit }, response) {
try {
commit("loginStop", null);
commit("setUserData", response.data);
this.categories = airtableQuery.getTable("Categories");
commit("setCategories", this.categories);
this.locations = airtableQuery.getTable("Locations");
commit("setLocations", this.locations);
}
catch( (error) => {
console.log(error.response.data.message);
commit("loginStop", error.response.data.message);
commit("delUserData");
})
}
And in the component where doLogin action is called
this.doLogin(this.loginData)
.then( () => {
this.$router.replace('/')
.catch( error => {
console.log(error)
})
})
.catch( e => {
console.log(e)
})
It happened to me on application boot, during an authorization check I tried to push to '/auth' but that navigation was cancelled by another call to '/', which occurred just after mine.
So in the end I found that is possible to listen for readiness (https://router.vuejs.org/api/#isready) of vue-router using:
await router.isReady()
or
router.isReady().then(...)
I had the exact same issue triggered by two specific pages, for the two pages that were being redirected due to a login:
if (isAdmin){
router.push({name: 'page'}).catch(error=>{
console.info(error.message)})
else...
otherwise everyone else who is a regular user gets pushed to a different page using "router.push"
ONLY on the redirects that were throwing the original error/warning. Suppressing the warning as suggested in an earlier comment:
if (!to.matched.length) console.warn('no match');
next()
allowed for users to sign in and access pages without proper permissions.
Catching the errors appears to be the way to go, per the suggestion of: kissu
I have the following code:
router.beforeEach((to, from, next) => {
if (to.name !== from.name) {
store
.dispatch("fetchCurrentUser")
.then(() => {
console.log('then');
// do something
next();
})
.catch(() => {
console.log('catch');
router.push("/login");
next();
});
} else {
next();
}
// next();
});
I'm trying to get the current user, and if this succeeds, then do something with this data, and if the request is not successful, then redirect the user to the login page. But next () calls do not work, I get the "then" or "catch" in the console, but the redirect does not occur and an infinite loop begins. But if I take next () from condition (commented row) the redirect works fine.
To redirect you should use next('/') or next({ path: '/' }).
From the documentation:
next: Function: this function must be called to resolve the hook. The
action depends on the arguments provided to next:
next(): move on to the next hook in the pipeline. If no hooks are
left, the navigation is confirmed.
next(false): abort the current navigation. If the browser URL was
changed (either manually by the user or via back button), it will be
reset to that of the from route.
next('/') or next({ path: '/' }): redirect to a different location.
The current navigation will be aborted and a new one will be started.
You can pass any location object to next, which allows you to specify
options like replace: true, name: 'home' and any option used in
router-link's to prop or router.push
The promise resolves after the function ends.
This means that the commented next happens regardless of the result of the promise result. Then the promise resolves and you call another next.
The bottom line is that you don't need the commented next and should just cover the promise resolve.
I was able to implement an async validation inside beforeEach, authentication in my case.
export async function onBeforeEach(to, from, next) {
let { someUserToken } = to.query
if (someUserToken) {
let nextRoute = await authenticate(to)
next(nextRoute)
} else {
const userToken = store.state.application.userToken
if (!to.meta.public && !userToken) {
next({ name: 'Forbidden' })
} else {
next()
}
}
}
async function authenticate(to) {
let { someUserToken, ...rest } = to.query
return store
.dispatch('application/authenticate', {
someUserToken
})
.then(() => {
return {
name: 'Home',
query: {
...rest
}
}
})
.catch(() => {
return { name: 'Forbidden' }
})
}
I hope this helps.