How do I cancel this axios request - javascript

I'm getting a memory leak when my browser is redirected away from this component. So I need to cancel it, but for some reason my cancel token isn't having any effect, and I'm wondering why.
Please take a look, below is my code:
const getBoards = async (**cancelToken**) => {
try {
if (localStorage.getItem("token") == null) {
throw new Error();
}
const response = await Axios.get("/boards", config, **{ cancelToken }**);
setBoards(response.data);
} catch (e) {}
};
useEffect(() => {
**const request = Axios.CancelToken.source();**
getBoards(request);
return () => {
**request.cancel();**
};
}, []);

You use the token via the .token property, you don't pass the entire object from source() as the token. From the documentation:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
So translating that to your code,
getBoards(request);
would be
getBoards(request.token);
// −−−−−−−−−−−−−−^^^^^^

Related

How to stream x-ndjson content using Express and parse the streamed data?

I have a TS library using Node v19.1.0. The library has a function that observes streamed server events.
The server provides a /events route streaming 'application/x-ndjson' content which might be an event/ping/... ( sending a ping every x seconds is important to keep the connection alive )
My observe function parses the streamed data and inspects it. If it is a valid event it will pass it to a callback function. The caller also receives an abort function to abort the streaming on demand.
Whenever I run tests locally or via CI I get the following error
Warning: Test "observes events." generated asynchronous activity after the test ended. This activity created the error "AbortError: The operation was aborted." and would have caused the test to fail, but instead triggered an unhandledRejection event.
I tried to minimize the example code using plain JavaScript
const assert = require('assert/strict');
const express = require('express');
const { it } = require('node:test');
it('observes events.', async () => {
const expectedEvent = { type: 'event', payload: { metadata: { type: 'entity-created', commandId: 'commandId' } } };
const api = express();
const server = api
.use(express.json())
.post('/events', (request, response) => {
response.writeHead(200, {
'content-type': 'application/x-ndjson',
});
const line = JSON.stringify(expectedEvent) + '\n';
response.write(line);
})
.listen(3000);
let stopObserving = () => {
throw new Error('should never happen');
};
const actualEventPayload = await new Promise(async resolve => {
stopObserving = await observeEvents(async newEvent => {
resolve(newEvent);
});
});
stopObserving();
server.closeAllConnections();
server.close();
assert.deepEqual(actualEventPayload, expectedEvent.payload);
});
const observeEvents = async function (onReceivedFn) {
const abortController = new AbortController();
const response = await fetch('http://localhost:3000/events', {
method: 'POST',
headers: { 'content-type': 'application/json' },
signal: abortController.signal,
});
if (!response.ok) {
throw new Error('error handling goes here - request failed');
}
Promise.resolve().then(async () => {
if (!response.body) {
throw new Error('error handling goes here - missing response body');
}
for await (const item of parseStream(response.body, abortController)) {
switch (item.type) {
case 'event': {
await onReceivedFn(item.payload);
break;
}
case 'ping':
// Intentionally left blank
break;
case 'error':
throw new Error('error handling goes here - stream failed');
default:
throw new Error('error handling goes here - should never happen');
}
}
});
return () => { abortController.abort(); };
};
const parseLine = function () {
return new TransformStream({
transform(chunk, controller) {
try {
const data = JSON.parse(chunk);
// ... check if this is a valid line...
controller.enqueue(data);
} catch (error) {
controller.error(error);
}
},
});
};
const splitLines = function () {
let buffer = '';
return new TransformStream({
transform(chunk, controller) {
buffer += chunk;
const lines = buffer.split('\n');
for (let i = 0; i < lines.length - 1; i++) {
controller.enqueue(lines[i]);
}
buffer = lines.at(-1) ?? '';
},
flush(controller) {
if (buffer.length > 0) {
controller.enqueue(buffer);
}
},
});
};
const parseStream = async function* (stream, abortController) {
let streamReader;
try {
const pipedStream = stream
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitLines())
.pipeThrough(parseLine());
streamReader = pipedStream.getReader();
while (true) {
const item = await streamReader.read();
if (item.done) {
break;
}
yield item.value;
}
} finally {
await streamReader?.cancel();
abortController.abort();
}
};
Unfortunately, when running node --test, the test does not finish. I have to cancel it manually.
The test breaks with these lines
const actualEventPayload = await new Promise(async resolve => {
stopObserving = await observeEvents(async newEvent => {
resolve(newEvent);
});
});
and I think that's because the Promise never resolves. I thought the stream parsing might have a bug but if you remove all the stream parsing stuff and replace
Promise.resolve().then(async () => {
/* ... */
});
with
Promise.resolve().then(async () => {
await onReceivedFn({ metadata: { type: 'entity-created', commandId: 'commandId' }});
});
it doesn't work neither. Does someone know what's wrong or missing?
The problem here has nothing to do with your promise not resolving since you never even get to that point.
The problem here is that observeEvents is not yet initialized when the test is being run and thus throws a ReferenceError: Cannot access 'observeEvents' before initialization error.
To see that for yourself you can add a simple const it = (name, fn) => fn(); stub to the top of the file and run it without the --test.
There are multiple ways to fix this and the simplest one is to move the test function to the bottom of the file.
If you don't want to do that you can also define the observeEvents function like this: async function observeEvents(onReceivedFn) {...}. This way it will be available immediately.

