I have a spring boot backend for which I want to implement an SSE Endpoint. This endpoint I want to consume using an App based on Xamarin Forms.
I managed to implement some examples for both sides, however, I did not manage to receive any message on the App.
For the backend part I implemented the following example:
@RequestMapping(value = "/event-stream", method = RequestMethod.GET)
public ResponseEntity<ResponseBodyEmitter> streamEvents() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
executor.execute(() -> {
try {
for (int i = 0; i < 30; i++) {
Thread.sleep(2000);
var msg = new ResponseTestObject("this is a message from " + new Date(), i);
emitter.send(msg, MediaType.APPLICATION_JSON);
}
emitter.complete();
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return new ResponseEntity<>(emitter, HttpStatus.OK);
}
@AllArgsConstructor
public static class ResponseTestObject {
public String message;
public int id;
}
NOTE: I intentionally implemented it in a way that the default 30s timeout would be reached. calling this method with postman it would load for the said 30 seconds and show all sent messages at the same time:
{
"message": "this is a message from Thu May 05 11:36:25 CEST 2022",
"id": 0
}{
"message": "this is a message from Thu May 05 11:36:27 CEST 2022",
"id": 1
}
[...]
{
"message": "this is a message from Thu May 05 11:36:51 CEST 2022",
"id": 13
}{
"message": "this is a message from Thu May 05 11:36:53 CEST 2022",
"id": 14
}
on my app part I used ServiceStack ServerEventsClient:
EventClient = new ServerEventsClient(BackendConnector.BACKEND_HOST + "/events/") {
OnMessage = OnMessage,
OnException = (ex) => {
Console.WriteLine("OnException: " + ex.Message);
}
};
// another REST backend connection is made previously in the app and I use its session cookie for authentication
EventClient.ServiceClient.SetCookie(BackendConnector.SESSION_COOKIE_VALUE, BackendConnector.BackendSessionCookieId);
EventClient.Start();
After I start the client I let it periodically post the status like that:
Console.WriteLine("SSE " + EventClient.Status);
What I can see is the following:
- The "subscription" to the backend works. I can see the request and it does start running the backend thread
- The status log shows that after a short while into "starting" the status changes to "started"
- The "started" status will remain this way until the timeout of 30s has been reached. then it reconnects and the circle continues
- The method OnMessage is never called, not even after timeout, like I could see it in postman
ADDITIONAL NOTE: I also had a server-side test implementation using SseEmitter. In this case I also could see the app request during "subscription" but it would timeout at some point with an exception and the client status would never leave "starting"
my first question obviously is: what I have missed that I will never receive a message?
my second question is: why does it timeout in the first place? According to the documentation it would send periodic heartbeats. Do I need a different approach on the spring side? Or do I need to implement a separate endpoint for the heartbeat?
thanks for your help!
EDIT:
following @mythz answer and accepting that using ServiceStack here is not a good idea, I followed the following example and implemented a simple way using the standard HttpClient and StreamReader: https://makolyte.com/event-driven-dotnet-how-to-consume-an-sse-endpoint-with-httpclient/
However, this resulted in the same problem I experienced with postman or my browser before. All messages are sent in a bulk after the 30s timeout. Therefore I also altered the backend part and use spring web flux instead:
public Flux<ServerSentEvent<String>> streamEvents() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> {
var msg = new ResponseTestObject("this is a message from " + new Date(), Math.toIntExact(sequence));
ObjectMapper mapper = new ObjectMapper();
try {
return ServerSentEvent.<String>builder()
.id(String.valueOf(sequence))
.event("periodic-event")
.data(mapper.writeValueAsString(msg))
.build();
} catch (JsonProcessingException e) {
return null;
}
});
}