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

  1. 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.
  2. Fetch the current ETag with a GET and use its value verbatim rather than hand-building it.
  3. 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

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

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/986a46b7bf2ef9b0. Report an issue: GitHub.