fetch-event-source
fetch-event-source copied to clipboard
How to close the connection from client side
I am getting the responses but need to stop the event source on a particular condition how can we do it from client side?
const controller = new AbortController()
const signal = controller.signal
pass signal to the object from the second parameter of fetchEventSource()
When you want to close the connection, just call controller.abort()
https://stackoverflow.com/questions/31061838/how-do-i-cancel-an-http-fetch-request
@ChepteaCatalin this doesnt work. Im not sure yet what the solution is but I checked above and after controller.abort() call messages are still there. onmessage callback is still firing.
Curious about this too. Aborting the fetch request didn't seem to work for me. Workaround I use is to throw an error in the onerror and onclose functions
onclose() {
// do something
throw new Error(); // hack to close the connection
},
onerror() {
// do something
throw new Error(); // hack to close the connection
@ChepteaCatalin this doesnt work. Im not sure yet what the solution is but I checked above and after controller.abort() call messages are still there.
onmessagecallback is still firing.
Make sure your controller or signal are the same. Especially in React.
Calling controller.abort() in onerror can't cancel retries with this library.
onerror is called here, after which the retry timer will be reset when it doesn't throw, without checking for aborted again:
https://github.com/Azure/fetch-event-source/blob/a0529492576e094374602f24d5e64b3a271b4576/src/fetch.ts#L129-L136
The retry timer will call create where a new AbortController is created that starts non-aborted:
https://github.com/Azure/fetch-event-source/blob/a0529492576e094374602f24d5e64b3a271b4576/src/fetch.ts#L102-L104
Retries will be stopped if controller.abort() was called in onopen, onmessage or onclose, but calls in onerror won't affect it.
Throwing in onerror is absolutely the correct thing to do to stop the request, which is also shown in the readme:
https://github.com/Azure/fetch-event-source/blob/a0529492576e094374602f24d5e64b3a271b4576/README.md?plain=1#L83-L86
Once an instance of AbortController is used, we need to create a new instance to reset the value. eg:
function stopResponseSSE() {
ctrl.abort()
ctrl = new AbortController();
}
its easy to do in modern browsers follow this.
const controller = new AbortController()
const { signal } = controller
await fetchEventSource('your API', {
onmessage(ev) {
console.log('data', ev.data)
},
onclose() {
controller.abort()
// do something
},
onerror() {
// do something
},
signal,
})
and When you want to close the connection, just call controller.abort()
its easy to do in modern browsers follow this.
Yes, it's easy and it's just as documented, except it doesn't close the connection, at least not in my case.
For those having the issue in React, this works:
const [ abortController, setAbortController ] = useState(new AbortController());
fetch(
signal: abortController.signal
// ...
)
function stopResponseSSE() {
abortController.abort();
setAbortController(new AbortController());
}
As June mentioned, "once an instance of AbortController is used, we need to create a new instance to reset the value."
In React, you most likely will need to set the state with a new Abort Controller.
Hope this helps!
For those having the issue in React, this works:
const [ abortController, setAbortController ] = useState(new AbortController()); fetch( signal: abortController.signal // ... ) function stopResponseSSE() { abortController.abort(); setAbortController(new AbortController()); }As June mentioned, "once an instance of AbortController is used, we need to create a new instance to reset the value."
In React, you most likely will need to set the state with a new Abort Controller.
Hope this helps!
Better to use a useRef to store the abortController instead of useState as you'll end up triggering a (likely undesired) re-render when you call setAbortController(new AbortController());
As @Systemcluster said, the only thing that works to stop the retry functionality is to throw an error in the onerror callback.
If this is the correct thing to do, it would probably help to add a small section to the readme about this (I would create a PR however I am not sure if this is the expected way to handle this cause or if throwing an error just happens to do what I want).
Hi folks!
I'm having a similar issue where aborting the control signal passed in to fetchEventSource doesn't cause the promise to throw and thus the .catch isn't called, contrary to what I would expect.
I have console.logs in the onclose, onerror callbacks of fetchEventSource, and in the .catch and .finally promise methods, and it seems that only the .finally is called when I call .abort on my controller.
Am I right in my expectations here or am I missing something? I'm happy to provide further details & code but first I just wanted to check if there was anything that I'm obviously missing first.
Thank you!
Hi folks!
I'm having a similar issue where aborting the control signal passed in to
fetchEventSourcedoesn't cause the promise to throw and thus the.catchisn't called, contrary to what I would expect.I have console.logs in the
onclose,onerrorcallbacks offetchEventSource, and in the.catchand.finallypromise methods, and it seems that only the.finallyis called when I call.aborton my controller.Am I right in my expectations here or am I missing something? I'm happy to provide further details & code but first I just wanted to check if there was anything that I'm obviously missing first.
Thank you!
actually abortcontroller not working here I do this
at onClose &onError
add this throw new Error() that will do
its easy to do in modern browsers follow this.
const controller = new AbortController() const { signal } = controller await fetchEventSource('your API', { onmessage(ev) { console.log('data', ev.data) }, onclose() { controller.abort() // do something }, onerror() { // do something }, signal, })and When you want to close the connection, just call
controller.abort()
thats works for me. Thanks!
For those having the issue in React, this works:
const [ abortController, setAbortController ] = useState(new AbortController()); fetch( signal: abortController.signal // ... ) function stopResponseSSE() { abortController.abort(); setAbortController(new AbortController()); }As June mentioned, "once an instance of AbortController is used, we need to create a new instance to reset the value." In React, you most likely will need to set the state with a new Abort Controller. Hope this helps!
Better to use a
useRefto store theabortControllerinstead ofuseStateas you'll end up triggering a (likely undesired) re-render when you callsetAbortController(new AbortController());
yes the useRef is better solution here and you can reinitialize it during close and error , and if you want to retrigger the functinoality.
const [ abortController, setAbortController ] = useState(new AbortController());
fetch( signal: abortController.signal // ... )
function stopResponseSSE() { abortController.abort(); setAbortController(new AbortController()); }
this one worked for me, thanks
===================================== ` const [ abortController, setAbortController ] = useState(new AbortController());
const fetchData = async (message?: ChatMessage, question_id?: string | null) => {
try {
const token = await getToken();
const headers = {
Authorization: `Bearer ${token}`,
Accept: "text/event-stream",
'Content-type': 'application/json'
};
let parsed = "";
let url: string = '';
let requestData = null;
if (chatStep === chatSteps.QuizScreen) {
url = API_URL + `/api/v0/chat/question/stream/${chatID}`;
requestData = {
new_messages: message ? [message] : chatStartMessageArray,
question_id: QuestionData ? QuestionData.question_id : null
}
} else if (chatStep === chatSteps.ConceptScreen) {
url = API_URL + `/api/v0/chat/videos/stream/${chatID}`;
requestData = {
new_messages: message ? [message] : chatStartMessageArray,
timestamp: activeVideo !== null ? playBackData[activeVideo].playTime : null,
video_id: activeVideo !== null ? playBackData[activeVideo].videoID : null
}
} else {
url = API_URL + `/api/v0/chat/stream/${chatID}`;
requestData = {
new_messages: message ? [message] : chatStartMessageArray,
};
}
await fetchEventSource(url, {
method: "POST",
headers: headers,
body: JSON.stringify(requestData),
onopen(res) {
return new Promise<void>((resolve, reject) => {
if (res.ok && res.status === 200) {
setOnApiResponse(true);
setShowLearnMore(false);
resolve();
} else if (
res.status >= 400 &&
res.status < 500 &&
res.status !== 429
) {
console.log("Client side error ", res);
reject();
}
});
},
onmessage(ev) {
if (ev.data) {
setOnApiResponse(false)
const newData = ev.data;
parsed += newData;
const temMsg = {
"role": "assistant",
"inserted": false,
"text": parsed
};
const assistantMessage = [...messages, temMsg];
setMessages(assistantMessage);
}
},
onclose() {
console.log('close called');
setOnApiResponse(false)
setShowLearnMore(true);
fetchChatHistory();
setIsStreaming(false);
if (chatStep === chatSteps.ActiveChatScreen) {
setFetchInitialHelpMsg(false);
}
if (chatStep === chatSteps.ConceptScreen) {
setFetchInitialVideoMsg(false);
}
if (chatStep === chatSteps.QuizScreen) {
setFetchInitialQuizMsg(false);
}
},
onerror(err) {
console.log(err);
setOnApiResponse(false);
setShowLearnMore(true);
setIsStreaming(false);
throw err;
},
signal: abortController.signal
});
} catch (error) {
console.error('Error:', error);
throw error;
}
}
const handleHalt = () => {
setIsStreaming(false);
abortController.abort();
setAbortController(new AbortController());
} `