caddyserver/caddy · error

decoding request body: %w, at offset %d

Error message

decoding request body: %w, at offset %d

What it means

Returned by unsyncedConfigAccess when the JSON request body cannot be unmarshaled and the failure is a *json.SyntaxError. The error includes the byte offset where JSON parsing failed, e.g. 'decoding request body: invalid character '\'' looking for beginning of value, at offset 2'. This is a malformed-JSON-body problem, not a schema problem.

Source

Thrown at admin.go:1170

// unsyncedConfigAccess traverses into the current config and performs
// the operation at path according to method, using body and out as
// needed. This is a low-level, unsynchronized function; most callers
// will want to use changeConfig or readConfig instead. This requires a
// read or write lock on currentCtxMu, depending on method (GET needs
// only a read lock; all others need a write lock).
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
	var err error
	var val any

	// if there is a request body, decode it into the
	// variable that will be set in the config according
	// to method and path
	if len(body) > 0 {
		err = json.Unmarshal(body, &val)
		if err != nil {
			if jsonErr, ok := err.(*json.SyntaxError); ok {
				return fmt.Errorf("decoding request body: %w, at offset %d", jsonErr, jsonErr.Offset)
			}
			return fmt.Errorf("decoding request body: %w", err)
		}
	}

	enc := json.NewEncoder(out)

	cleanPath := strings.Trim(path, "/")
	if cleanPath == "" {
		return fmt.Errorf("no traversable path")
	}

	parts := strings.Split(cleanPath, "/")
	if len(parts) == 0 {
		return fmt.Errorf("path missing")
	}

	// A path that ends with "..." implies:

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Validate the exact bytes you send with a JSON linter or jq: echo "$BODY" | jq .
  2. Use a real JSON serializer in your script/client instead of string concatenation
  3. Use a heredoc with quoted delimiter in curl so the shell does not mangle quotes: curl -X POST --data @- http://localhost:2019/config/apps/http/servers <<'EOF' ... EOF
  4. Check the reported offset against your payload to find the exact broken byte

Example fix

# before
curl -X POST http://localhost:2019/config/apps/http/servers/myserver/routes \
  -d "{ listen: [':8080'] ,}"   # invalid JSON
# after
curl -X POST http://localhost:2019/config/apps/http/servers/myserver/routes \
  -H 'Content-Type: application/json' \
  -d '{"listen":[":8080"]}'
Defensive patterns

Strategy: validation

Validate before calling

import json
def safe_config_call(body: str):
    json.loads(body)  # raises locally with a clear error before touching Caddy
    return body

Try / catch

Catch locally first: json.loads()/jq validation before send; on 400 from Caddy, parse the offset from 'at offset N' and point at byte N of your payload.

Prevention

When it happens

Trigger: POST/PUT/PATCH to /config/... with syntactically broken JSON: trailing commas, single quotes, unescaped newlines in strings, wrong content-type encoding, or a truncated body cut off by a proxy.

Common situations: Hand-written curl payloads with shell quoting mistakes; JSON produced by string concatenation instead of a serializer; request bodies truncated by a reverse proxy or client timeout; comments in JSON (JSON5 style) which Go's decoder rejects.

Related errors


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