caddyserver/caddy · error · APIError
malformed If-Match header; expect format \"<path> <hash>\"
Error message
malformed If-Match header; expect format \"<path> <hash>\"
What it means
After unquoting, changeConfig splits the If-Match value on whitespace and requires exactly two fields: the config path and its expected hash, i.e. "<path> <hash>". Any other field count — one, three+, or only quotes — returns an APIError with HTTP 400. The hash is the hex SHA-256 of the config at that path (etagHasher).
Source
Thrown at caddy.go:185
rawCfgMu.Lock()
defer rawCfgMu.Unlock()
if ifMatchHeader != "" {
// expect the first and last character to be quotes
if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect quoted string"),
}
}
// read out the parts
parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
if len(parts) != 2 {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect format \"<path> <hash>\""),
}
}
// get the current hash of the config
// at the given path
hash := etagHasher()
err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash)
if err != nil {
return err
}
if hex.EncodeToString(hash.Sum(nil)) != parts[1] {
return APIError{
HTTPStatus: http.StatusPreconditionFailed,
Err: fmt.Errorf("If-Match header did not match current config hash"),
}
}
}View on GitHub (pinned to 50e54ee279)
Solutions
- Format the header exactly as If-Match: "<path> <hash>", e.g. If-Match: "/ 5f2c...a1" for the whole config or If-Match: "/apps/http/servers/srv0 5f2c...a1" for a subtree.
- Fetch the current ETag with a GET and use its value verbatim rather than hand-building it.
- Remember the path is the JSON config path, not a URL path minus /config — check how your client maps it.
Example fix
# before curl -X POST localhost:2019/load -H 'If-Match: "5f2c...a1"' --data @cfg.json # after curl -X POST localhost:2019/load -H 'If-Match: "/ 5f2c...a1"' --data @cfg.json
Defensive patterns
Strategy: validation
Validate before calling
parts := strings.Fields(strings.Trim(ifMatch, "\""))
if len(parts) != 2 {
return fmt.Errorf("If-Match must be \"<path> <hash>\", got %d fields", len(parts))
} Prevention
- Construct the header as quoted "<path> <hex hash>" with exactly one space.
- Avoid config paths containing spaces, or URL-choose different paths.
- Round-trip test your client's ETag handling against a live GET.
When it happens
Trigger: Sending If-Match: "5f2c..." (hash only, no path), If-Match: "/apps/http 5f2c... extra" (three fields), or a path containing an unescaped space that shifts the field count. Applies to POST/PUT/PATCH /load and /config/ requests with concurrency control.
Common situations: Clients that send a bare ETag hash assuming standard RFC semantics; splitting path and hash on the wrong delimiter when constructing the header; paths with spaces; copying examples that omit the leading / for the root path.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- malformed If-Match header; expect quoted string
- request path is missing object ID
- malformed object path
- unknown object ID '%s'
- method not allowed
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/986a46b7bf2ef9b0.
Report an issue: GitHub.