Help with proxying a Server Sent Event

Hi -- I'm trying to proxy a Server Sent Event using traefik 3. The device I'm trying to proxy to is an SLZ06p7 SLZB-06p7 Zigbee Ethernet PoE USB LAN WIFI Adapter.
I've contacted the manufacturer of the device. Typically the web interface to the device is on port 80. They said SSE uses port 81 and their instructions were simply to open port 81 - no other details.

I'm under the understanding SSE is sent using a layer 7 (http) proxy rather than a layer 4 (tcp) proxy. Additionally from my understanding SSE's establish a communication stream from server to client. I've seen people referring to the headers such as:

header("X-Accel-Buffering: no");
header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");

I've also seen these nginx proxy parameters mentioned:
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;

Supposedly there can't be any buffering of the stream, no caching and the mime type is text/even-stream.

Soooo -- that's just all background information. Just "theoretical" knowledge which may or may not have any relevance to this particular implementation. How would I actually go about setting this up? I really don't know how to configure Traefik for SSE? I can set request and response headers however do I need to set response headers?

I know it's asking a lot but I've just never dealt with SSE events before and the resources I'm using are pretty limited and unfortunately the manufacture of device (via their support forums), isn't being very helpful. I'd be greatful if someone could give me a heads up.

AFAIK will SSE simply work over http(s). If the application requires a dedicated port, then publish a Docker port, create an additional entrypoint and router with service and it should work.

Here is what I have -- I'm not using docker but I get what your saying. Here is the dynamic config file:

---

http:
  routers:
    slzb06-host.domain.com:
      rule: "Host(`slzb06-host.domain.com`) || Host (`slzb-06-host.domain.com`) || Host (`slzb06P7-host.domain.com`) || Host (`slzb06p7-host.domain.com`) || Host (`zc-host.domain.com`) || Host (`zigbee-coordinator-host.domain.com`)"
      entryPoints:
        - web
        - websecure
      tls:
        options: modern@file
        certResolver: letsencrypt
        domains:
          - main: "slzb06-host.domain.com"
            sans:
              - "slzb06-host.domain.com"
              - "slzb-06-host.domain.com"
              - "slzb06P7-host.domain.com"
              - "slzb06p7-host.domain.com"
              - "zc-host.domain.com"
              - "zigbee-coordinator-host.domain.com"
      middlewares:
        - basic-auth
      service: sv_proxy_pass_slzb06-host.domain.com

    slzb06-host.domain.com-http-SSE:
      rule: "Host(`slzb06-host.domain.com`) || Host (`slzb-06-host.domain.com`) || Host (`slzb06P7-host.domain.com`) || Host (`slzb06p7-host.domain.com`) || Host (`zc-host.domain.com`) || Host (`zigbee-coordinator-host.domain.com`)"
      entryPoints:
        - SSE
      tls:
        options: modern@file
        certResolver: letsencrypt
        domains:
          - main: "slzb06-host.domain.com"
            sans:
              - "slzb06-host.domain.com"
              - "slzb-06-host.domain.com"
              - "slzb06P7-host.domain.com"
              - "slzb06p7-host.domain.com"
              - "zc-host.domain.com"
              - "zigbee-coordinator-host.domain.com"
      service: sv_proxy_pass_slzb06-host.domain.com-http-SSE


  services:
    sv_proxy_pass_slzb06-host.domain.com:
      loadBalancer:
        servers:
          - url: http://SLZB-06P7-host.domain.com
        passHostHeader: true

    sv_proxy_pass_slzb06-host.domain.com-http-SSE:
      loadBalancer:
        servers:
          - url: http://SLZB-06P7-host.domain.com:81
        passHostHeader: true

And here is the portion of my static config:

---
entryPoints:
  web:
    address: ':80'
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https

  websecure:
    address: ':443'

  SSE:
    address: ':81'

Does it work or you still got an issue?

Check Traefik debug log. You got a lot of sans names, they all need to exist.

Check browser developer tools network tab.

Ok - so I've confirmed a few things:

In terms of the sans names, they are all listed on the SSL certificate. I don't think that is the problem.

When contacting the developer for the SLZ06p7 device, the forum moderator said if you go directly to the device IP:81 you should see the SSEs. Which in this case he was correct. Only strange part is this SSE broadcast works only in Chrome but not FF. That's another issue for another day.

so in my case within Chrome, if I visit:
http://SLZB-06P7-host.domain.com:81
I will see the following:

However if I try to visit the same port 81 site through the reverse proxy:
https://zc-host.domain.com:81 which should in theory proxy to http://SLZB-06P7-host.domain.com:81 I get a Internal Server Error

Examining the logs, I get the following:

{"level":"debug","time":"2025-01-01T13:51:53-06:00","caller":"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/wrr/wrr.go:196","message":"Service selected by WRR: 2fa3f05fb18cabcc"}
{"level":"debug","error":"net/http: HTTP/1.x transport connection broken: unexpected EOF","time":"2025-01-01T13:51:53-06:00","caller":"github.com/traefik/traefik/v3/pkg/proxy/httputil/proxy.go:113","message":"500 Internal Server Error"}

So I'm kind of stuck. I've asked the developer of the project to send my any configuration for any reverse proxy that's been tested against for the SSEs, and I haven't heard back.

I quickly tested Traefik with sveltekit-sse-chat-example and SSE works fine.

Enable and check Traefik access log in JSON format, it might supply more information.

Aye -- finally got something here

Idk what's going on with FF, but lets stick to chrome.

So SSE is on port 81. If I try to visit the SSE page through the reverse proxy I'm getting this:
Note the address is https://zc-host.domain.com:81

So that looks pretty favorable.

