{"record":{"id":"986a46b7bf2ef9b0","repo":"caddyserver/caddy","slug":"malformed-if-match-header-expect-format-path","errorCode":null,"errorMessage":"malformed If-Match header; expect format \\\"<path> <hash>\\\"","messagePattern":"malformed If-Match header; expect format \\\\\"<path> <hash>\\\\\"","errorType":"http","errorClass":"APIError","httpStatus":400,"severity":"error","filePath":"caddy.go","lineNumber":185,"sourceCode":"\n\trawCfgMu.Lock()\n\tdefer rawCfgMu.Unlock()\n\n\tif ifMatchHeader != \"\" {\n\t\t// expect the first and last character to be quotes\n\t\tif len(ifMatchHeader) < 2 || ifMatchHeader[0] != '\"' || ifMatchHeader[len(ifMatchHeader)-1] != '\"' {\n\t\t\treturn APIError{\n\t\t\t\tHTTPStatus: http.StatusBadRequest,\n\t\t\t\tErr:        fmt.Errorf(\"malformed If-Match header; expect quoted string\"),\n\t\t\t}\n\t\t}\n\n\t\t// read out the parts\n\t\tparts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])\n\t\tif len(parts) != 2 {\n\t\t\treturn APIError{\n\t\t\t\tHTTPStatus: http.StatusBadRequest,\n\t\t\t\tErr:        fmt.Errorf(\"malformed If-Match header; expect format \\\"<path> <hash>\\\"\"),\n\t\t\t}\n\t\t}\n\n\t\t// get the current hash of the config\n\t\t// at the given path\n\t\thash := etagHasher()\n\t\terr := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif hex.EncodeToString(hash.Sum(nil)) != parts[1] {\n\t\t\treturn APIError{\n\t\t\t\tHTTPStatus: http.StatusPreconditionFailed,\n\t\t\t\tErr:        fmt.Errorf(\"If-Match header did not match current config hash\"),\n\t\t\t}\n\t\t}\n\t}","sourceCodeStart":167,"sourceCodeEnd":203,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/caddy.go#L167-L203","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\ncurl -X POST localhost:2019/load -H 'If-Match: \"5f2c...a1\"' --data @cfg.json\n\n# after\ncurl -X POST localhost:2019/load -H 'If-Match: \"/ 5f2c...a1\"' --data @cfg.json","handlingStrategy":"validation","validationCode":"parts := strings.Fields(strings.Trim(ifMatch, \"\\\"\"))\nif len(parts) != 2 {\n    return fmt.Errorf(\"If-Match must be \\\"<path> <hash>\\\", got %d fields\", len(parts))\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["caddy","admin-api","http","if-match","etag","validation"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}