0

Goal: Use Server-Sent Events in my Angular App with Express Backend

Problem: Server-Sent Events do not reach the client

Backend Code
router.get('/options/:a/:b/:c', async (req, res) => {
    console.log('options endpoint called..', req.params.a,req.params.b,req.params.c);

    // ---- SERVER SENT EVENTS ---- //
   
    // Setting Headers
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader("Access-Control-Allow-Origin", "*");   
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders(); // flush the headers to establish SSE with client
   
   for(let i=0;i<10;i++){
        console.log("should now send this response...banane")
        res.write('banane!')
    }
});
Client Side Code
export class NetworkingService {

  BASE_URL;
  OPTIONS = 'options'
  searchResultSubject: Subject<SearchResults>;


  constructor(private http: HttpClient, private ls: LoggingService, private _zone: NgZone) { // shortened }
  getOptions(a: string, b: string, c: string) {
    this.ls.log("get options endpoint about to be triggered")
    this.getServerSentEvent(this.BASE_URL + this.OPTIONS + "/" + a + "/" + b + "/" + c).subscribe(resp => {
      this.ls.log("got from server: ", resp)
     });
    return this.searchResultSubject;
  }


 getServerSentEvent(url: string): Observable<any> {
    console.log('creating an eventsource...')
    return new Observable(observer => {
      const eventSource = this.getEventSource(url);
      eventSource.onopen = event => {
        console.log('opening connection', eventSource.readyState)
      };
      eventSource.onmessage = event => {
        console.log('event from server..')
        this._zone.run(() => {
          observer.next(event);
        });
      };
      eventSource.onerror = error => {
        console.log('error from server..')
        this._zone.run(() => {
          observer.error(error);
        });
      };
    });
  }

  private getEventSource(url: string): EventSource {
    return new EventSource(url);
  }
}
Server Side log output (as expected)
options endpoint called.. A B C
should now send this response...banane
should now send this response...banane
should now send this response...banane
should now send this response...banane
should now send this response...banane
should now send this response...banane
should now send this response...banane
should now send this response...banane
should now send this response...banane
should now send this response...banane
Client Side log output (NOT as expected)
get options endpoint about to be triggered
creating an eventsource...
opening connection 1

...and then nothing.

What have I tried?

I fail to see how I differ from these: so-question, tutorial, tutorial In the Networks Tab in Dev Tools I see a Status 200, type eventsource line entry with the correct headers. But only one!

I think I am making a really obvious mistake, since it is ALMOST working and seems to be straightforward from the examples. My Angular is 10.1.6 and express is 4.17.1 I am new to interacting directly with ngZone is there a potential error?

The problem persists even when I comment out the compression library or use res.flush(), as suggested here.

SLLegendre
  • 680
  • 6
  • 16
  • In the tutorials res.send() is never there either. only res.write. If I use res.send I run into an error message ~"Headers set after response was sent". From my understanding res.send() is used for normal responses as it is shorthand for res.write and res.end and in this case "res.write()" should be used because I have potentially many responses to write back to the client and do not want to .end() yet. – SLLegendre Apr 13 '21 at 23:37
  • Update: I ended up using a library https://www.npmjs.com/package/@toverux/expresse as suggested here: https://stackoverflow.com/questions/34657222/how-to-use-server-sent-events-in-express-js that works but I still do not know where I went wrong. – SLLegendre Apr 18 '21 at 21:09

3 Answers3

4

I was having the same problem, I was not getting the response on the client. After a number of changes it seems to be working.

First I set the headers:

response.setHeader('Cache-Control', 'no-cache');
response.setHeader('Content-Type', 'text/event-stream');
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader('Connection', 'keep alive');
response.setHeader('X-Accel-Buffering', 'no');
response.flushHeaders(); // flush headers to establish SSE with client

If you are using the compress middleware, it is necessary that when sending the data to the client, you put

response.flush();

example

response.write(`event: ${event}\ndata: ${data}\n\n`);
response.flush();

It seems that the client receives messages from the generated event, that is, if you send the client the following response

response.write(`event: some_event\ndata: some_data\n\n`);

the client should have a code similar to this:

const eventSource = new EventSource(url);

eventSource.addEventListener('some_event', listener => {
  console.log(listener);
  ...
}, false);

I hope to be helpful

daacdev
  • 101
  • 5
1

What worked for me was similar to what daacdev said, but not entirely.

Serverside I used:

res.setHeader('Cache-Control', 'no-cache, no-transform'); // Notice the no-transform! 
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders(); // Flush the headers to establish SSE with client

// Do some stuff...
// Like sending an event with data
res.write('event: fooEvent\n'); // Note the 1 newline after the event name
res.write(`data: ${JSON.stringify({ foo: 'bar' })}\n\n`); // Note the 2 newlines after the data

// And when you're done, end it
res.end();

And clientside, we have eventListeners for event types:

const eventSource = new EventSource('/api/foo');

eventSource.addEventListener('fooEvent', event => {
    console.log('Got fooEvent: ', event.data); // event.data has your data
    eventSource.close(); // Close the eventSource if the client is done
});

You can leave out the event type altogether and only send 'data' in your res.write(). In which case your client listens for event type-less messages like this:

eventSource.onmessage = event => {
    console.log('Got some data: ', event.data); // event.data has your data
    eventSource.close(); // Close the eventSource if the client is done

Noteworthy:

  • The Cache-Control header needed 'no-transform' aside from 'no-cache'. Before I did that, the client would only get the messages after the server ended the whole thing
  • I did not need to flush after every write
  • You can't only send an event type. It always needs data to follow it. So if you simply want to send a message, do something like:
res.write('event: doubleProcesses\n');
res.write('data: doubleProcesses\n\n');
  • You must adhere to the defined structure: the optional 'event' (followed by a colon, a name, and 1 newline) and the required 'data' (followed by a colon, your data as a string, and 2 newlines). You cannot simply put anything in your res.write.

That last point is for sure what you were missing.

Docs here:

Victor
  • 578
  • 7
  • 12
0

For anyone who landed here after searching similar issues, what our problem turns out to be is that our node.js app is being hosted on Azure windows, and it is using IIS under the hood. The handler iisnode has a default setting of buffering response, which is the reason why response from our server was never received at the client side. All we had to do was to edit the web.config files according to this Microsoft guide

https://learn.microsoft.com/en-us/azure/app-service/app-service-web-nodejs-best-practices-and-troubleshoot-guide

pay attention to the flushResponse section

ian gao
  • 11
  • 2