Now if I actually try to visit the application (https://zc-host.domain.com) I'm still getting the warning about the SSE, but if you in the debug window it's complaining about blocked:mixed-content

Further investigation of this error shows this:

So the application is a requesting a http connection to SSE rather than an https connection which the reverse proxy is actually setup to serve?? So I'm guessing the error to this is because the original connection to the application was through an encrypted TLS connection but the SSE is trying to use http.

I found this on the chrome developer website: A new default Referrer-Policy for Chrome - strict-origin-when-cross-origin  |  Blog  |  Chrome for Developers.

  • Like no-referrer-when-downgrade, strict-origin-when-cross-origin is secure: no referrer (Referer header and document.referrer) is present when the request is made from an HTTPS origin (secure) to an HTTP one (insecure). This way, if your website uses HTTPS (if not, make it a priority), your website's URLs won't leak in non-HTTPS requests—because anyone on the network can see these, so this would expose your users to man-in-the-middle-attacks.
  • Within the same origin, the Referer header value is the full URL.

So I made some changes to reverse proxy since it seems like the SSE stream is specifically tagged to http. This was my experiment.

So here is my current test setup:
Static file (no longer is port 80 auto-redirected to 443):

---
entryPoints:
  web:
    address: ':80'

  websecure:
    address: ':443'

  SSE:
    address: ':81'

Dynamic file (made an additional http router listening on port 80. Port 80/81 use no TLS)

---
http:
  routers:
    slzb06-host.domain.com:
      rule: "Host(`slzb06-host.domain.com`) || Host (`slzb-06-host.domain.com`) || Host (`slzb06P7-host.domain.com`) || Host (`slzb06p7-host.domain.com`) || Host (`zc-host.domain.com`) || Host (`zigbee-coordinator-host.domain.com`)"
      entryPoints:
        - websecure
      tls:
        options: modern@file
        certResolver: letsencrypt
        domains:
          - main: "slzb06-host.domain.com"
            sans:
              - "slzb06-host.domain.com"
              - "slzb-06-host.domain.com"
              - "slzb06P7-host.domain.com"
              - "slzb06p7-host.domain.com"
              - "zc-host.domain.com"
              - "zigbee-coordinator-host.domain.com"
      middlewares:
        - basic-auth
      service: sv_proxy_pass_slzb06-host.domain.com

    slzb06-host.domain.com-http:
      rule: "Host(`slzb06-host.domain.com`) || Host (`slzb-06-host.domain.com`) || Host (`slzb06P7-host.domain.com`) || Host (`slzb06p7-host.domain.com`) || Host (`zc-host.domain.com`) || Host (`zigbee-coordinator-host.domain.com`)"
      entryPoints:
        - web
      middlewares:
        - basic-auth
      service: sv_proxy_pass_slzb06-host.domain.com


    slzb06-host.domain.com-http-SSE:
      rule: "Host(`slzb06-host.domain.com`) || Host (`slzb-06-host.domain.com`) || Host (`slzb06P7-host.domain.com`) || Host (`slzb06p7-host.domain.com`) || Host (`zc-host.domain.com`) || Host (`zigbee-coordinator-host.domain.com`)"
      entryPoints:
        - SSE
      service: sv_proxy_pass_slzb06-host.domain.com-http-SSE


  services:
    sv_proxy_pass_slzb06-host.domain.com:
      loadBalancer:
        servers:
          - url: http://SLZB-06P7-host.domain.com
        passHostHeader: true

    sv_proxy_pass_slzb06-host.domain.com-http-SSE:
      loadBalancer:
        servers:
          - url: http://SLZB-06P7-host.domain.com:81
        passHostHeader: true

Just for clarification -- these are the parameters I'm using
Reverse Proxy URL - zc-host.domain.com
SLZB06 URL - slzb06-host.domain.com

So visiting http://zc-host.domain.com -- I can reach slzb06-host.domain.com and I get no warning in regards to SSE not being correctly setup

Problems however arise when trying to reach device at using a TLS connection to the reverse proxy:
https://zc-domain.com (proxy address) with upstream address of http://slzb06-host.domain.com


The SSE is hardcoded as http.

It seems the app developer explicitly hard-coded "http" and port 81 into the source code. When you request the regular page via https, then the browser will not allow you to include content via http.

Tell the app developer that their app is :face_with_symbols_over_mouth: and they should use the same scheme (http or https) for SSE that is used for the main page.

My example SSE app uses regular http and SSE on the same port, that would make it even easier. And it works fine on Firefox.

Can I ask a question? To be fair their app main page only supports http. It doesn't support https. Is it possible from the apps point of view to know if https was used from reverse proxy? Is scheme information passed? Is it easier to use SSE on a different or same port as the main app?

**Update
Talked to developer and he rolled the app and SSE to the same port. He stated he originally had them on the same port, but do to problem with the device app becoming "frozen" over http or significantly slowed, he split the two to different ports. Anyway I flashed the latest fw with the two combined back on the same port and am testing things as it usually takes about 2-3 days for the issue to manifest per prior testing.

Yes, the app should be able to check the protocol.

If the app is server-side-rendered, it can check if a X-Forwarded-Proto header is set to http or https, and should be able to set the protocol accordingly for the SSE link.

If the app is mostly JS on the client side, it can check for the currently used scheme of the URL and adapt the link accordingly.

Coming from the nginx world, it's a little bit of an enigma how traefik forwards headers. I believe the app is server-side-rendered. Traefik I take it passes an X-Forwarded-Proto? I'm aware this is a very common header as I've seen it in various nginx configs many times -- just not quite as versed in the traefik ways of doing things.

Run traefik/whoami behind Traefik and you will see all the standard headers echoed back to you, they are automatically set.

Damn, very well aware of that container -- just hadn't thought about it for header debugging. Thanks.