How to gracefully handle errors in responses on a component level with Axios and Vue 3? - axios

I am sorry if it is obvious/well-covered elsewhere, but my google-fu has been failing me for over a full day by now. What I would like to achieve is a rich component-level handling of request errors: toaster notifications, status bars, you name it. The most obvious use case is auth guards/redirects, but there may be other scenarios as well (e.g. handling 500 status codes). For now, app-wide interceptors would do, but there is an obvious (to me, at least) benefit in being able to supplement or override higher-level interceptors. For example, if I have interceptors for 403 and 500 codes app-wide, I might want to override an interceptor for 403, but leave an interceptor for 500 intact on a component level.
This would require access to component properties: I could then pass status messages in child components, create toaster notifications with custom timeouts/animations and so on. Naively, for my current app-wide problem, this functionality belongs in App.vue, but I can not figure out how to get access to App in axios.interceptors.response using the current plugin arrangement and whether it is okay to use a single axios instance app-wide in the first place.
The trimmed down code I have tried so far (and which seems the most ubiquitous implementation found online) can be found below. It works with redirects, producing Error: getTranslators: detection is already running in the process (maybe because another 401 happens right after redirect with my current testing setup). However, import Vue, both with curly brackets and without, fails miserably, and, more importantly, I have no way of accessing app properties and child components from the plugin.
// ./plugins/axios.js
import axios from 'axios';
import { globalStorage } from '#/store.js';
import router from '../router';
// Uncommenting this import gives Uncaught SyntaxError: ambiguous indirect export: default.
// Circular dependency?..
// import Vue from 'vue';
const api = axios.create({
baseURL: import.meta.env.VUE_APP_API_URL,
});
api.interceptors.response.use(response => response,
error => {
if (error.response.status === 401) {
//Vue.$toast("Your session has expired. You will be redirected shortly");
delete globalStorage.userInfo;
localStorage.setItem('after_login', router.currentRoute.value.name);
localStorage.removeItem('user_info');
router.push({ name: 'login' });
}
return Promise.reject(error);
});
export default api;
// --------------------------------------------------------------------------
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import axios from './plugins/axios'
import VueAxios from 'vue-axios'
const app = createApp(App)
app.use(router)
  .use(VueAxios, axios)
  .mount('#app')
So, then, how do I get access to component properties in interceptors? If I need them to behave differently for different components, would I then need multiple axios instances (assuming the behavior is not achieved by pure composition)? If so, where to put the relevant interceptor configuration and how to ensure some parts of global configuration such as baseURL apply to all of these instances?
I would prefer not having more major external dependencies such as Vuex as a complete replacement for the existing solution, but this is not a hill to die on, of course.

Instead of using axios's interceptors, you should probably create a composable. Consider the following:
composables/useApiRequest.js
import axios from 'axios';
import { useToast } from "vue-toastification";
const useApiRequest = () => {
const toast = useToast();
const fetch = async (url) => {
try {
await axios.get(url);
} catch (error) {
if (error.response.status === 403) {
toast.error("Your session has expired", {
timeout: 2000
});
}
}
};
return {
fetch,
};
};
export default useApiRequest;
Here we're creating a composable called useApiRequest that serves as our layer for the axios package where we can construct our api requests and create generic behaviors for certain response attributes. Take note that we can safely use Vue's Composition API functions and also components such as the vue-toastification directly in this composable:
if (error.response.status === 403) {
toast.error("Your session has expired", {
timeout: 2000
});
}
We can import this composable in the component and use the fetch function to send a GET request to whatever url that we supply:
<script setup>
import { ref } from 'vue';
import useApiRequest from '../composables/useApiRequest';
const searchBar = ref('');
const request = useApiRequest();
const retrieveResult = async () => {
await request.fetch(`https://api.ebird.org/v2/data/obs/${searchBar.value}/recent`);
}
</script>
And that's it! You can check the example here.
Now, you mentioned that you want to access component properties. You can accomplish this by letting your composable accept arguments containing the component properties:
// `props` is our component props
const useApiRequest = (props) => {
// add whatever logic you plan to implement for the props
}
<script setup>
import { ref } from 'vue';
import useApiRequest from '../composables/useApiRequest';
import { DEFAULT_STATUS } from '../constants';
const status = ref(DEFAULT_STATUS);
const request = useApiRequest({ status });
</script>
Just try to experiment and think of ways to make the composable more reusable for other components.
Note
I've updated the answer to change "hook" to "composable" as this is the correct term.

Related

How to retrieve auth0 access token inside Axios Config file?

