fetch-event-source icon indicating copy to clipboard operation
fetch-event-source copied to clipboard

How to close the connection from client side

Open yuvraj88 opened this issue 3 years ago • 16 comments
trafficstars

I am getting the responses but need to stop the event source on a particular condition how can we do it from client side?

yuvraj88 avatar Sep 23 '22 09:09 yuvraj88

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 avatar Nov 03 '22 15:11 ChepteaCatalin

@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.

pociej avatar Feb 02 '23 13:02 pociej

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

jchiare avatar Mar 13 '23 09:03 jchiare

@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.

Make sure your controller or signal are the same. Especially in React.

xiaohundun avatar Mar 14 '23 03:03 xiaohundun

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

Systemcluster avatar Mar 15 '23 16:03 Systemcluster

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();
}

JuneDan avatar Aug 24 '23 10:08 JuneDan

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()

Codefa avatar Sep 25 '23 09:09 Codefa

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.

lucrus73 avatar Sep 25 '23 10:09 lucrus73

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!

nickscamara avatar Oct 13 '23 19:10 nickscamara

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());

bsaphier avatar Nov 06 '23 22:11 bsaphier

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).

rzec-allma avatar Nov 15 '23 15:11 rzec-allma

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!

spen avatar Jan 29 '24 13:01 spen

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!

actually abortcontroller not working here I do this at onClose &onError add this throw new Error() that will do

Codefa avatar Jan 29 '24 13:01 Codefa

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!

welltrade avatar Feb 21 '24 15:02 welltrade

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());

yes the useRef is better solution here and you can reinitialize it during close and error , and if you want to retrigger the functinoality.

Mgrdich avatar Mar 07 '24 14:03 Mgrdich

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());

} `

Ravibuzzyears avatar Mar 13 '24 07:03 Ravibuzzyears