Skip to content

Admin API /load returns 200 OK for invalid Caddyfile when it's not formatted with 'caddy fmt' #7246

@psviderski

Description

@psviderski

How to reproduce

Here is the Caddyfile that can be adapted to JSON correctly but is invalid when trying to load into Caddy because cert.pem and key.pem don't exist:

example.com {
# Note a two-space indentation, not a tab
  tls cert.pem key.pem
}

Let's load it through the Caddy Admin API /load via the unix socket:

curl -v --unix-socket /run/uncloud/caddy/admin.sock -H "Content-Type: text/caddyfile" --data-binary @Caddyfile http://localhost/load
*   Trying /run/uncloud/caddy/admin.sock:0...
* Connected to localhost (/run/caddy/admin.sock) port 80 (#0)
> POST /load HTTP/1.1
> Host: localhost
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: text/caddyfile
> Content-Length: 39
> 
< HTTP/1.1 200 OK
< Date: Tue, 09 Sep 2025 07:39:22 GMT
< Content-Length: 336
< Content-Type: text/plain; charset=utf-8
< 
[{"file":"Caddyfile","line":2,"message":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies"}]{"error":"loading config: loading new config: loading http app module: provision http: getting tls app: loading tls app module: provision tls: loading certificates: open cert.pem: no such file or directory"}
* Connection #0 to host localhost left intact

It returns 200 OK while 400 Bad Request would be more appropriate in this case.

Let's replace two-space indentation with a tab so that the Caddyfile becomes a properly formatted config and try to load it again:

curl -v --unix-socket /run/uncloud/caddy/admin.sock -H "Content-Type: text/caddyfile" --data-binary @Caddyfile http://localhost/load
*   Trying /run/uncloud/caddy/admin.sock:0...
* Connected to localhost (/run/caddy/admin.sock) port 80 (#0)
> POST /load HTTP/1.1
> Host: localhost
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: text/caddyfile
> Content-Length: 38
> 
< HTTP/1.1 400 Bad Request
< Content-Type: application/json
< Date: Tue, 09 Sep 2025 07:43:33 GMT
< Content-Length: 208
< Connection: close
< 
{"error":"loading config: loading new config: loading http app module: provision http: getting tls app: loading tls app module: provision tls: loading certificates: open cert.pem: no such file or directory"}
* Closing connection 0

There is no warning about the formatting in the body and the status code is correct 400 Bad Request.

The problem

The reason why this happens is that when the /load handler adapts the Caddyfile and the adapter returns non-empty warnings, they're written to the response body right away: https://github.com/caddyserver/caddy/blob/master/caddyconfig/load.go#L104-L110. This happens before a proper status code is written to the response.

According to the docs for the ResponseWriter.Write method:

// If [ResponseWriter.WriteHeader] has not yet been called, Write calls
// WriteHeader(http.StatusOK) before writing the data.

This is what happens here I believe. The caddy.APIError returned later in the handler https://github.com/caddyserver/caddy/blob/master/caddyconfig/load.go#L118-L121 can't change the response status code because it's too late.

Expected behaviour

The response should return 400 Bad Request status code and the body should contain a valid JSON instead of two concatenated JSONs (warnings + error):

[{"file":"Caddyfile","line":2,"message":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies"}]{"error":"loading config: loading new config: loading http app module: provision http: getting tls app: loading tls app module: provision tls: loading certificates: open cert.pem: no such file or directory"}

I'm not sure if there is a canonical type for replies that include both the result/error and warnings but I believe a user should be able to correctly parse the body as json and extract warnings and errors from it if there are any.

Assistance Disclosure

AI not used

If AI was used, describe the extent to which it was used.

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions