Using following path: "/%", will break Caddy, why?

I just stumbled upon this a few minutes ago, it might have been discussed before and it might be stupid as I really didn’t investigate it in any way, but I thought I should add it here for discussion.

If you access any Caddy server with the % in the url, it breaks, for instance: https://caddyserver.com/%

Why is that?
It looks like a decoding issue, at most it should show either an internal server error page or, just like nginx does, a 404.

What do you mean by “break”? Please use specific language. Show evidence. Show your logs, your config, etc.

Hope this makes things a bit more clear:

What do you see in Caddy’s logs? Turn on the debug global option. What’s your config, as I asked?

Please provide full and detailed information. We can’t help otherwise.

You can see that I am doing this on the official caddy website, do you?
You don’t need logs to reproduce it, you can do it on any Caddy website, you will get the same result.

P.S: I don’t have access to a Caddy instance where I can enable debug, sorry for that.

I’m not able to reproduce that. Not in a browser, anyway.

curl -v "https://caddyserver.com/% rightly responds with 400, because the URI is illegal.

If there is a protocol error going on from your computer, check your network to make sure no intercepting proxies are breaking anything.

This is from a VPS from DigitalOcean:

[root@v2 tmp]# curl -v -I "https://caddyserver.com/%"
*   Trying 165.227.20.207...
* TCP_NODELAY set
* Connected to caddyserver.com (165.227.20.207) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, [no content] (0):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, [no content] (0):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, [no content] (0):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, [no content] (0):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, [no content] (0):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=caddyserver.com
*  start date: Nov  4 07:01:19 2022 GMT
*  expire date: Feb  2 07:01:18 2023 GMT
*  subjectAltName: host "caddyserver.com" matched cert's "caddyserver.com"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.3 (OUT), TLS app data, [no content] (0):
* TLSv1.3 (OUT), TLS app data, [no content] (0):
* TLSv1.3 (OUT), TLS app data, [no content] (0):
* Using Stream ID: 1 (easy handle 0x55a0f6c714a0)
* TLSv1.3 (OUT), TLS app data, [no content] (0):
> HEAD /% HTTP/2
> Host: caddyserver.com
> User-Agent: curl/7.61.1
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, [no content] (0):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS app data, [no content] (0):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
* TLSv1.3 (OUT), TLS app data, [no content] (0):
* TLSv1.3 (IN), TLS app data, [no content] (0):
* HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)
* stopped the pause stream!
* Connection #0 to host caddyserver.com left intact
curl: (92) HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)

And this one from my local development laptop:

cristi@MacBook-Pro ~ % curl -v -I "https://caddyserver.com/%"
*   Trying 165.227.20.207:443...
* Connected to caddyserver.com (165.227.20.207) port 443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES128-GCM-SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=caddyserver.com
*  start date: Nov  4 07:01:19 2022 GMT
*  expire date: Feb  2 07:01:18 2023 GMT
*  subjectAltName: host "caddyserver.com" matched cert's "caddyserver.com"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* h2h3 [:method: HEAD]
* h2h3 [:path: /%]
* h2h3 [:scheme: https]
* h2h3 [:authority: caddyserver.com]
* h2h3 [user-agent: curl/7.84.0]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x7fa4a680ec00)
> HEAD /% HTTP/2
> Host: caddyserver.com
> user-agent: curl/7.84.0
> accept: */*
> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
* HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)
* Connection #0 to host caddyserver.com left intact
curl: (92) HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)

Maybe it’s a bug in your curl version?

$ curl -v -I "https://caddyserver.com/%"
*   Trying 165.227.20.207:443...
* Connected to caddyserver.com (165.227.20.207) port 443 (#0)
* successfully set certificate verify locations:
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* ALPN: offers http/1.1
* ALPN: server accepted http/1.1
* SSL connection using TLSv1.3 / TLS13-AES128-GCM-SHA256
> HEAD /% HTTP/1.1
> Host: caddyserver.com
> User-Agent: curl/7.86.0-DEV
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
HTTP/1.1 400 Bad Request
< Content-Type: text/plain; charset=utf-8
Content-Type: text/plain; charset=utf-8
< Connection: close
Connection: close

< 
* Excess found: excess = 15 url = /% (zero-length body)
* Closing connection 0

$ curl --version
curl 7.86.0-DEV (x86_64-pc-linux-gnu) libcurl/7.86.0-DEV wolfSSL/5.5.0 zlib/1.2.11 ngtcp2/0.10.0-DEV nghttp3/0.8.0-DEV
Release-Date: [unreleased]
Protocols: dict file ftp ftps gopher gophers http https imap imaps mqtt pop3 pop3s rtsp smtp smtps telnet tftp 
Features: alt-svc AsynchDNS HSTS HTTP3 IPv6 Largefile libz SSL threadsafe UnixSockets

Works fine.

Although, mine is only seeing HTTP/1.1 as an option. I’m curious why you’re seeing HTTP/2.

I don’t know the inner workings of HTTP/2 but it’s quite possible the binary format of HTTP/2 causes parsing the request to fail when the URI is illegal. But if that’s the case, it’s not “breaking Caddy”, it’s breaking the HTTP protocol. It doesn’t look like Caddy’s code is even seeing the request… what you’re seeing is probably being properly handled by the Go standard library.

Just a FYI, latest version of curl:

cristi@MacBook-Pro ~ % docker run -it --rm curlimages/curl -v -I https://caddyserver.com/%  
*   Trying 165.227.20.207:443...
* Connected to caddyserver.com (165.227.20.207) port 443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: /cacert.pem
*  CApath: none
* 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
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=caddyserver.com
*  start date: Nov  4 07:01:19 2022 GMT
*  expire date: Feb  2 07:01:18 2023 GMT
*  subjectAltName: host "caddyserver.com" matched cert's "caddyserver.com"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* h2h3 [:method: HEAD]
* h2h3 [:path: /%]
* h2h3 [:scheme: https]
* h2h3 [:authority: caddyserver.com]
* h2h3 [user-agent: curl/7.86.0-DEV]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x7fef61146a90)
> HEAD /% HTTP/2
> Host: caddyserver.com
> user-agent: curl/7.86.0-DEV
> accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
* HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)
* Connection #0 to host caddyserver.com left intact
curl: (92) HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)
cristi@MacBook-Pro ~ % 

I don’t know what Caddy sees or not TBH, it just seems to me this is not the right behavior, this is what made me post it here. Since I don’t know the inner workings, I don’t know if this is something you can fix or not, I just thought it worth pointing it out.