简体   繁体   中英

Can't get server sent events and event source to work in JHipster

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?

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.

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;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM