caddyserver / caddy

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
https://caddyserver.com
Apache License 2.0
55.45k stars 3.91k forks source link

handle_errors 200 empty body #6422

Open odama626 opened 3 days ago

odama626 commented 3 days ago

I think I found kind of an odd bug. I am using caddy as a static file server for a SPA, so if I get an error, I just want it to try again at the root directory -- mostly for index.html and favicon

this works, returns index page but the status is still 404 on the successful document, odd but ok looking at the docs seems to be by design?

handle_errors {
  rewrite * /
  file_server
}

This one however -- it works on the initial page load, but if you refresh the page it returns an empty body for the page request and the index.html file for the favicon

handle_errors {
  rewrite * /
  file_server {
    status 200
  }
}

initial page load:

image image

page refresh:

image

when doing a hard refresh you again get the correct response

I am assuming this is a bug with some kind of caching key mechanism in handle_errors?

mholt commented 2 days ago

Thanks for opening an issue! We'll look into this.

It's not immediately clear to me what is going on, so I'll need your help to understand it better.

Ideally, we need to be able to reproduce the bug in the most minimal way possible using the latest version of Caddy. This allows us to write regression tests to verify the fix is working. If we can't reproduce it, then you'll have to test our changes for us until it's fixed -- and then we can't add test cases, either.

In this case, we especially need to reproduce this with curl -v commands (and not web browsers), and a minimal directory structure to understand the problem fully.

I've attached a template below that will help make this easier and faster! This will require some effort on your part -- please understand that we will be dedicating time to fix the bug you are reporting if you can just help us understand it and reproduce it easily.

This template will ask for some information you've already provided; that's OK, just fill it out the best you can. :+1: I've also included some helpful tips below the template. Feel free to let me know if you have any questions!

Thank you again for your report, we look forward to resolving it!

Template

## 1. Environment

### 1a. Operating system and version

```
paste here
```

### 1b. Caddy version (run `caddy version` or paste commit SHA)

This should be the latest version of Caddy:

```
paste here
```

## 2. Description

### 2a. What happens (briefly explain what is wrong)

### 2b. Why it's a bug (if it's not obvious)

### 2c. Log output

```
paste terminal output or logs here
```

### 2d. Workaround(s)

### 2e. Relevant links

## 3. Tutorial (minimal steps to reproduce the bug)

Instructions -- please heed otherwise we cannot help you (help us help you!)

  1. Environment: Please fill out your OS and Caddy versions, even if you don't think they are relevant. (They are always relevant.) If you built Caddy from source, provide the commit SHA and specify your exact Go version.

  2. Description: Describe at a high level what the bug is. What happens? Why is it a bug? Not all bugs are obvious, so convince readers that it's actually a bug.

    • 2c) Log output: Paste terminal output and/or complete logs in a code block. DO NOT REDACT INFORMATION except for credentials. Please enable debug and access logs.
    • 2d) Workaround: What are you doing to work around the problem in the meantime? This can help others who encounter the same problem, until we implement a fix.
    • 2e) Relevant links: Please link to any related issues, pull requests, docs, and/or discussion. This can add crucial context to your report.
  3. Tutorial: What are the minimum required specific steps someone needs to take in order to experience the same bug? Your goal here is to make sure that anyone else can have the same experience with the bug as you do. You are writing a tutorial, so make sure to carry it out yourself before posting it. Please:

    • Start with an empty config. Add only the lines/parameters that are absolutely required to reproduce the bug.
    • Do not run Caddy inside containers.
    • Run Caddy manually in your terminal; do not use systemd or other init systems.
    • If making HTTP requests, avoid web browsers. Use a simpler HTTP client instead, like curl.
    • Do not redact any information from your config (except credentials). Domain names are public knowledge and often necessary for quick resolution of an issue!
    • Note that ignoring this advice may result in delays, or even in your issue being closed. 😞 Only actionable issues are kept open, and if there is not enough information or clarity to reproduce the bug, then the report is not actionable.

Example of a tutorial:

Create a config file: ``` { ... } ``` Open terminal and run Caddy: ``` $ caddy ... ``` Make an HTTP request: ``` $ curl ... ``` Notice that the result is ___ but it should be ___.
odama626 commented 2 days ago

I wasn't able to reproduce it with a curl command. I think it has something to do with some kind of caching and I'm not sure how to achieve that in curl.

I was able to create a minimal setup that uses the browser. I created a basic project with a docker compose script below and you can reproduce it by navigating to http://localhost/test/me and refreshing the page with the keyboard shortcut

I tested in in chrome and firefox along with in chrome and firefox in incognito mode

caddy-reproduction.zip

francislavoie commented 2 days ago

You should use try_files for this, not handle_errors. Wrong tool for this job. See the docs: https://caddyserver.com/docs/caddyfile/patterns#single-page-apps-spas

odama626 commented 2 days ago

I'll have to give it a try tomorrow. I do think this is a bug though since it works as expected during navigation or a hard refresh but returns an empty body during a soft refresh

francislavoie commented 1 day ago

If you could add the debug global option and the log directive in your site block (for access logs), then show us your logs, that would help us understand.

mholt commented 1 day ago

That sounds like a bug in the SPA :thinking: especially if it can't be reproduced with curl. Having to use the browser to reproduce a bug is almost always indicative of a bug in the web app or even the browser (we've seen both).

odama626 commented 1 day ago

@mholt It's not the spa, I reproduced it above with a bare html file. also, if you remove the status 200 from the caddyfile it consistently returns the correct body.

<!DOCTYPE html>
<html>
  <head>
    <link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
  </head>
  <body>
    hi
  </body>
</html>

@francislavoie

Attaching to caddy-1
caddy-1  | {"level":"info","ts":1719681921.4997637,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
caddy-1  | {"level":"warn","ts":1719681921.503575,"msg":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies","adapter":"caddyfile","file":"/etc/caddy/Caddyfile","line":18}
caddy-1  | {"level":"info","ts":1719681921.5046237,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
caddy-1  | {"level":"warn","ts":1719681921.5048866,"logger":"http.auto_https","msg":"automatic HTTPS is completely disabled for server","server_name":"srv0"}
caddy-1  | {"level":"debug","ts":1719681921.5049403,"logger":"http.auto_https","msg":"adjusted config","tls":{"automation":{"policies":[{}]}},"http":{"servers":{"srv0":{"listen":[":80"],"routes":[{"handle":[{"handler":"vars","root":"/files"},{"handler":"file_server","hide":["/etc/caddy/Caddyfile"]}]}],"errors":{"routes":[{"group":"group0","handle":[{"handler":"rewrite","uri":"/"}]},{"handle":[{"handler":"file_server","hide":["/etc/caddy/Caddyfile"],"status_code":200}]}]},"automatic_https":{"disable":true},"logs":{}}}}}
caddy-1  | {"level":"debug","ts":1719681921.5053997,"logger":"http","msg":"starting server loop","address":"[::]:80","tls":false,"http3":false}
caddy-1  | {"level":"info","ts":1719681921.5056481,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
caddy-1  | {"level":"info","ts":1719681921.5056968,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc0001d6880"}
caddy-1  | {"level":"info","ts":1719681921.5059073,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
caddy-1  | {"level":"info","ts":1719681921.5059547,"msg":"serving initial configuration"}
caddy-1  | {"level":"warn","ts":1719681921.5076447,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/data/caddy","instance":"06b15745-6be8-4705-b5dd-6909966c476d","try_again":1719768321.5076432,"try_again_in":86399.999999605}
caddy-1  | {"level":"info","ts":1719681921.507745,"logger":"tls","msg":"finished cleaning storage units"}
caddy-1  | {"level":"debug","ts":1719681935.3437395,"logger":"http.handlers.file_server","msg":"sanitized path join","site_root":"/files","request_path":"/asdf/fds","result":"/files/asdf/fds"}
caddy-1  | {"level":"debug","ts":1719681935.3440337,"logger":"http.handlers.rewrite","msg":"rewrote request","request":{"remote_ip":"192.168.65.1","remote_port":"33178","client_ip":"192.168.65.1","proto":"HTTP/1.1","method":"GET","host":"localhost","uri":"/asdf/fds","headers":{"Sec-Ch-Ua":["\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\""],"Sec-Fetch-Mode":["navigate"],"If-None-Match":["\"sftwhh42\""],"Connection":["keep-alive"],"Sec-Ch-Ua-Platform":["\"macOS\""],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Fetch-Site":["none"],"Cache-Control":["max-age=0"],"Sec-Ch-Ua-Mobile":["?0"],"Dnt":["1"],"Accept-Language":["en-US,en;q=0.9"],"Upgrade-Insecure-Requests":["1"],"Sec-Fetch-User":["?1"],"Sec-Fetch-Dest":["document"],"Accept-Encoding":["gzip, deflate, br, zstd"]}},"method":"GET","uri":"/"}
caddy-1  | {"level":"debug","ts":1719681935.3440845,"logger":"http.handlers.file_server","msg":"sanitized path join","site_root":"/files","request_path":"/","result":"/files"}
caddy-1  | {"level":"debug","ts":1719681935.345007,"logger":"http.handlers.file_server","msg":"located index file","filename":"/files/index.html"}
caddy-1  | {"level":"debug","ts":1719681935.3450918,"logger":"http.handlers.file_server","msg":"opening file","filename":"/files/index.html"}
caddy-1  | {"level":"debug","ts":1719681935.3506725,"logger":"http.log.error","msg":"{id=b8m4t24ad} fileserver.(*FileServer).notFound (staticfiles.go:629): HTTP 404","request":{"remote_ip":"192.168.65.1","remote_port":"33178","client_ip":"192.168.65.1","proto":"HTTP/1.1","method":"GET","host":"localhost","uri":"/asdf/fds","headers":{"Connection":["keep-alive"],"Sec-Ch-Ua-Platform":["\"macOS\""],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Cache-Control":["max-age=0"],"Sec-Ch-Ua-Mobile":["?0"],"Dnt":["1"],"Sec-Fetch-Site":["none"],"Upgrade-Insecure-Requests":["1"],"Sec-Fetch-User":["?1"],"Sec-Fetch-Dest":["document"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept-Language":["en-US,en;q=0.9"],"Sec-Ch-Ua":["\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\""],"Sec-Fetch-Mode":["navigate"],"If-None-Match":["\"sftwhh42\""]}},"duration":0.000301506,"status":404,"err_id":"b8m4t24ad","err_trace":"fileserver.(*FileServer).notFound (staticfiles.go:629)"}
caddy-1  | {"level":"info","ts":1719681935.3507602,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"192.168.65.1","remote_port":"33178","client_ip":"192.168.65.1","proto":"HTTP/1.1","method":"GET","host":"localhost","uri":"/asdf/fds","headers":{"Sec-Ch-Ua-Mobile":["?0"],"Dnt":["1"],"Sec-Fetch-Site":["none"],"Cache-Control":["max-age=0"],"Sec-Fetch-Dest":["document"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept-Language":["en-US,en;q=0.9"],"Upgrade-Insecure-Requests":["1"],"Sec-Fetch-User":["?1"],"If-None-Match":["\"sftwhh42\""],"Sec-Ch-Ua":["\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\""],"Sec-Fetch-Mode":["navigate"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Connection":["keep-alive"],"Sec-Ch-Ua-Platform":["\"macOS\""]}},"bytes_read":0,"user_id":"","duration":0.000301506,"size":0,"status":200,"resp_headers":{"Server":["Caddy"],"Etag":["\"sftwhh42\""]}}
odama626 commented 1 day ago

@mholt I just remembered that you can copy a network request as a curl command in chrome, and I was able to reproduce it with curl. you may have to run the curl command twice because the first request works, it's just subsequent ones

curl -v 'http://localhost/asdf/fds' \ 
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'Accept-Language: en-US,en;q=0.9' \
  -H 'Cache-Control: max-age=0' \
  -H 'Connection: keep-alive' \
  -H 'DNT: 1' \
  -H 'If-None-Match: "sftwhh42"' \
  -H 'Sec-Fetch-Dest: document' \
  -H 'Sec-Fetch-Mode: navigate' \
  -H 'Sec-Fetch-Site: none' \
  -H 'Sec-Fetch-User: ?1' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"'
mohammed90 commented 1 day ago

-H 'If-None-Match: "sftwhh42"'

My gut tells me it has to do with the If-None-Match. I'm trying to remember the nuance of sending the header to Caddy file_server.

mohammed90 commented 1 day ago

Alright, I was right. It has to do with the etag and If-None-Match header.

When using file_server, Caddy calculates the etag and sends it across. Web browsers store the etag and send it on repeat requests for the same resource as the value for If-None-Match header. Caddy receives this request with the If-None-Match header, so Caddy finds the subject resource and calculates its etag hash. Caddy finds the hash matches the etag, and the browser did send the If-None-Match header, indicating it knows how to manage caches, so Caddy responds with HTTP 304 without body, conforming to the respective HTTP specs. BUT you told Caddy "don't give me 304, return 200 instead", so Caddy overrides the status code but the body is is still empty.

Ultimately, this is not a bug in Caddy. It's a user configuration error. The proper configuration is to follow @francislavoie suggestion and the doc page he linked to.

odama626 commented 1 day ago

if I remove the status 200 it consistently returns a 404, but I'm guessing that the browser don't pass cache headers on 404s? This was really confusing and I spent a lot of time trying to debug what was going on with it. maybe it would be a good idea to log a warning in debug mode when a status is changed from a 304 to something else?

odama626 commented 1 day ago

so I switched to try_files, and it is returning index.html for the favicon. I looked through the docs and wasn't able to find where {path} comes from or if there are other similar ones, because I think what I want would be closer to try_files {path} {filename}

:80 {
    root * /files
    log

    try_files {path} /index.html
    file_server

}
francislavoie commented 1 day ago

so I switched to try_files, and it is returning index.html for the favicon.

Yeah, so you need to add a favicon.ico file to your webroot, or set up your index.html to have the necessary meta tags for the favicon so the browser doesn't try to request the default one.

I looked through the docs and wasn't able to find where {path} comes from

It's a placeholder: https://caddyserver.com/docs/caddyfile/concepts#placeholders, it's the current request path.

because I think what I want would be closer to try_files {path} {filename}

No, that's wrong. That wouldn't do anything useful.

odama626 commented 1 day ago

Yeah, so you need to add a favicon.ico file to your webroot, or set up your index.html to have the necessary meta tags for the favicon so the browser doesn't try to request the default one.

I mentioned this, because I'd like caddy to return a 404 since there is not a favicon.ico

No, that's wrong. That wouldn't do anything useful.

try_files {path} /index.html will return a 200 with index.html for an image that should be a 404,

I was trying to achieve:

/random/path -> random/path/index.png or /index.html or 404 /random/path/image.png -> /random/path/image.png or /image.png or 404 /random/path/index.html -> /random/path/index.html or /index.hml or 404

I mentioned it above but it may have gotten missed: I think it would be a good idea to log a warning in debug mode when a status is changed from a 304 to something since that is probably not what someone wants to do normally

mohammed90 commented 1 day ago

I think it would be a good idea to log a warning in debug mode when a status is changed from a 304 to something since that is probably not what someone wants to do normally

It was changed from 304 to 200 because you explicitly configured Caddy to do that. It'd be very strange to WARN for something the user explicitly configure here:

file_server { status 200 }

For your use case, I'm suspecting we're dealing with the XY Problem because in your original post you mentioned favicons. For favicon, the browsers actually request /favicon.png (on non-Windows) and favicon.ico on Windows. It doesn't request the favicon by requesting /. The browser requests example.com/favicon.png (unless overridden by the link HTML tag) when visiting any page under the domain.

Regarding try_files, the 404 fallback pattern is described in the docs on the try_files documentation page.

odama626 commented 6 hours ago

It was changed from 304 to 200 because you explicitly configured Caddy to do that. It'd be very strange to WARN for something the user explicitly configure here:

I think warning the user against doing something that could be an easy mistake and is obviously not something you would want to do (would anyone ever rationally want to change a 304 to a 200?) makes for a good dev experience. like the go compiler does or other very user friendly applications do, and would prevent people from opening tickets like this one in the future.

I agree that it was an xy problem, because I was trying a hack and then I found some odd and confusing behavior that looked to me like it was a bug, and I do think the warning would be a nice feature since users are fallible -- I made a stupid configuration and realized it was cache related but didn't expect it was because the status was just being changed from a 304

as for the try_files, it doesn't seem like you can use /index.html and =404 together, and I'm starting to think that maybe caddy doesn't supply the specific type of functionality I was asking for. I thought the handle_errors rewrite hack I was using before did but after looking at it again, it just returns the index page for all 404s too

mohammed90 commented 4 hours ago

as for the try_files, it doesn't seem like you can use /index.html and =404 together

Now let's take a step back. What is the problem you're trying to resolve? What is the directory structure? And do you have index.html in all directories? If you've made any attempts, share the Caddyfile along with the access and debug logs, i.e. with the debug global option added and with the log directive added inside the site block.

odama626 commented 3 hours ago

what I am trying to achieve:

navigating to a url without an extension navigating to a url with a file extension
try {path} then /index.html try {path} then {file} then =404

example dir structure

files
- index.html
- cat.png
example requests url caddy result
localhost return root index.html
localhost/abc/def return root index.html
localhost/abc/def/xyz/cat.png return /cat.png
localhost/another_random_file_with_extension.xyz 404
localhost/path/without/extension return root index.html