hope you’re all well and safe!
I'm currently working on a Vue 3 application with Pinia as my store, Auth0 Vue SDK for authentication/authorization and Axios to call my backend API.
In Auth0 docs, they recommend an access token be retrieved using the getAccessTokenSilently() method everytime I want to call my backend API:
const { getAccessTokenSilently } = useAuth0();
const accessToken = await getAccessTokenSilently();
The problem is I have to type this whenever I use axios in my component files to call my backend API with the access token.
Since I have too many endpoints, my plan is to pass the access token once in an axios request interceptor and use Pinia actions to call my APIs.
I’ve created a /config/axios.js file in my application that contains the following:
//Import Axios Library and Auth0
import axios from 'axios';
import { useAuth0 } from "#auth0/auth0-vue"
//Create instance of axios
const instance = axios.create({
baseURL: 'http://localhost:5000/api/v1'
});
// Create a request interceptor for my instance and get accessToken on the fly
instance.interceptors.request.use(async (config) => {
const { getAccessTokenSilently } = useAuth0();
const accessToken = await getAccessTokenSilently();
config.headers['Authorization'] = accessToken;
return config;
}, (error) => {
return Promise.reject(error)
});
export default instance;
Simple enough, just create a baseURL and intercept requests to add the authorization header with the access token.
Now in Pinia, I've created a user store that'll fetch users with the axios config as seen below:
//Import the Pinia Library
import { defineStore } from 'pinia'
//Import the Axios Library for API calls
import axiosConfig from "#/config/axios.js"
export const useUserStore = defineStore('user', {
state: () => ({
user:{}
}),
getters: {
getUser(state){
return state.user
}
},
actions: {
async fetchUser(){
try {
const data = await axiosConfig.get('/profile')
this.user = data.data
} catch (error) {
console.log("User Pinia error: " + error)
}
}
},
})
And lastly in my component file, I just import the store and call the Pinia action fetchUsers.
When trying an axios call, I get the following error!
TypeError: auth0_auth0_vue__WEBPACK_IMPORTED_MODULE_5_.useAuth0() is undefined
I can't figure out how to retrieve the access token from auth0 library in my interceptor function.
A similar question was raised as an issue on auth0-vue github:
https://github.com/auth0/auth0-vue/issues/99
The above link describes numerous approaches. I went for the plugin approach that is described in this PR, namely:
https://github.com/auth0-samples/auth0-vue-samples/commit/997f262dabbab355291e5710c51d8056a5b142cf
But the issue was officially resolved by offering a mechanism to allow the sdk from outside a vue component:
https://github.com/auth0/auth0-vue/issues/99#issuecomment-1099704276

SvelteKit With MongoDB ReferenceError: global is not defined

