caddy: Reverse proxy is not working with Mercure module

I have a Caddy binary w/ mercure module. And I have a Caddy on the system for anything else from a PPA. (w/o any modules, just factory default Caddy) If I try to connect to the server w/ javascript, it will return w/ http 200 and close the connection because http header error instead of keep the connection as an event-stream. However the http request headers are OK.

Error message in browser:

EventSource's response has a MIME type ("text/plain") that is not "text/event-stream". Aborting the connection.

1.) Not working w/ Caddy

Mercure settings w/ Caddyfile

I started a caddy binary w/ a pre-built mercure module. $ ./caddy run -config Caddyfile

{
	admin 0.0.0.0:2020
	http_port 8080
}

mercure.test:8080

log {
	output file access.log
}

route {
	encode zstd gzip

	mercure {
		transport_url {$MERCURE_TRANSPORT_URL:bolt://mercure.db}
		publisher_jwt xxxxxxxx
		subscriber_jwt xxxxxxx
		cors_origins http://test.local https://test.local http://localhost http://127.0.0.1
		publish_origins *
		subscriptions
	}

	respond /healthz 200
	respond "Not Found" 404
}

The website’s (Symfony project) Caddy config

test.local {
    root * /var/www/test/public
    encode gzip zstd
    file_server

    php_fastcgi unix//run/php/php8.0-fpm.sock

  # fig 1 --> IT DOES NOT WORK
  reverse_proxy /.well-known/mercure {
    to mercure.test:8080
  }

  # fig 2 --> IT DOES NOT WORK
  reverse_proxy /.well-known/mercure mercure.test:8080

  # fig 3 --> IT DOES NOT WORK
  handle_path  /.well-known/mercure {
    rewrite *  /.well-known/mercure
    reverse_proxy mercure.test:8080
  }

  # fig x --> I tried w/ a lot of way what I forgot later...

    log {
        output file /var/log/caddy/test.access.log {
                roll_size 3MiB
                roll_keep 5
                roll_keep_for 48h
        }
        format console
    }
}

The js code it does not matter because w/ nginx it is working properly. But, for the connection I use EventSource:

Js code

      const eventSource = new EventSource('https://test.local/.well-known/mercure', {
        withCredentials: true
      });

Caddy log

2022/01/07 09:52:16.128	INFO	http.log.access	handled request	{"request": {"remote_addr": "127.0.0.1:37472", "proto": "HTTP/1.1", "method": "GET", "host": "test.local", "uri": "/.well-known/mercure?topic=xxxxx", "headers": {"User-Agent": ["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0"], "Referer": ["https://test.local/admin"], "X-Forwarded-For": ["127.0.0.1"], "Cookie": ["mercureAuthorization=xxxxx; XDEBUG_SESSION=PHPSTORM; i_like_gitea=db38ff893f25a3a2; PHPSESSID=envdj5o0rrs8cu04fu8aqffq5f"], "Sec-Fetch-Site": ["same-origin"], "Te": ["trailers"], "X-Forwarded-Proto": ["https"], "Accept-Encoding": ["gzip, deflate, br"], "Accept": ["text/event-stream"], "Accept-Language": ["en-US,en;q=0.5"], "Cache-Control": ["no-cache"], "Pragma": ["no-cache"], "Sec-Fetch-Dest": ["empty"], "Sec-Fetch-Mode": ["cors"]}}, "common_log": "127.0.0.1 - - [07/Jan/2022:10:52:16 +0100] \"GET /.well-known/mercure?topic=xxxxx HTTP/1.1\" 0 0", "user_id": "", "duration": 0.000002843, "size": 0, "status": 0, "resp_headers": {"Server": ["Caddy"]}}

2.) Working solution w/ nginx

Nginx config in a virtual server

    location /.well-known/mercure {
        proxy_pass http://mercure.test:8080;
    }

Caddy log

2022/01/07 09:54:13.324	INFO	http.handlers.mercure	New subscriber	{"subscriber": {"id": "urn:uuid:7278e1bd-8f0c-40ec-a25f-92ab1ea14b32", "last_event_id": "", "remote_addr": "127.0.0.1:37474", "topic_selectors": ["2db5d020-abbb-4638-a6c3-43276b3626fb"], "topics": ["2db5d020-abbb-4638-a6c3-43276b3626fb"]}}

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 31 (14 by maintainers)

Most upvoted comments

For anyone, who want to use the Mercure protocol easily; If you can do this, then do not use apache or nginx and so on, use just Caddy.

If you cannot use Caddy only, then you do not worth to read further. 😞

1.) Install the newest version of GoLang if you do not have on your system.

2.) Install xcaddy and build a custom Caddy binary.

$ xcaddy build --with github.com/dunglas/mercure --with github.com/dunglas/mercure/caddy

3.) Set up the service manually.

4.) Then, you can set up your domain in caddy include your mercure settings. Example setting

/etc/caddy/Caddyfile

...

