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 err

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Wrap the ETag in double quotes: If-Match: "<path> <hash>" — copy the value verbatim from the ETag response header of a prior GET.
  2. Ensure the quoted content contains exactly two fields: the config path and the hash (see the follow-up format error).
  3. 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

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

Related errors


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