error boundary when using hooks to display data

I try to get user data when load page using hooks(in case useEffect), but i got error said "Consider adding an error boundary to your tree" and my app getting blank.
my code looks similar like below
still configure out how to fix it, but i dont know how. coz i new using react
const [ProfileData, setProfileData] = useState({});
const [ownerId, setOwnerId] = useState('')
let ProfileID = 12
const getId = async () => {
const responseData = await getUserData();
setOwnerId(responseData.ID);
};
this is my function to get data from API
const getProfileData = () => {
setLoading(true);
getId();
const jsonData = {
ID: ProfileID,
OwnerId: ownerId
};
const headers = {
'Access-Control-Allow-Headers': '*'
};
try {
axios
.post(
config.API_SERVER + 'SearchProfile',
{
Data: jsonData ,
},
headers
)
.then(function (response) {
if (response.data.status == 'Success') {
setProfileData(response.data.Data);
setLoading(false);
} else {
setProfileData({});
alert(response.data.status);
}
})
} catch (error) {
console.log(error);
}
};
this is my hooks
useEffect(() => {
const load = async () => {
try {
await getProfileData();
} catch (err) {
throw err
}
}
load()
}, []);
and this is my return to display data
return (
<p>{ProfileData.Name}</p>
)
Since you throw an error (throw err) but you don't catch it, you can omit that part, pass the error message to state, or use react-error-boundary.
useEffect(() => {
getProfileData();
}, []);

Preventing Unnecessary Requests when update the input

