Search code examples
javaspringangularserver-sent-events

SSE response from Spring app does not reach client side


Am trying to implement SSE for my application. My client is angular 4 app and I use the EventSourcePolyfill (to make it work for IE as well). My server side is spring and I use spring SseEmitter for the same.

I open a new SSE connection, based on a specific user event on the client side. I can see that the request reaches the server, SSE events are logged and I can see the response is getting created as well. I want to send response as json. I'm basically trying to send data updates, and am creating a json with the updated data. But the SSE never reaches the browser. It goes to the Eventsource.onerror method only. So the browser keeps retrying. *

In Chrome -> Dev tools -> Network tab, I can see the SSE request showing in pending for few seconds, then changes to cancelled.

  • I have put the code snippets and error information here.

Angular Code:

  let eventSource = new EventSourcePolyfill('v1/sse/getInfiniteMessages', {
  // headers: {
  //   'Accept': 'text/event-stream'
  // }, 
  heartbeatTimeout:180
});

eventSource.onmessage = (eventResponse => {
  console.log("Message from event source is :: " + eventResponse);
  console.log("JSON from event source is :: " + eventResponse.data);

});
eventSource.onopen = (a) => {
  // Do stuff here
  console.log("Eventsource.onopen.. " + JSON.stringify(a));
};
eventSource.onerror = (e) => {
  // Do stuff here
  console.log("Eventsource.onerror.. Exception is:: " + JSON.stringify(e));
  if (e.readyState == eventSource.CLOSED) {
    console.log('event source is closed');
    eventSource.close();
   }
   else {
     console.log("Not a event source closed error");
   }
}

Spring (server side)

Controller:

@RequestMapping(method = RequestMethod.GET, value = "/getInfiniteMessages")
public SseEmitter getInfiniteMessages() { 
    return iSSEService.getInfiniteMessages();
}

Service:

public SseEmitter getInfiniteMessages(String chatRefId) {
    logger.info("In SSEService.. getInfiniteMessages method.." );

    boolean stopSSE = false;
    while (!stopSSE) {
        try {
            ResponseVo responseVO = new ResponseVo();
            responseVO = getData();
//              Gson gson = new Gson();
//              String sseMessage = gson.toJson(responseVO);
//              logger.info("sseMessage to send: "  + sseMessage);
//                emitter.send(sseMessage , MediaType.APPLICATION_JSON);
            emitter.send(responseVO);

            Thread.sleep(30000);
            //stopSSE = true;
        } catch (Exception e) {
            e.printStackTrace();
            emitter.completeWithError(e);
            //return;
        }
    }

    /*for (int i = 0; i < 100; i++) {
        try {
            emitter.send(i + " - Message", MediaType.TEXT_PLAIN);

            Thread.sleep(10);
        } catch (Exception e) {
            e.printStackTrace();
            emitter.completeWithError(e);
            //return;
        }
    }*/

    return emitter;
}    

The commented lines of code are few things which I tried but did not work.

Note: In the service code, if I uncomment the for loop (running 100 times), the sse text reaches the browser. But I want to send a json with data updates. I tried creating object as json, and send as text, but it did not help.

Error:

From console.log on onError method :

Not a event source closed error
    Eventsource.onerror.. Exception is:: {"type":"error","target":{"listeners":{"data":{}},"url":"/v1/sse/getInfiniteMessages","readyState":0,"withCredentials":false}}

Browser error:

core.es5.js:1020 ERROR Error: No activity within 45000 milliseconds. Reconnecting.
    at eventsource.js:363
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:425)
    at Object.onInvokeTask (core.es5.js:3881)
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.runTask (zone.js:192)
    at webpackJsonp.../../../../zone.js/dist/zone.js.ZoneTask.invokeTask (zone.js:499)
    at ZoneTask.invoke (zone.js:488)
    at timer (zone.js:2040)

Looking for some help from friends here, to solve my issue.


Solution

  • You are working with the SseEmitter before returning it, it doesn't work like that. Since you don't return the emitter reference to the client until the end, the client does not receive anything.

    Correct steps: you have to create an SseEmitter, store it in memory, let the controller method return it, and then begin emitting items.

    Quick and probably incomplete example below:

    @Controller
    public SseController {
        private final List<SseEmitter> emitters = new ArrayList<>();
    
        @GetMapping("/listen")
        public SseEmitter getEvents() {
            SseEmitter emitter = new SseEmitter();
            emitters.add(emitter);
            emitter.onCompletion(() -> emitters.remove(emitter));
            return emitter;
        }
    
        @PostMapping("/notify")
        public void postMessage(String message) {
            for (SseEmitter emitter : emitters) {
                emitter.send(message);
            }
        }
    }
    

    In this example, we do a GET /listen to subscribe to an event stream, and then we can publish messages with POST /notify to push messages to all the listening clients, but you could send messages from other sources, such as a looping thread like in your example.

    The key concept is the order: create emitter, store emitter, return emitter, then send to emitter.