import /etc/caddy/conf.d/*

/etc/caddy/conf.d/your-domain.caddy

<your-domain> {
        route {
                root * <your-project-dir>
                php_fastcgi unix//run/php-fpm/php-fpm.sock
                encode gzip zstd
                file_server
        }

        route /.well-known/mercure {
                mercure {
                        transport_url local://local
                        publisher_jwt <pub-key>
                        subscriber_jwt <sub-key>
                        cors_origins https://localhost https://127.0.0.1
                        subscriptions
                        heartbeat 25s
                }
        }
        ...
}

The only disadvantage (it is so annoying) of this setup, there is no general solution to upgrade your Caddy w/ the system. For example w/ a package manager. A hook might can help you, with the xcaddy command, if you use Arch. On Debian - as I know - there are hooks too in dpkg.

Alright well at this point I think I’ll close this issue because if you’ve found a workaround and we’ve not heard complaints from other people since, I have to assume it’s not a problem in general.

Okay, interesting. So comparing the two log messages:

# Through nginx:
2022/01/10 08:38:54.159	INFO	http.log.access.log0	handled request	{"request": {"remote_addr": "127.0.0.1:39900", "proto": "HTTP/1.0", "method": "GET", "host": "mercure.test:8080", "uri": "/.well-known/mercure?topic=2db5d020-abbb-4638-a6c3-43276b3626fb", "headers": {"Pragma": ["no-cache"], "Connection": ["close"], "User-Agent": ["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0"], "Accept-Language": ["en-US,en;q=0.5"], "Referer": ["https://test.local/admin"], "Sec-Fetch-Dest": ["empty"], "Sec-Fetch-Mode": ["cors"], "Sec-Fetch-Site": ["same-origin"], "Cache-Control": ["no-cache"], "Accept": ["text/event-stream"], "Accept-Encoding": ["gzip, deflate, br"], "Cookie": ["mercureAuthorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDE4MDc1MzEuNDc3NzYxLCJtZXJjdXJlIjp7InB1Ymxpc2giOltdLCJzdWJzY3JpYmUiOlsiMmRiNWQwMjAtYWJiYi00NjM4LWE2YzMtNDMyNzZiMzYyNmZiIl19fQ.tTxGu08MM4hIx0VQgwQ9-M-jt25ILPZdajNiHUl6CWk; XDEBUG_SESSION=PHPSTORM; i_like_gitea=db38ff893f25a3a2; PHPSESSID=6pkd059t5k616csp7i0r983su9"]}}, "common_log": "127.0.0.1 - - [10/Jan/2022:09:38:54 +0100] \"GET /.well-known/mercure?topic=2db5d020-abbb-4638-a6c3-43276b3626fb HTTP/1.0\" 200 2", "user_id": "", "duration": 2.597564378, "size": 2, "status": 200, "resp_headers": {"X-Accel-Buffering": ["no"], "Content-Security-Policy": ["default-src 'self' mercure.rocks cdn.jsdelivr.net"], "X-Frame-Options": ["DENY"], "X-Content-Type-Options": ["nosniff"], "Cache-Control": ["private, no-cache, no-store, must-revalidate, max-age=0"], "Pragma": ["no-cache"], "Expire": ["0"], "Server": ["Caddy"], "X-Xss-Protection": ["1; mode=block"], "Connection": ["keep-alive"], "Content-Type": ["text/event-stream"]}}

# Through Caddy:
2022/01/10 07:13:00.594	INFO	http.log.access	handled request	{"request": {"remote_addr": "127.0.0.1:39874", "proto": "HTTP/1.1", "method": "GET", "host": "test.local", "uri": "/.well-known/mercure?topic=2db5d020-abbb-4638-a6c3-43276b3626fb", "headers": {"User-Agent": ["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0"], "Accept-Encoding": ["gzip, deflate, br"], "Referer": ["https://test.local/admin"], "Sec-Fetch-Dest": ["empty"], "Accept": ["text/event-stream"], "Accept-Language": ["en-US,en;q=0.5"], "Cache-Control": ["no-cache"], "Sec-Fetch-Site": ["same-origin"], "X-Forwarded-Proto": ["https"], "Sec-Fetch-Mode": ["cors"], "Te": ["trailers"], "X-Forwarded-For": ["127.0.0.1"], "Cookie": ["mercureAuthorization=xxxxxxxxxx; XDEBUG_SESSION=PHPSTORM; i_like_gitea=db38ff893f25a3a2; PHPSESSID=6pkd059t5k616csp7i0r983su9"], "Pragma": ["no-cache"]}}, "common_log": "127.0.0.1 - - [10/Jan/2022:08:13:00 +0100] \"GET /.well-known/mercure?topic=2db5d020-abbb-4638-a6c3-43276b3626fb HTTP/1.1\" 0 0", "user_id": "", "duration": 0.000007299, "size": 0, "status": 0, "resp_headers": {"Server": ["Caddy"]}}

So looking at the request headers in each, these are the header differences:

  • Nginx sends:
    • "Connection": ["close"]
  • Caddy sends
    • "X-Forwarded-Proto": ["https"],
    • "X-Forwarded-For": ["127.0.0.1"],
    • "Te": ["trailers"],

I highly doubt the X-Forwarded headers are problematic here.

Caddy explicitly removes the Connection header when proxying because it’s a “hop-by-hop” header. I don’t think Caddy or the Go stdlib does anything with that header anyways.

The Te trailers header is just an “announcement” that the client/proxy supports trailers, in case the upstream cares to use the feature. I don’t think mercure does in this case.

So, I’m confused, basically. I think we’ll need help from @dunglas when he finds time, because I don’t know what to look for at this point.

If you refresh the browser or something, it would close the connection, right? We might see the access log written at that point.

Access logs only have INFO and ERROR, so lowering the level to DEBUG doesn’t do anything.

Like Matt wrote, you need to use the debug global option, which sets the level to DEBUG for all runtime logs (not just access logs).

FWIW Caddy does have specific handling for text/event-stream to turn off the flush interval:

https://github.com/caddyserver/caddy/blob/4b9849c7922c3a0a7b1bd487f5d890fcff32aaba/modules/caddyhttp/reverseproxy/streaming.go#L110

Can you replicate this with the debug global option turned on, for both Caddy instances? It would be helpful to see the reverse_proxy module’s logs for this.

Also to narrow it down, to make sure it’s not a more recent regression, could you also try with Caddy v2.4.0? We made some changes since that version which may or may not be related.