I'm trying to setup MongoDB connection library function. I know this function is solid, its used in a whole lot of places (search for Global is used here to maintain a cached connection across hot reloads) and you'll find a whole lot of uses including next.js releases. Note, the purpose of global storage for the database connection is to reduce the overall # of db connections in use at any one time.
What I'm not understanding is the error I'm getting when I import this library via import { connectToDatabase } from '$lib/database';
database.js
// https://github.com/mongodb-developer/mongodb-next-todo/blob/main/util/mongodb.js
import { ENV_OBJ } from "$lib/env";
import { MongoClient } from "mongodb";
const uri = ENV_OBJ.MONGODB_URI;
if (!uri) {
throw new Error("Please define the Mongodb uri environment variable inside .env");
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = global.mongo
if (!cached) {
cached = global.mongo = { conn: null, promise: null }
}
export const connectToDatabase = async() => {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const options = {
useNewUrlParser: true,
useUnifiedTopology: true
};
cached.promise = MongoClient.connect(MONGODB_URI, opts).then((client) => {
return {
client,
db: client.db(MONGODB_DB),
}
})
}
cached.conn = await cached.promise;
return cached.conn;
}
The errors:
global is not defined
ReferenceError: global is not defined
at node_modules/mongodb/lib/promise_provider.js (http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:548:25)
at __require2 (http://localhost:3000/node_modules/.vite/chunk-6ODJH7E3.js?v=3885e04e:10:44)
at node_modules/mongodb/lib/utils.js (http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:6524:30)
at __require2 (http://localhost:3000/node_modules/.vite/chunk-6ODJH7E3.js?v=3885e04e:10:44)
at node_modules/mongodb/lib/cursor/abstract_cursor.js (http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:10873:19)
at __require2 (http://localhost:3000/node_modules/.vite/chunk-6ODJH7E3.js?v=3885e04e:10:44)
at node_modules/mongodb/lib/index.js (http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:25281:29)
at __require2 (http://localhost:3000/node_modules/.vite/chunk-6ODJH7E3.js?v=3885e04e:10:44)
at http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:25616:23
Note, I do see a file in my generated minimal sveltekit repo called global.d.ts I'm not sure of its purpose. It contains only:
/// <reference types="#sveltejs/kit" />
Any ideas on what's causing the error?
Reference: "#sveltejs/kit": "version": "1.0.0-next.118",
Edit: After spending a whole lot of time on this issue, the global not defined error seems to come from import { MongoClient } from "mongodb"; If I add appropriate console.logs, I can see that the MongoClient function works fine on the server, but then I get the global error on the client. The server indicates no errors at all.
So it turns out I was calling import { connectToDatabase } from '$lib/database' not in a .js helper file or api style (.js) endpoints. I was attempting to use that import and make a database call directly from the <script> portion of a xxx.svelte file.
Definite no go. That generates an immediate global not defined error.

How to handle multiple dispatches in nuxtServerInit using Axios

For some context: I am collecting data from my API using axios and adding it to the Vuex store in my Nuxt application using nuxtServerInit. I am using name-spaced modules as recommended in the docs to dispatch requests from my index.js file.
The problem is that while this technically works (everything is set in the store correctly), I end up with a long list of await dispatch(...) expressions in my index.js file. This does not seem to be a very efficient approach (or what the intended use is as I can't find a similar configuration in any of the docs) and I am wondering if I have misunderstood either the axios or nuxt documentation in my implementation. I have been looking into Promise.all() but cannot see how I would implement it here. Hopefully someone has some advice!
This is an example of the configuration I have arrived at with this approach:
app/store/index.js
export const actions = {
async nuxtServerInit ({ commit, dispatch }) {
await dispatch('endpointOne/storeDispatchInit')
await dispatch('endpointTwo/storeDispatchInit')
await dispatch('endpointThree/storeDispatchInit')
...
...
}
}
app/store/endpointOne.js
export const actions = {
async storeDispatchInit ({ commit }) {
const { data } = await axios.get(`${this.$axios.defaults.baseURL}/endpoint-1`)
commit('SET_ALL', data)
}
}
I would also be quite happy to hear that this is the correct configuration after all...!

Ionic 3 HTTP GET – getting changes in the observable

I am creating a chat page in my ionic application and I would like to know when the get request changes. I am also OK with doing this all off of an interval, but I can’t find a good example on how to do it. I would prefer to not have to have the user refresh the page to see if a new chat message has appeared. I am newer to observables and have not seen this done with the remote data source and the HTTP package. My data is already JSON so that's why it's not mentioned at all. Thanks!
Chat.ts
import { Component, ViewChild ,ElementRef} from '#angular/core';
import { IonicPage, NavController, NavParams, Events, Content } from 'ionic-angular';
import { ChatProvider } from "../../../../../providers/chat/chat";
import { Observable } from "rxjs/Observable";
export class ChatShowPage {
public id:string;
public items: Observable<ChatMessage[]> = [];
constructor(navParams: NavParams, private chatService: ChatProvider, private events: Events,) {
this.id = navParams.get('id_convo');
}
getMsg() {
return this.chatService.getMsgList(this.id).subscribe(res => {
this.items=res;
console.log(this.items);
});
}
}
Provider.ts
getMsgList(id): Observable<ChatMessage[]> {
const msgListUrl = this.base_url + 'user/account/chat/show/' + id;
console.log('getMsgList - ' + msgListUrl);
return this.http.get<any>(msgListUrl,
{headers: new HttpHeaders({'Content': 'application/json','Accept': 'application/json','Authorization': 'Bearer ' + localStorage.getItem('tk') })});
}
You want to use Events to update your component
In this particular example, you could use:
ionViewWillLeave() {
// unsubscribe
this.events.unsubscribe('chat:received');
}
ionViewDidEnter() {
//get message list
this.getMsg();
// Subscribe to received new message events
this.events.subscribe('chat:received', msg => {
this.pushNewMsg(msg);
})
}
In your provider.ts your getMsgList function should emit the chat:received event as such:
this.events.publish('chat:received')
Then in your pushNewMsg function, you would do some safety checks and then push the new message into the array which you can display with an *ngFor directive.
There are two famous ways to make chat module via web(including ionic).
Polling
WebSocket (Event-driven)
Polling
: If you make simple or personal chat application and want to keep current code structure, then you'd better use polling pattern which merely repeat request with some interval. In addition, for better polling, please consider HTTP Long Polling
setTimeout() with recursion or setInterval() can be options.
WebSocket (Event-driven)
: If you make commercial application or application with lots of users, then you have to use Event-driven pattern. For using this pattern, both of server-side work and client-side work are needed. If you use node.js as server-side framework, then I recommend to use Socket.IO.

What's the equivalent of Angular Service in VueJS?

I want to put all my functions that talk to the server and fetch data into a single reusable file in VueJS.
Plugins don't seem to be the best alternative. Template less components..?
In total there are 4 ways:
Stateless service: then you should use mixins
Stateful service: use Vuex
Export service and import from a vue code
any javascript global object
I am using axios as HTTP client for making api calls, I have created a gateways folder in my src folder and I have put files for each backend, creating axios instances, like following
myApi.js
import axios from 'axios'
export default axios.create({
baseURL: 'http://localhost:3000/api/v1',
timeout: 5000,
headers: {
'X-Auth-Token': 'f2b6637ddf355a476918940289c0be016a4fe99e3b69c83d',
'Content-Type': 'application/json'
}
})
Now in your component, You can have a function which will fetch data from the api like following:
methods: {
getProducts () {
myApi.get('products?id=' + prodId).then(response => this.product = response.data)
}
}
As I assume you want to re-use this method in multiple components, you can use mixins of vue.js:
Mixins are a flexible way to distribute reusable functionalities for Vue components. A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be “mixed” into the component’s own options.
So you can add a method in mixin and it will be available in all the components, where mixin will be mixed. See following example:
// define a mixin object
var myMixin = {
methods: {
getProducts () {
myApi.get('products?id=' + prodId).then(response => this.product = response.data)
}
}
}
// define a component that uses this mixin
var Component = Vue.extend({
mixins: [myMixin]
})
// alternate way to have a mixin while initialising
new Vue({
mixins: [myMixin],
created: function () {
console.log('other code')
}
})
I'm using Vue Resource mostly.
1.I create new file where I do connection to API endpoint using Vue.http.xxx.So let's say we have endpoint that output the posts.Create new directory in your project, I call it services, and then create file called PostsService.js - content looks like this:
import Vue from 'vue'
export default {
get() {
return Vue.http.get('/api/posts)
}
}
Then I go to component where I want use this service, and import it
import PostsService from '../services/PostsService'
export default {
data() {
return {
items: []
}
},
created() {
this.fetchPosts()
},
methods: {
fetchPosts() {
return PostsService.get()
.then(response => {
this.items = response.data
})
}
}
}
For more info about this approach, feel free to check my repo on GitHub https://github.com/bedakb/vuewp/tree/master/public/app/themes/vuewp/app
I suggest creating an API Provider that you can access from anywhere in your app.
Simply create a src/utils folder and inside of it a file called api.js.
In it, export your wrapper that knows how to communicate with your API as an object or a ES6 static class (I prefer how the latter looks and works if you're not afraid of classes). This provider can use any HTTP request library that you like and you can easily swap it later by changing a single file (this one) instead of hunting down the whole codebase. Here's an example of using axios, assuming we have a REST API available at api.example.com/v1 that uses SSL:
import axios from 'axios'
import { isProduction, env } from '#/utils/env'
const http = null // not possible to create a private property in JavaScript, so we move it outside of the class, so that it's only accessible within this module
class APIProvider {
constructor ({ url }) {
http = axios.create({
baseURL: url,
headers: { 'Content-Type': 'application/json' }
})
}
login (token) {
http.defaults.headers.common.Authorization = `Bearer ${token}`
}
logout () {
http.defaults.headers.common.Authorization = ''
}
// REST Methods
find ({ resource, query }) {
return http.get(resource, {
params: query
})
}
get ({ resource, id, query }) {
return http.get(`${resource}/${id}`, {
params: query
})
}
create ({ resource, data, query }) {
return http.post(resource, data, {
params: query
})
}
update ({ resource, id, data, query }) {
return http.patch(`${resource}/${id}`, data, {
params: query
})
}
destroy ({ resource, id }) {
return http.delete(`${resource}/${id}`)
}
}
export default new APIProvider({
url: env('API_URL') // We assume 'https://api.example.com/v1' is set as the env variable
})
Next, in your main.js file or wherever else you bootstrap the Vue app, do the following:
import api from '#/src/utils/api'
Vue.$api = api
Object.defineProperty(Vue.prototype, '$api', {
get () {
return api
}
})
Now you can access it anywhere in your Vue app as well as anywhere you import Vue itself:
<template>
<div class="my-component">My Component</div
</template>
<script>
export default {
name: 'MyComponent',
data () {
return {
data: []
}
},
async created () {
const response = await this.$api.find({ resource: 'tasks', query: { page: 2 } })
this.data = response.data
}
}
</script>
or:
// actions.js from Vuex
import Vue from 'vue'
export async function fetchTasks ({ commit }) {
const response = await Vue.$api.find({ resource: 'tasks', query: { page: 2 } })
commit('SAVE_TASKS', response.data)
return response
}
Hope this helps.
I think for your simple question the answer could be any ES6 module containing functions (equivalent to methods in class in ANgular) and directly importing them in components using ES6 imports and exports. There are no such services that could be injected in components.
You can make your own service where you can place all your HTTP server calls and then import that to the components where you want to use them.
Best is to make use of Vuex for complex state management applications because in Vuex you are able to handle all async calls via actions which always run asynchronously and then commit the mutation once you have the result.Mutation will directly interact with the state and will update it in an immutable manner (which is preferred). This is stateful approach.
There are other approaches as well. But these are the ones which I follow in my code.