Reverse-proxy websocket

1. The problem I’m having:

I am trying to use caddy as reverse-proxy for a FastAPI server. It works otherwise but fails to open websocket connection.

I have spend some days trying all possible hints from web and AI tools. I do not have any deep understanding on the topic.

2. Error messages and/or full log output:

brython.js:10369 WebSocket connection to 'ws://caddy.local/ws' failed: Establishing a tunnel via proxy server failed

curl -vL caddy.local/slideshow.html
* Uses proxy env variable no_proxy == 'localhost,127.0.0.1,127.0.0.3'
* Uses proxy env variable http_proxy == 'http://127.0.0.1:8000'
*   Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000
* using HTTP/1.x
> GET http://caddy.local/slideshow.html HTTP/1.1
> Host: caddy.local
> User-Agent: curl/8.12.1
> Accept: */*
> Proxy-Connection: Keep-Alive
> 
* Request completely sent off
< HTTP/1.1 200 OK
< date: Mon, 31 Mar 2025 17:17:08 GMT
< server: uvicorn
< content-length: 1519
< content-type: text/html; charset=utf-8
< 
<!DOCTYPE html>
<html lang="fi">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="/static/styles/album_client.css" />
    <script type="text/javascript" src="static/scripts/brython.js"></script>
    <script type="text/javascript" src="static/scripts/brython_stdlib.js"> </script>
    <title>slideshow</title>
</head>

<body onload="brython()"> <!-- {pythonpath: ['static/scripts/']} -->
<div id='sivu'>
    <div id='valikot'>
        <div id="valikko_0"></div>
        <div id="valikko_1"></div>
        <div id="valikko_2"></div>
        <div id="start"></div>
        <p style="text-align: center; margin: 2em; color: hsla(216, 100%, 10%, 1)">
            <a href="{{ url_for('home', path='/') }}">Home</a></p>
    </div>
    <div id='kehys'>
         <button id="seis" type="button">SEIS</button>
        <img id='photo' src="" alt="kuva"/>
        <!--
        <video id='video' controls preload="none">
        <video id='video' src='' type="video/mp4" controls preload="none" width="1280" height="720" style="float: right">
        </video>
        -->
        <div id='info'>
            <h1 id='otsikko'></h1>
            <div id='kuvaus'></div>
            <p id='ryhma'></p>
            <p id='paivays'></p>
            <p id='n_kuva'></p>
        </div>
        <!--        <div id="nappulat"></div> -->
    </div>
</div>

<script type="text/python" src="static/scripts/slideshow.py"></script>
</body>
</html>
* Connection #0 to host 127.0.0.1 left intact


3. Caddy version:

version v2.9.1

4. How I installed and ran Caddy:

installed with pip

a. System environment:

Operating System: openSUSE Tumbleweed 20250329
KDE Plasma Version: 6.3.3
KDE Frameworks Version: 6.12.0
Qt Version: 6.8.2
Kernel Version: 6.13.8-1-default (64-bit)
Graphics Platform: Wayland
Processors: 8 × 11th Gen Intel® Core™ i7-1165G7 @ 2.80GHz
Memory: 62.6 GiB of RAM
Graphics Processor: Intel® Iris® Xe Graphics
Manufacturer: Notebook
Product Name: NS50_70MU
System Version: 2.2A

b. Command:

 sudo caddy run --config Caddyfile

c. Service/unit/compose file:

d. My complete Caddy config:

{
debug
}
caddy.local  {
reverse_proxy localhost:8000
}

Your curl seems to be using a forward proxy (127.0.0.1:8000), which sits in front of your Caddy server.

Try this command:

curl --noproxy '*' -vL caddy.local/slideshow.html

to check the result when bypassing 127.0.0.1:8000.

Your other option is to update your system environment variables and add caddy.local to your proxy exception list:

no_proxy='localhost,127.0.0.1,127.0.0.3,caddy.local'

Thank you,

I got a lot further, but as a beginner, I still have some issue. curl-command opens a websocket connection with wss://caddy.local/ws but not with https://caddy.local/ws nor caddy local/ws. caddy.local does not request websocket and works fine.

Caddyfile trial


caddy.local  {
@websockets {
    header Connection *Upgrade*
    header Upgrade websocket
    }
reverse_proxy @websockets http://127.0.0.1:8000
reverse_proxy  http://127.0.0.1:8000
    log {
        output file caddy_log.json
        format json
        # level  <level>
        }
    }

server side

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    print('server waiting websocket')
    await websocket.accept()
    print('accepted')
    while True:
        ...

Three curl -vL tests

success: curl -vL caddy.local

* Host caddy.local:80 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.3
*   Trying 127.0.0.3:80...
* Connected to caddy.local (127.0.0.3) port 80
* using HTTP/1.x
> GET / HTTP/1.1
> Host: caddy.local
> User-Agent: curl/8.12.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://caddy.local/
< Server: Caddy
< Date: Tue, 01 Apr 2025 10:19:38 GMT
< Content-Length: 0
< 
* shutting down connection #0
* Clear auth, redirects to port from 80 to 443
* Issue another request to this URL: 'https://caddy.local/'
* Host caddy.local:443 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.3
*   Trying 127.0.0.3:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*  subject: 
*  start date: Apr  1 05:41:37 2025 GMT
*  expire date: Apr  1 17:41:37 2025 GMT
*  subjectAltName: host "caddy.local" matched cert's "caddy.local"
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify ok.
*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 2: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
* Connected to caddy.local (127.0.0.3) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://caddy.local/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: caddy.local]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.12.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: caddy.local
> User-Agent: curl/8.12.1
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Request completely sent off
< HTTP/2 200 
< alt-svc: h3=":443"; ma=2592000
< content-type: application/json
< date: Tue, 01 Apr 2025 10:19:38 GMT
< server: Caddy
< server: uvicorn
< content-length: 37
< 
* Connection #1 to host caddy.local left intact
{"message":"Slideshow in /slideshow"}```

FAIL curl -vL caddy.local/ws

* Host caddy.local:80 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.3
*   Trying 127.0.0.3:80...
* Connected to caddy.local (127.0.0.3) port 80
* using HTTP/1.x
> GET /ws HTTP/1.1
> Host: caddy.local
> User-Agent: curl/8.12.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://caddy.local/ws
< Server: Caddy
< Date: Tue, 01 Apr 2025 10:23:36 GMT
< Content-Length: 0
< 
* shutting down connection #0
* Clear auth, redirects to port from 80 to 443
* Issue another request to this URL: 'https://caddy.local/ws'
* Host caddy.local:443 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.3
*   Trying 127.0.0.3:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*  subject: 
*  start date: Apr  1 05:41:37 2025 GMT
*  expire date: Apr  1 17:41:37 2025 GMT
*  subjectAltName: host "caddy.local" matched cert's "caddy.local"
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify ok.
*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 2: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
* Connected to caddy.local (127.0.0.3) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://caddy.local/ws
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: caddy.local]
* [HTTP/2] [1] [:path: /ws]
* [HTTP/2] [1] [user-agent: curl/8.12.1]
* [HTTP/2] [1] [accept: */*]
> GET /ws HTTP/2
> Host: caddy.local
> User-Agent: curl/8.12.1
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Request completely sent off
< HTTP/2 404 
< alt-svc: h3=":443"; ma=2592000
< content-type: application/json
< date: Tue, 01 Apr 2025 10:23:36 GMT
< server: Caddy
< server: uvicorn
< content-length: 22
< 
* Connection #1 to host caddy.local left intact
{"detail":"Not Found"}

