EventSource / eventsource

EventSource client for Node.js and Browser (polyfill)
MIT License
908 stars 252 forks source link

Header Last-Event-ID is not automatically sent on reconnect #291

Open matpen opened 2 years ago

matpen commented 2 years ago

I set up a simple test where the following happens:

  1. on client-side, we connect to the server with EventSource;
  2. the server will start sending events, once every two seconds: the events contain the id field;
  3. on client-side, we log the received events, to verify that everything is working correctly;
  4. then the server is intentionally restarted, in order to simulate a loss of connection;
  5. the client will wait a few seconds, then correctly re-attempt connection: however, in this case the header Last-Event-ID is not sent to the server.

IIUC this does not conform to the specs. This is also confirmed by running the test described above with the native implementation of EventSource in Chrome (i.e. not this repo), which correctly sends the header.

SimonX200 commented 1 year ago

I am currently implementing and testing SSE too.

Here that last-event-id works.

On reconnect the last received id is sent back as last-event-id.

We use eventsource 2.0.2 for the test-client and nestjs 9.1.2 as backend

@Sse('sse') public async sseEndpoint( @Headers('last-event-id') lastEventId: string, @Query('request') requestStr: string ): Promise<Observable> { return await data(requestStr, lastEventId); }

SimonX200 commented 1 year ago

A more detail testing shows that there is a problem with multiple reconnects in a row.

If one reconnect fails with some http-error-code then the next successful reconnect sends the first ever received id as last-event-id.

A single successful reconnect attempt always sends the correct last-event-id.

BTW: Actually the last-event-id that is sent after multiple failed reconnects is always '1'.

joebailey26 commented 1 year ago

I was hitting rate limits with this bug as there would be continuous reconnects to my server, rather than waiting the 10 seconds like I wanted.

To get around this, I've written some code on the client side of my application that others might find helpful.

// Add some variables
let hasSseTimerExpired = false
let sseTimer = null
let isFirstSseRequest = true
let sseRetries = 0
function start_sse_timer () {
      hasSseTimerExpired = false
      sseTimer = setTimeout(() => {
        hasSseTimerExpired = true
      }, 9000)
}
function start_sse () {
        // If we already have a timer running, stop it
        if (sseTimer) {
          clearTimeout(this.sseTimer)
        }
        // Start the timer as we are starting the connection
        set_sse_timer()
        // Set isFirstSseRequest to true, as this time, we receive a response immediately
        isFirstSseRequest = true
        // Code to start the SSE connection
        ...
}
function stop_sse () {
      // Code to stop your SSE connection
      ...
}
function on_sse_message_received() {
      // If the timer has expired, it means it's been longer than 9 seconds so we can continue to process the request
      // We also check if this is the first request, if it is, we can ignore the timer as a response is returned immediately
      if (hasSseTimerExpired || isFirstSseRequest) {
          // We set the timer as we are now waiting on the next SSE request
          set_sse_timer()
          // We set isFirstSseRequest to false as we have now processed the first request
          if (isfirstSseRequest) {
                isFirstSseRequest = false
          }
          // Code to process the response from the SSE connection
          ...
          // We only want to retry this 3 times to avoid hammering the server
      } else if (sseRetries < 3) {
          // Restart SSE as something has gone wrong and a response has been returned sooner than 9 seconds
          sse_end()
          sse_start()
          sseRetries++
      }
}