caddyserver/caddy · error · APIError
malformed If-Match header; expect quoted string
Error message
malformed If-Match header; expect quoted string
What it means
When a config-change request carries an If-Match header, changeConfig requires the header value to be a quoted string (per ETag conventions). Values shorter than two characters or lacking surrounding double quotes are rejected as an APIError with HTTP 400 before any comparison happens. This is a client-side formatting bug, not a config mismatch.
Source
Thrown at caddy.go:176
func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error {
switch method {
case http.MethodGet,
http.MethodHead,
http.MethodOptions,
http.MethodConnect,
http.MethodTrace:
return fmt.Errorf("method not allowed")
}
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 errView on GitHub (pinned to 50e54ee279)
Solutions
- Wrap the ETag in double quotes: If-Match: "<path> <hash>" — copy the value verbatim from the ETag response header of a prior GET.
- Ensure the quoted content contains exactly two fields: the config path and the hash (see the follow-up format error).
- Drop the If-Match header entirely if optimistic concurrency is not needed.
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
func validIfMatch(h string) bool {
return len(h) >= 2 && h[0] == '"' && h[len(h)-1] == '"'
}
if ifMatch != "" && !validIfMatch(ifMatch) {
return errors.New("If-Match must be a quoted string")
} Prevention
- Always copy the ETag response header verbatim into If-Match.
- Never hand-build the header from a bare hash.
- Store ETags opaque — do not strip quotes when persisting them.
When it happens
Trigger: Sending If-Match: abc123 (unquoted), If-Match: " (single quote char), or an empty quoted string "" against POST/PUT/PATCH /load or /config/ endpoints; copying the raw hex hash from a GET response's ETag body instead of the quoted ETag header value.
Common situations: Automation scripts that build If-Match from the hash alone; clients that strip quotes when storing ETags; tutorials showing the unquoted hash; using W/ weak prefixes or * which this implementation does not accept.
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 format \"<path> <hash>\"
- 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/99b59f56f8f6555f.
Report an issue: GitHub.