SUCCESS (?) curl -vL wss://caddy.local/ws

* Host caddy.local:443 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.3
*   Trying 127.0.0.3:443...
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / id-ecPublicKey
* Server certificate:
*  subject: 
*  start date: Apr  1 05:41:37 2025 GMT
*  expire date: Apr  1 17:41:37 2025 GMT
*  subjectAltName: host "caddy.local" matched cert's "caddy.local"
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify ok.
*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 2: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
* Connected to caddy.local (127.0.0.3) port 443
* using HTTP/1.x
> GET /ws HTTP/1.1
> Host: caddy.local
> User-Agent: curl/8.12.1
> Accept: */*
> Upgrade: websocket
> Connection: Upgrade
> Sec-WebSocket-Version: 13
> Sec-WebSocket-Key: MrU33WLxUqqVRKYeqBhiKQ==
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Request completely sent off
< HTTP/1.1 101 Switching Protocols
< Alt-Svc: h3=":443"; ma=2592000
< Connection: Upgrade
< Date: Tue, 01 Apr 2025 10:27:56 GMT
< Sec-WebSocket-Accept: AQjQteJFysFB0hERbbVO8ZYngGY=
< Server: Caddy
< Server: uvicorn
< Upgrade: websocket
< 
* Received 101, switch to WebSocket; mask 78194a98
* WS: auto-respond to PING with a PONG
* WS-ENC: sending [PONG payload=0/4]
* WS-ENC: buffered [PONG payload=4/4]
* WS: flushed 10 bytes

I do not know, where the error was, but now caddy works locally with the following settings

caddy.local  {

    handle /ws {
        reverse_proxy fastapi.local:8000 {
            }
        }
        handle {
            reverse_proxy  fastapi.local:8000
            }
        }

I will test on my production server one of these days.

Thank you

Both of them go to the same port and address. You don’t need the multiple handles. This is more than enough:

caddy.local {
	reverse_proxy fastapi.local:8000
}
1 Like

That is true. The other one was just a left-over from my haphazard experimentation.

Quite amazing how little configuration caddy needs.

I had problems to get websocket work and tried all kinds of tricks I found from the internet (there are awfully many different ). All of them useless.

I guess the problem was the proxy running in my computer which I failed to bypass. I set my browser to no-proxy and testing goes nicely.

I can also use linkchecker.