caddyserver/caddy · info · HandlerError

%s

Error message

%s

What it means

Not a defect: the static 'error' handler returns the configured message and status code to trigger Caddy's error chain. ServeHTTP resolves the StatusCode string and Error body through the replacer, then returns Error(statusCode, fmt.Errorf("%s", body)). The only genuine failure path is when a placeholder-driven status code resolves to a non-integer, which yields HTTP 500 wrapping strconv.Atoi's error.

Source

Thrown at modules/caddyhttp/staticerror.go:108

		default:
			return d.Errf("unrecognized subdirective '%s'", d.Val())
		}
	}
	return nil
}

func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)

	statusCode := http.StatusInternalServerError
	if codeStr := e.StatusCode.String(); codeStr != "" {
		intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, ""))
		if err != nil {
			return Error(http.StatusInternalServerError, err)
		}
		statusCode = intVal
	}
	return Error(statusCode, fmt.Errorf("%s", repl.ReplaceKnown(e.Error, "")))
}

// Interface guard
var (
	_ MiddlewareHandler     = (*StaticError)(nil)
	_ caddyfile.Unmarshaler = (*StaticError)(nil)
)

View on GitHub (pinned to 50e54ee279)

Solutions

  1. If 500 appears unexpectedly: make sure any placeholder in the status code always resolves to a 3-digit integer (or omit it)
  2. Remember the configured body is returned as an error by design — pair it with handle_errors to render a custom response
  3. For plain responses without error semantics, use 'respond' instead of 'error'

Example fix

# before
error {http.request.header.x-status} "custom"
# header missing -> Atoi("") fails -> 500

# after
error 403 "forbidden"
Defensive patterns

Strategy: validation

Validate before calling

// if the status uses placeholders, guarantee it resolves to an integer:
resolved := repl.ReplaceAll(statusCodeStr, "")
if n, err := strconv.Atoi(resolved); err != nil || n < 100 || n > 599 {
    statusCode = http.StatusInternalServerError // explicit fallback, not accidental
}

Try / catch

// in handle_errors, inspect the returned error to branch:
handle_errors {
    rewrite * /error.html
    file_server
}

Prevention

When it happens

Trigger: Using the 'error' Caddyfile directive or http.handlers.error module: e.g. 'error 403 "forbidden"'. Failing path: statusCode contains a placeholder like {http.error.status} that resolves to non-numeric text at request time.

Common situations: Intentionally aborting requests with a fixed status inside routes; placeholder-based status that is empty or textual; users surprised to see their configured string surface as a Go error in logs.

Related errors


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