How to preventing unnecessary requests when update the input?
I tried below solution.But in the App file, that search is declared but never used. I tried something like: https://alligator.io/react/live-search-with-axios/.
What is the variable let token in the fileutils.js. Should I assign let token = localStorage.getItem ('token') to this variable;?
App
import search from /.utils
class App extends Component {
constructor (props) {
super(props);
this.state = {
todos: [],
}
}
search = (query) => {
axios({
url: `/api/v1/todos/{query}`,
method: "GET"
})
.then(res => {
this.setState({
todos: res.data
});
})
.catch(error => {
console.log(error);
})
render () {
return (
<input onChange={this.search} />
)
}
}
utils.js
import axios from 'axios';
const makeRequestCreator = () => {
let token;
return (query) => {
// Check if we made a request
if(token){
// Cancel the previous request before making a new request
token.cancel()
}
// Create a new CancelToken
token = axios.CancelToken.source()
try{
const res = axios(query, {cancelToken: cancel.token})
const result = data.data
return result;
} catch(error) {
if(axios.isCancel(error)) {
// Handle if request was cancelled
console.log('Request canceled', error.message);
} else {
// Handle usual errors
console.log('Something went wrong: ', error.message)
}
}
}
}
const search = makeRequestCreator()
export default search;
You can do that with a function that delays executing of your onChange.you can use debounce function from lodash.js
// _.debounce(yourSearch function, delay time);
search(e){
let str = e.target.value;
_.debounce(() => yourFunction, 500);
}

Dispatch async action

By changing my action to async I am not able to dispatch it. Whenever I make the dispatch it enters the cath with the value false. Here is my page where I dispatch the action from mounted hook (I tried created too)
mounted () {
this.$store.dispatch('productById', this.$route.params['id']).then((response) => {
this.product = response
})
.catch(err => {
console.log(err)
})
}
And this is my action
async productById ({commit}, payload) {
const AuthStr = await getAdminOrRespondentAuth()
return new Promise((resolve, reject) => {
commit(PRODUCT_BY_ID)
axios.get(`${API_BASE}/products/${payload}`, {
params: {
origin: '1'
},
transformRequest: [function (data, headers) {
delete headers.common.Authorization
headers.Authorization = AuthStr
return data
}],
paramsSerializer: params => parseParams(params)
}).then(response => {
if (response.status === 200) {
commit(PRODUCT_BY_ID_SUCCESS, response.data)
resolve(response.data)
} else {
reject(response)
}
})
.catch(err => {
if (err.response.data.idStatus === 1) {
commit(PRODUCT_BY_ID_SUCCESS, err.response.data.data)
reject(err)
}
})
})
}
When the Vue enters mounted hook it "dispatch the action" and goes straight into the catch block not calling my action. My action is not executed.
If I change my action to sync, everything works normally. I need this action to be async because getAdminOrRespondentAuth function thats call oidc async method to recover user.
What I'm doing wrong?
#Samurai8 is right. Thanks for the help. My getAdminOrRespondentAuth was not returning a promise correctly. After correcting the error of the function everything came back to work. This is the function that generate error discribed:
async function getAdminOrRespondentAuth () {
let mgr = new Mgr()
var adminToken = await mgr.getToken()
if (adminToken !== false) {
return 'Bearer '.concat(adminToken)
} else {
let usrToken = localStorage.getItem('user-token')
return 'Bearer '.concat(usrToken)
}
}
Here is the function that works:
async function getAdminOrRespondentAuth () {
var adminToken = ''
return new Promise(async (resolve, reject) => {
let mgr = new Mgr()
try {
adminToken = await mgr.getToken()
} catch (error) {
adminToken = error
}
if (adminToken !== false) {
resolve('Bearer '.concat(adminToken))
} else {
let usrToken = localStorage.getItem('user-token')
if (usrToken !== null) {
resolve('Bearer '.concat(usrToken))
} else {
resolve('')
}
}
})
}

Axios: how to cancel request inside request interceptor properly?

I want to cancel the request if there's no token, so I do like this:
instance.interceptors.request.use(config => {
if (!getToken()) {
console.log("interceptors: no access token");
} else {
config.headers.Authorization = "Bearer " + getToken().accessToken;
return config;
}
});
But in negative scenario there's an error TypeError: Cannot read property 'cancelToken' of undefined.
You cannot use the token inside the interceptors but instead throw Cancel
axios.interceptors.response.use(function (response) {
throw new axios.Cancel('Operation canceled by the user.');
}, function (error) {
return Promise.reject(error);
});
Refer to this post:
https://github.com/axios/axios/issues/583
Axios v0.22.0 and higher
As per the documentation, cancellation is now pretty straightforward with the AbortController class
instance.interceptors.request.use(config => {
/* some logic */
const controller = new AbortController();
if (needToCancelRequest) {
controller.abort();
}
return {
...config,
signal: controller.signal
};
});
Browser Compatibility
You might be tempted to do a pretty concise signal: AbortSignal.abort() instead. Please, note that it is much less supported than the solution above. See AbortSignal.abort() vs new AbortController().abort() compatibility.
Axios before v0.22.0
This is a solution taken from the axios issue on github
instance.interceptors.request.use(config => {
/* some logic */
return {
...config,
cancelToken: new CancelToken((cancel) => {
if (needToCancelRequest) {
cancel('Cancel repeated request')
}
})
};
});
So for whatever reason none of these answers worked for me. Here is what did.
axiosInstance.interceptors.request.use(
function (config) {
const controller = new AbortController();
const cfg = {
...config,
signal: controller.signal,
};
controller.abort('We gotta cancel this');
return cfg;
},
function (error) {
return Promise.reject(error);
},
);
Thing I learned from this: AbortController is native to javascript/node.
I have implemented this in this way. I am not sure if this is the best solution, but for my use case is useful.
My idea is not to cancel the last request. I would like to cancel previous requests to the same endpoint, and let the last one to do his job. For that reason I keep track of the request that are being executed.
// I keep track of the current requests that are being executed
const currentExecutingRequests = {};
axios.interceptors.request.use(
(req) => {
let originalRequest = req;
if (currentExecutingRequests[req.url]) {
const source = currentExecutingRequests[req.url];
delete currentExecutingRequests[req.url];
source.cancel();
}
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
originalRequest.cancelToken = source.token;
currentExecutingRequests[req.url] = source;
// here you could add the authorization header to the request
return originalRequest;
},
(err) => {
return Promise.reject(err);
}
);
axios.interceptors.response.use(
(response) => {
if (currentExecutingRequests[response.request.responseURL]) {
// here you clean the request
delete currentExecutingRequests[response.request.responseURL];
}
return response;
},
(error) => {
const { config, response } = error;
const originalRequest = config;
if (axios.isCancel(error)) {
// here you check if this is a cancelled request to drop it silently (without error)
return new Promise(() => {});
}
if (currentExecutingRequests[originalRequest.url]) {
// here you clean the request
delete currentExecutingRequests[originalRequest.url];
}
// here you could check expired token and refresh it if necessary
return Promise.reject(error);
}
);
As of Axios v0.22.0 an AbortSignal is the recommended way to cancel from a request interceptor.
axios.interceptors.request.use(
(requestConfig) => {
/* some logic */
return {
...requestConfig,
signal: AbortSignal.abort()
};
}
},
(error) => {
return Promise.reject(error);
}
);
#Kirill Taletski's answer solve this perfectly, but add one line:
const CancelToken = Axios.CancelToken;
then ,it gonna be like this :
instance.interceptors.request.use(config => {
/* some logic */
const CancelToken = Axios.CancelToken;
return {
...config,
cancelToken: new CancelToken((cancel) => cancel('Cancel repeated request'))
};
});
here is the solution
import axios from 'axios';
const CancelToken = axios.CancelToken;
let cancel;
axios.interceptors.request.use((config) => {
if (cancel) {
cancel(); // cancel request
}
config.cancelToken = new CancelToken(function executor(c)
{
cancel = c;
})
return config
}, function (error) {
return Promise.reject(error)
});
My solution based on https://stackoverflow.com/a/64228288/2051938
axios.ts
const axiosInstance = axios.create({ baseURL: apiBaseUrl });
axiosInstance.interceptors.request.use(
req => {
const originalRequest = req;
const cancelUniqId = (originalRequest.cancelToken as unknown) as string;
if (Object.hasOwnProperty.call(currentExecutingRequests, cancelUniqId)) {
const source = currentExecutingRequests[cancelUniqId];
delete currentExecutingRequests[cancelUniqId];
source.cancel();
}
if (cancelUniqId) {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
originalRequest.cancelToken = source.token;
currentExecutingRequests[cancelUniqId] = source;
}
return originalRequest;
},
err => {
return Promise.reject(err);
}
);
axiosInstance.interceptors.response.use(
response => {
for (const key of Object.keys(currentExecutingRequests)) {
if (currentExecutingRequests[key].token === response.config.cancelToken) {
delete currentExecutingRequests[key];
break;
}
}
return response;
},
error => {
const { response } = error;
if (axios.isCancel(error)) {
return new Promise(() => {
//
});
}
for (const key of Object.keys(currentExecutingRequests)) {
if (currentExecutingRequests[key].token === response.config.cancelToken) {
delete currentExecutingRequests[key];
break;
}
}
return Promise.reject(error);
}
);
export { axiosInstance };
Usage:
axiosInstance.request({
url: "some/req/path",
method: "POST",
params: {...},
data: {...},
cancelToken: "someUniqRequestID" // <-- IMPORTANT!
})
as a result, all requests with someUniqRequestID token will be cancelled when previous request with SAME cancelToken was not finished before.
This works for me for axios 0.20.0:
const interceptorRequestConfig = (config) => {
if (sourceRequest[config.url]) {
sourceRequest[config.url].cancel('Automatic cancellation')
}
const axiosSource = axios.CancelToken.source()
sourceRequest[config.url] = { cancel: axiosSource.cancel }
config.cancelToken = axiosSource.token
return config
}
credit for the idea: https://stackoverflow.com/a/66701130/8840359

Categories

Resources