Reputation: 193
I'm using JHipster 3.5.0 with spring-boot and angular to build an application. I would like to send updates from the backend to the UI using server sent events, but I can't get it to work.
Here is the code of my RestController:
@RestController
@RequestMapping("/api")
public class SSEResource {
private final List<SseEmitter> sseEmitters = new CopyOnWriteArrayList<>();
@RequestMapping(value = "/sse", method = RequestMethod.GET)
@Timed
public SseEmitter getSSE() throws IOException {
SseEmitter sseEmitter = new SseEmitter();
this.sseEmitters.add(sseEmitter);
sseEmitter.send("Connected");
return sseEmitter;
}
@Scheduled(fixedDelay = 3000L)
public void update() {
this.sseEmitters.forEach(emitter -> {
try {
emitter.send(String.valueOf(System.currentTimeMillis()));
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
My Angaluar controller looks like this:
(function () {
'use strict';
angular
.module('myApp')
.controller('SSEController', SSEController);
SSEController.$inject = [];
function SSEController() {
var vm = this;
vm.msg = {};
function handleCallback(msg) {
vm.msg = msg.data;
}
vm.source = new EventSource('api/sse');
vm.source.addEventListener('message', handleCallback, false);
}
})
();
When I try to use that code I receive a
406 Not Acceptable HTTP status
because of the request header Accept:"text/event-stream". If I manually change that Header to Accept:"/*" and replay that request using the debug tools of my browser I get
401 Unauthorized HTTP status
I think I'm missing something quite simple, but I allready checked my SecurityConfiguration and authInterceptor without understanding what is missing.
Can anybody explain what I'm doing wrong?
Upvotes: 1
Views: 4134
Reputation: 504
Building upon @Jan answer, here is how I modified the JWTFilter.resolveToken method to look like this:
File: java/myapp/security/jwt/JWTFilter.java
private String resolveToken(HttpServletRequest request){
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
// pick token as GET parameter
bearerToken = request.getParameter("access_token");
return bearerToken;
}
In the front-end this is the relevant code I used:
this.eventSource = new EventSource('api/screens/sse?access_token=' + this.authServer.getToken());
this.eventSource.addEventListener('message', evt => {
const messageEvent = evt as MessageEvent;
this._alarmCount = parseInt(messageEvent.data, 10);
});
this.eventSource.onerror = evt => {
console.log('EventSource error' + evt.data);
};
Finally, I had to modify the rest controller to cleanup completed emitter:
public SseEmitter getSSE() throws IOException {
SseEmitter sseEmitter = new SseEmitter();
synchronized (this) {
this.sseEmitters.add(sseEmitter);
}
sseEmitter.onCompletion(() -> {
synchronized (this) {
// clean up completed emitters
this.sseEmitters.remove(sseEmitter);
}
});
sseEmitter.send(String.valueOf(System.currentTimeMillis()));
return sseEmitter;
}
Upvotes: 0
Reputation: 193
To anwser my own question: The solution is really really easy und really really unsatisfing:
I'm using Jhipster with JWT authentication, which relies on a HTTP-Header "Authorization". EventSource don't support headers! See
The solution could be using a polyfill with support for headers. I successfully tested it with this Commit of eventsource polyfill with support for headers
(function () {
'use strict';
angular
.module('myApp')
.controller('SSEController', SSEController);
SSEController.$inject = ['$scope', 'AuthServerProvider'];
function SSEController($scope, AuthServerProvider) {
var vm = this;
vm.msg = {};
var options = {
headers : {
Authorization : "Bearer " + AuthServerProvider.getToken()
}
}
vm.source = new EventSource('api/sse', options);
vm.source.addEventListener('message', handleCallback, false);
}
})
();
For some reason the header support is no longer included in the master branch nor in the original polyfill. So I'm not entirly sure thats the right way to go. I will probably switch to websockets.
EDIT: I think I found a way to use standard EventSource. The class JWTFilter contains a way to retrieve the access token from a request parameter. So I can just use the EventSource like this:
source = new EventSource('api/sse?access_token=' + AuthServerProvider.getToken());
So easy that I'm kind of embarrassed that I didn't see that before.
Upvotes: 6