I'm using axios in my react app using import axios from 'axios in many of my scripts. I want to use sort of a middleware that is invoked for all axios calls/errors. How do I approach this?
As per the documentation - You need to create a file i.e
// api-client.js
import axios from 'axios';
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
console.log(config);
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
export default axios;
Then from your container or controller, import above file:
// Home.js
import apiClient from './api-client.js';
Interceptors are the Axios way of doing this. For me though, it was too limited, tangled in Axios' API, difficult to test, etc.
Axios-middleware
So I wrote the axios-middleware module, a simple middleware service that hooks itself in your axios instance (either global or a local one) and provides a simple, self-contained and easily testable middleware API.
Note: it shines in bigger apps where minimal coupling is really important.
Simple example
Here's a simple example from the documentation
import axios from 'axios';
import { Service } from 'axios-middleware';
const service = new Service(axios);
service.register({
onRequest(config) {
console.log('onRequest');
return config;
},
onSync(promise) {
console.log('onSync');
return promise;
},
onResponse(response) {
console.log('onResponse');
return response;
}
});
console.log('Ready to fetch.');
// Just use axios like you would normally.
axios('https://jsonplaceholder.typicode.com/posts/1')
.then(({ data }) => console.log('Received:', data));
It should output:
Ready to fetch.
onRequest
onSync
onResponse
Received: {userId: 1, id: 1, title: ...
Testing a middleware
Say we have the following self-contained middleware class that we want to test.
export default class ApiErrorMiddleware {
constructor(toast) {
this.toast = toast;
}
onResponseError(err = {}) {
let errorKey = 'errors.default';
const { response } = err;
if (response && response.status) {
errorKey = `errors.${response.status}`;
} else if (err.message === 'Network Error') {
errorKey = 'errors.network-error';
}
this.toast.error(errorKey);
throw err;
}
}
Then it's really easy, we don't even need to mock Axios.
import ApiErrorMiddleware from '#/middlewares/ApiErrorMiddleware';
describe('ApiErrorMiddleware', () => {
let toast;
let middleware;
// Jest needs a function when we're expecting an error to be thrown.
function onResponseError(err) {
return () => middleware.onResponseError(err);
}
beforeEach(() => {
toast = { error: jest.fn() };
middleware = new ApiErrorMiddleware(toast);
});
it('sends a code error message', () => {
expect(onResponseError({ response: { status: 404 } })).toThrow();
expect(toast.error).toHaveBeenLastCalledWith('errors.404');
});
});
Related
I have an instance of axios with baseURL:"http....".
This instance is imported into data.js to be able to make all calls with axios in another file.
This is data.js:
import axios from 'src/utils/DataService';
function getEwo(id) {
return axios.get('api.../' + id);
}
function getorders(){
return axios.get("api/.../orders");
}
export{getEwo,getorders}
The first call is working, but the second one does not.
In the last file I have the following code:
import * as dataEwo from 'src/dataservice/dataEwo';
const getOrders = useCallback(async () => {
try {
const response = await dataEwo.getorders();
var resp = response.data.data;
if (isMountedRef.current) {
setOrders(resp);
}
} catch (err) {
console.error(err);
}
}, [isMountedRef]);
useEffect(() => {
getOrders();
}, []);
I tried getorders() on Swagger and from BE it returns the data correctly.
If I try to make the same call from the FE client with axios it doesn't work and the BE is queried but the query returns no data and no errors.
Why doesn't axios work in this case?
I'm running tests via node with Jest, on a Next/React project.
I'm also using cross-fetch as well.
When I try to mock cross-fetch for my component
import crossFetch from 'cross-fetch'
jest.mock('cross-fetch')
crossFetch.mockResolvedValue({
status: 200,
json: () => {{
user : testUser
}},
})
render(<UserProfile />)
The API request in the getServerSideProps
always returns 500
export async function getServerSideProps({ query: { userId } }) {
let user = null
let code = 200
try {
let response = await fetch(`https://example.com/users/${userId}`, { method: 'GET' })
let statusCode = response.status
let data = await response.json()
if (statusCode !== 200) {
code = statusCode
} else {
user = data.user
}
} catch (e) {
console.log(e.message)
}
return {
props: {
user,
code,
},
}
}
I have a feel it has something to do with the tests being initiating from Node and testing-library library is simulating a browser, that the actual lib making the request is not being mocked for the correct execution environment (browser in my case). But I'm not entirely sure.
Thanks in advance
Perhaps it's not working because of the default export being a function. Try this:
//test.js
import crossFetch from 'cross-fetch';
jest.mock('cross-fetch', () => {
//Mock the default export
return {
__esModule: true,
default: jest.fn()
};
});
test('should do a mock fetch', () => {
crossFetch.mockResolvedValue({
status: 200,
json: () => {{
user: testUser
}},
});
expect(crossFetch().status).toEqual(200);
});
I make use of fetch-mock (https://www.wheresrhys.co.uk/fetch-mock/) when testing fetch calls. You may need to provide a polyfill for fetch to be understood in the context of Jest tests, and this can be done with an import "cross-fetch/polyfill"; in the file where fetch is being used.
Note that the Create React App generated environment handles the necessary polyfill imports, but I'm not sure if Next.js has something similar.
I have a try/catch block inside a React service that I now need to add the actual Axios call and make a GET request to an external API in order to get live data back and then set that data response to an existing variable, but I'm not sure how to do it. I have my service listed below, any feedback or insight on how to better accomplish this would be a huge help
customerService.js
import { logError } from './logging'
export async function getCustomer(customer = {}) {
try {
const { customerId } = customer
console.info('Customer ID:', customerId)
// TODO: Make call to new API
return new Promise(res => {
setTimeout(() => res({}), 1000)
})
} catch (error) {
logError(error)
throw error
}
}
Since your getCustomer() function is async, you should be able to await your Axios call:
import Axios from 'axios'
import { logError } from './logging'
export async function getCustomer(customer = {}) {
try {
const { customerId } = customer
console.info('Customer ID:', customerId)
const result = await Axios.get("http://example.com");
return new Promise(res => {
// I assume this is where you want to use the API result?
setTimeout(() => res(result), 1000)
})
} catch (error) {
logError(error)
throw error
}
}
I have a React component that calls a function getAllPeople:
componentDidMount() {
getAllPeople().then(response => {
this.setState(() => ({ people: response.data }));
});
}
getAllPeople is in my api module:
export function getAllPeople() {
return axios
.get("/api/getAllPeople")
.then(response => {
return response.data;
})
.catch(error => {
return error;
});
}
I think this is a very basic question, but assuming I want to handle the error in my root component (in my componentDidMount method), not in the api function, how does this root component know whether or not I the axios call returns an error? I.e. what is the best way to handle errors coming from an axios promise?
Better way to handle API error with Promise catch method*.
axios.get(people)
.then((response) => {
// Success
})
.catch((error) => {
// Error
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
// console.log(error.response.data);
// console.log(error.response.status);
// console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the
// browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
console.log(error.config);
});
The getAllPeople function already returns the data or error message from your axios call. So, in componentDidMount, you need to check the return value of your call to getAllPeople to decide whether it was an error or valid data that was returned.
componentDidMount() {
getAllPeople().then(response => {
if(response!=error) //error is the error object you can get from the axios call
this.setState(() => ({ people: response}));
else { // your error handling goes here
}
});
}
If you want to return a promise from your api, you should not resolve the promise returned by your axios call in the api. Instead you can do the following:
export function getAllPeople() {
return axios.get("/api/getAllPeople");
}
Then you can resolve in componentDidMount.
componentDidMount() {
getAllPeople()
.then(response => {
this.setState(() => ({ people: response.data}));
})
.catch(error => {
// your error handling goes here
}
}
My suggestion is to use a cutting-edge feature of React. Error Boundaries
This is an example of using this feature by Dan Abramov.
In this case, you can wrap your component with this Error Boundary component.
What is special for catching the error in axios is that you can use
interceptors for catching API errors.
Your Error Boundary component might look like
import React, { Component } from 'react';
const errorHandler = (WrappedComponent, axios) => {
return class EH extends Component {
state = {
error: null
};
componentDidMount() {
// Set axios interceptors
this.requestInterceptor = axios.interceptors.request.use(req => {
this.setState({ error: null });
return req;
});
this.responseInterceptor = axios.interceptors.response.use(
res => res,
error => {
alert('Error happened');
this.setState({ error });
}
);
}
componentWillUnmount() {
// Remove handlers, so Garbage Collector will get rid of if WrappedComponent will be removed
axios.interceptors.request.eject(this.requestInterceptor);
axios.interceptors.response.eject(this.responseInterceptor);
}
render() {
let renderSection = this.state.error ? <div>Error</div> : <WrappedComponent {...this.props} />
return renderSection;
}
};
};
export default errorHandler;
Then, you can wrap your root component passing axios instance with it
errorHandler(Checkout, api)
As a result, you don't need to think about error inside your component at all.
You could check the response before setting it to state. Something like
componentDidMount() {
getAllPeople().then(response => {
// check if its actual response or error
if(error) this.setState(() => ({ error: response }));
else this.setState(() => ({ people: response}));
});
}
Its relying on the fact that axios will return different objects for success and failures.
The solution from Yevhenii Herasymchuk was very close to what I needed however, I aimed for an implementation with functional components so that I could use Hooks and Redux.
First I created a wrapper:
export const http = Axios.create({
baseURL: "/api",
timeout: 30000,
});
function ErrorHandler(props) {
useEffect(() => {
//Request interceptor
http.interceptors.request.use(function (request) {
// Do something here with Hooks or something else
return request;
});
//Response interceptor
http.interceptors.response.use(function (response) {
if (response.status === 400) {
// Do something here with Hooks or something else
return response;
}
return response;
});
}, []);
return props.children;
}
export default ErrorHandler;
Then I wrapped the part of the project that I needed to check how axios behaved.
<ErrorHandler>
<MainPage/>
</ErrorHandler>
Lastly, I import the axios instance(http) wherever I need it in the project.
Hope it helps anyone that wishes for a different approach.
I just combined both answers by Yevhenii Herasymchuk and GeorgeCodeHub, fixed some mistakes and ported it into React hooks. So here is my final working version:
// [project-directory]/src/lib/axios.js
import Axios from 'axios';
const axios = Axios.create({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
withCredentials: true,
});
export default axios;
// [project-directory]/src/components/AxiosErrorHandler.js
import {useEffect} from 'react';
import axios from '#/lib/axios';
const AxiosErrorHandler = ({children}) => {
useEffect(() => {
// Request interceptor
const requestInterceptor = axios.interceptors.request.use((request) => {
// Do something here with request if you need to
return request;
});
// Response interceptor
const responseInterceptor = axios.interceptors.response.use((response) => {
// Handle response here
return response;
}, (error) => {
// Handle errors here
if (error.response?.status) {
switch (error.response.status) {
case 401:
// Handle Unauthenticated here
break;
case 403:
// Handle Unauthorized here
break;
// ... And so on
}
}
return error;
});
return () => {
// Remove handlers here
axios.interceptors.request.eject(requestInterceptor);
axios.interceptors.response.eject(responseInterceptor);
};
}, []);
return children;
};
export default AxiosErrorHandler;
Usage:
// Wrap it around your Layout or any component that you want
return (
<AxiosErrorHandler>
<div>Hello from my layout</div>
</AxiosErrorHandler>
);
I use axios for ajax requests and reactJS + flux for render UI. In my app there is third side timeline (reactJS component). Timeline can be managed by mouse's scroll. App sends ajax request for the actual data after any scroll event. Problem that processing of request at server can be more slow than next scroll event. In this case app can have several (2-3 usually) requests that already is deprecated because user scrolls further. it is a problem because every time at receiving of new data timeline begins redraw. (Because it's reactJS + flux) Because of this, the user sees the movement of the timeline back and forth several times. The easiest way to solve this problem, it just abort previous ajax request as in jQuery. For example:
$(document).ready(
var xhr;
var fn = function(){
if(xhr && xhr.readyState != 4){
xhr.abort();
}
xhr = $.ajax({
url: 'ajax/progress.ftl',
success: function(data) {
//do something
}
});
};
var interval = setInterval(fn, 500);
);
How to cancel/abort requests in axios?
Axios does not support canceling requests at the moment. Please see this issue for details.
UPDATE: Cancellation support was added in axios v0.15.
EDIT: The axios cancel token API is based on the withdrawn cancelable promises proposal.
UPDATE 2022: Starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
Example:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
Using useEffect hook:
useEffect(() => {
const ourRequest = Axios.CancelToken.source() // <-- 1st step
const fetchPost = async () => {
try {
const response = await Axios.get(`endpointURL`, {
cancelToken: ourRequest.token, // <-- 2nd step
})
console.log(response.data)
setPost(response.data)
setIsLoading(false)
} catch (err) {
console.log('There was a problem or request was cancelled.')
}
}
fetchPost()
return () => {
ourRequest.cancel() // <-- 3rd step
}
}, [])
Note: For POST request, pass cancelToken as 3rd argument
Axios.post(`endpointURL`, {data}, {
cancelToken: ourRequest.token, // 2nd step
})
Typically you want to cancel the previous ajax request and ignore it's coming response, only when a new ajax request of that instance is started, for this purpose, do the following:
Example: getting some comments from API:
// declare an ajax request's cancelToken (globally)
let ajaxRequest = null;
function getComments() {
// cancel previous ajax if exists
if (ajaxRequest ) {
ajaxRequest.cancel();
}
// creates a new token for upcomming ajax (overwrite the previous one)
ajaxRequest = axios.CancelToken.source();
return axios.get('/api/get-comments', { cancelToken: ajaxRequest.token }).then((response) => {
console.log(response.data)
}).catch(function(err) {
if (axios.isCancel(err)) {
console.log('Previous request canceled, new request is send', err.message);
} else {
// handle error
}
});
}
import React, { Component } from "react";
import axios from "axios";
const CancelToken = axios.CancelToken;
let cancel;
class Abc extends Component {
componentDidMount() {
this.Api();
}
Api() {
// Cancel previous request
if (cancel !== undefined) {
cancel();
}
axios.post(URL, reqBody, {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
}),
})
.then((response) => {
//responce Body
})
.catch((error) => {
if (axios.isCancel(error)) {
console.log("post Request canceled");
}
});
}
render() {
return <h2>cancel Axios Request</h2>;
}
}
export default Abc;
There is really nice package with few examples of usage called axios-cancel.
I've found it very helpful.
Here is the link: https://www.npmjs.com/package/axios-cancel
https://github.com/axios/axios#cancellation
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
let url = 'www.url.com'
axios.get(url, {
progress: false,
cancelToken: source.token
})
.then(resp => {
alert('done')
})
setTimeout(() => {
source.cancel('Operation canceled by the user.');
},'1000')
This is how I did it using promises in node. Pollings stop after making the first request.
var axios = require('axios');
var CancelToken = axios.CancelToken;
var cancel;
axios.get('www.url.com',
{
cancelToken: new CancelToken(
function executor(c) {
cancel = c;
})
}
).then((response) =>{
cancel();
})
Using cp-axios wrapper you able to abort your requests with three diffent types of the cancellation API:
1. Promise cancallation API (CPromise):
Live browser example
const cpAxios= require('cp-axios');
const url= 'https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=5s';
const chain = cpAxios(url)
.timeout(5000)
.then(response=> {
console.log(`Done: ${JSON.stringify(response.data)}`)
}, err => {
console.warn(`Request failed: ${err}`)
});
setTimeout(() => {
chain.cancel();
}, 500);
2. Using AbortController signal API:
const cpAxios= require('cp-axios');
const CPromise= require('c-promise2');
const url= 'https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=5s';
const abortController = new CPromise.AbortController();
const {signal} = abortController;
const chain = cpAxios(url, {signal})
.timeout(5000)
.then(response=> {
console.log(`Done: ${JSON.stringify(response.data)}`)
}, err => {
console.warn(`Request failed: ${err}`)
});
setTimeout(() => {
abortController.abort();
}, 500);
3. Using a plain axios cancelToken:
const cpAxios= require('cp-axios');
const url= 'https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=5s';
const source = cpAxios.CancelToken.source();
cpAxios(url, {cancelToken: source.token})
.timeout(5000)
.then(response=> {
console.log(`Done: ${JSON.stringify(response.data)}`)
}, err => {
console.warn(`Request failed: ${err}`)
});
setTimeout(() => {
source.cancel();
}, 500);
4. Usage in a custom React hook (Live Demo):
import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
/*
Note: the related network request will be aborted as well
Check out your network console
*/
function TestComponent({ url, timeout }) {
const [cancel, done, result, err] = useAsyncEffect(
function* () {
return (yield cpAxios(url).timeout(timeout)).data;
},
{ states: true, deps: [url] }
);
return (
<div>
{done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
<button onClick={cancel} disabled={done}>
Cancel async effect (abort request)
</button>
</div>
);
}
Update
Axios v0.22.0+ supports AbortController natively:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
Starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
CancelToken deprecated
You can also cancel a request using a CancelToken.
The axios cancel token API is based on the withdrawn cancelable promises proposal.
This API is deprecated since v0.22.0 and shouldn't be used in new projects
You can create a cancel token using the CancelToken.source factory as shown below:
import {useState, useEffect} from 'react'
export function useProfileInformation({accessToken}) {
const [profileInfo, setProfileInfo] = useState(null)
useEffect(() => {
const abortController = new AbortController()
window
.fetch('https://api.example.com/v1/me', {
headers: {Authorization: `Bearer ${accessToken}`},
method: 'GET',
mode: 'cors',
signal: abortController.signal,
})
.then(res => res.json())
.then(res => setProfileInfo(res.profileInfo))
return function cancel() {
abortController.abort()
}
}, [accessToken])
return profileInfo
}
// src/app.jsx
import React from 'react'
import {useProfileInformation} from './hooks/useProfileInformation'
export function App({accessToken}) {
try {
const profileInfo = useProfileInformation({accessToken})
if (profileInfo) {
return <h1>Hey, ${profileInfo.name}!</h1>
} else {
return <h1>Loading Profile Information</h1>
}
} catch (err) {
return <h1>Failed to load profile. Error: {err.message}</h1>
}
}