binaryminds / react-native-sse

Event Source implementation for React Native. Server-Sent Events (SSE) for iOS and Android 🚀
https://www.npmjs.com/package/react-native-sse
MIT License
202 stars 30 forks source link

Fetch request restarting without finishing #41

Closed jackadair closed 7 months ago

jackadair commented 7 months ago

Hello, trying to use this package to consume events emitted by my backend server using sse_starlette

This is the formatting of my events:

{
                "event": "message",
                "data": token + "\n\n",
 }

However when I try to fetch them in the frontend the request seems to be restarting before finishing, it eventually reaches the end and closes but not before restarting a few times:

 DEBUG  [EventSource] Will open new connection in 500 ms.
 DEBUG  [EventSource][onreadystatechange] ReadyState: HEADERS_RECEIVED(2), status: 200
 DEBUG  [EventSource][onreadystatechange] ReadyState: LOADING(3), status: 200
 LOG  Event: undefined
 DEBUG  [EventSource][onreadystatechange][OPEN] Connection opened.
 LOG  Event:
 DEBUG  [EventSource][onreadystatechange] ReadyState: LOADING(3), status: 200
 LOG  Event:
 LOG  Event: Hi
 LOG  Event: !
 LOG  Event: Welcome
 DEBUG  [EventSource][onreadystatechange] ReadyState: LOADING(3), status: 200
 LOG  Event:
 LOG  Event: Hi
 LOG  Event: !
 LOG  Event: Welcome
 LOG  Event: to
 LOG  Event: Canada
 LOG  Event: !
 DEBUG  [EventSource][onreadystatechange] ReadyState: LOADING(3), status: 200
 LOG  Event:
 LOG  Event: Hi
 LOG  Event: !
 LOG  Event: Welcome
 LOG  Event: to
 LOG  Event: Canada
 LOG  Event: !
 LOG  Event: How
 LOG  Event: can
 LOG  Event: I
 DEBUG  [EventSource][onreadystatechange] ReadyState: LOADING(3), status: 200
 LOG  Event:
 LOG  Event: Hi
 LOG  Event: !
 LOG  Event: Welcome
 LOG  Event: to
 LOG  Event: Canada
 LOG  Event: !
 LOG  Event: How
 LOG  Event: can
 LOG  Event: I
 LOG  Event: assist
 LOG  Event: you
 LOG  Event: today
 LOG  Event: ?
 LOG  Event:
 LOG  Event: STOP
 DEBUG  [EventSource][onreadystatechange] ReadyState: DONE(4), status: 200
 LOG  Event:
 LOG  Event: Hi
 LOG  Event: !
 LOG  Event: Welcome
 LOG  Event: to
 LOG  Event: Canada
 LOG  Event: !
 LOG  Event: How
 LOG  Event: can
 LOG  Event: I
 LOG  Event: assist
 LOG  Event: you
 LOG  Event: today
 LOG  Event: ?
 LOG  Event:
 LOG  Event: STOP
 DEBUG  [EventSource][onreadystatechange][DONE] Operation done.

Frontend Code:

const chat = async () => {
    const stream = new EventSource(
      "https://my-url.com",
      {
        body: JSON.stringify({
          query: inputText,
        }),
        headers: {
          "Content-Type": "application/json",
        },
        method: "POST",
        pollingInterval: 0,
        debug: true,

      }
    );

    const eventListener = (event: any) => {
      console.log("Event: " + event.data);
    };

    stream.addEventListener("message", eventListener);
    stream.addEventListener("open", eventListener)
    stream.addEventListener("close", eventListener)

    return () => {

      stream.removeAllEventListeners();
      stream.close();
    };
  };

Using latest package version with Expo SDK 49. My backend server works as expected when using Postman but only seems to have issues in my frontend environment.

Any help is appreciated!

EmilJunker commented 7 months ago

From the logs you posted, it looks like each time it receives new events, it also re-fires all the events that came before that. The most likely reason why this is happening is because the response text from the backend doesn't have the correct format. Please check the response again with Postman and maybe provide an example here so we can see what it looks like.

The response text needs to look something like this. Note the double newlines \n\n between each event block. They are very important, react-native-sse internally relies on them for parsing.

event: message
data: Welcome

event: message
data: to

event: message
data: Canada

For more information on the SSE protocol, also see https://javascript.info/server-sent-events

jackadair commented 7 months ago

@EmilJunker thanks for the quick response found my issue, leaving this here for anyone also using sse-starlette

Make sure you set your sep attribute like so: return EventSourceResponse(api_stream, sep="\n")

manish181192 commented 7 months ago

OMG i cant believe i spent 3 days figuring out this, thanks @jackadair for the solution. It should give a descriptive error if it cant parse. Successfully showing the message is misleading :(

jackadair commented 7 months ago

@manish181192 I agree with you, could probably have saved this issue report if this behaviour caused an error to be thrown.

@EmilJunker what do you think? Doesn't look like this was the first issue report that stemmed from the \n\n rule. I get wanting people to actually be familiar with the SSE protocol but throwing an error message may save you time in the long run if more people encounter this issue. I'm not too familiar with the source code of this package though so maybe it would be difficult to implement.

Thanks!

EmilJunker commented 7 months ago

@jackadair Could you elaborate a bit more what exactly the actual issue was that you encountered? What is the behavior when you don't pass the sep="\n" argument? Does it use a different line ending like \r\n? Or does it only add one newline instead of the required double newline?

The thing is: The SSE protocol is pretty strict and doesn't leave much room for interpretation. Different message data blocks must be separated by a double newline. If the are two data blocks separated by only one newline, it represents a line break within the same message.

data: This is the first message

data: This is the second message

data: This is line one of the third message
data: and this is line two of the third message

Source: https://javascript.info/server-sent-events

jackadair commented 7 months ago

@EmilJunker Yes as you've guessed this is the default separator on sse-starlette: self.DEFAULT_SEPARATOR = "\r\n"

Maybe instead this would be an issue to raise with sse-starlette as I'm not sure why their default separator would be incorrect as you've pointed out.

EmilJunker commented 7 months ago

this is the default separator on sse-starlette: self.DEFAULT_SEPARATOR = "\r\n"

@jackadair Thank you for the clarification. In that case, maybe we should adjust the parsing logic in react-native-sse to be able to match both \n and \r\n (using a regex or something). I'll see what I can do.