caddyserver/caddy · error
%v: the default header modifier ('?') can only be used on re
Error message
%v: the default header modifier ('?') can only be used on response headers; for conditional manipulation of request headers, use matchers What it means
Returned while parsing the `header` Caddyfile directive: the '?' prefix marks a default value (set only if the header is absent) and is implemented via response matchers, which only exist for response headers. Using '?Field' in a request-header context (the `header` directive before a matcher separates request ops, or request header manipulation blocks) is a config error.
Source
Thrown at modules/caddyhttp/headers/caddyfile.go:241
}
func applyHeaderOp(ops *HeaderOps, respHeaderOps *RespHeaderOps, field, value string, replacement *string) error {
switch {
case strings.HasPrefix(field, "+"): // append
if ops.Add == nil {
ops.Add = make(http.Header)
}
ops.Add.Add(field[1:], value)
case strings.HasPrefix(field, "-"): // delete
ops.Delete = append(ops.Delete, field[1:])
if respHeaderOps != nil {
respHeaderOps.Deferred = true
}
case strings.HasPrefix(field, "?"): // default (conditional on not existing) - response headers only
if respHeaderOps == nil {
return fmt.Errorf("%v: the default header modifier ('?') can only be used on response headers; for conditional manipulation of request headers, use matchers", field)
}
if respHeaderOps.Require == nil {
respHeaderOps.Require = &caddyhttp.ResponseMatcher{
Headers: make(http.Header),
}
}
field = strings.TrimPrefix(field, "?")
respHeaderOps.Require.Headers[field] = nil
if respHeaderOps.Set == nil {
respHeaderOps.Set = make(http.Header)
}
respHeaderOps.Set.Set(field, value)
case replacement != nil: // replace
// allow defer shortcut for replace syntax
if strings.HasPrefix(field, ">") && respHeaderOps != nil {
respHeaderOps.Deferred = true
}View on GitHub (pinned to 50e54ee279)
Solutions
- Use the '?' modifier only where response headers are set, e.g. after a matcher or within `header <matcher> ?Field value` response context.
- For conditional request headers, use a matcher (e.g. `@missing not header X-Custom *`) plus `request_header` inside a matched route.
- Switch to explicit logic: set the request header unconditionally or use variable/placeholders to emulate defaults.
Example fix
# before (invalid: default modifier on request header) header ?X-Forwarded-Custom value # after (conditional via matcher + request_header) @no_custom not header X-Forwarded-Custom * request_header @no_custom X-Forwarded-Custom value
Defensive patterns
Strategy: validation
Validate before calling
# Lint: forbid '?' modifiers outside response-header contexts
# '?' fields are only valid in `header <matcher>` (response) usage; flag request_header blocks
grep -nE 'request_header[^{]*\?' Caddyfile && { echo "'?' default modifier used for request headers"; exit 1; } Prevention
- Remember '?' = default-value modifier, response headers only.
- For conditional request headers use matchers (not header_at_least_once semantics).
- Run `caddy adapt` after writing header rules to confirm they parse.
When it happens
Trigger: Writing a Caddyfile like `header ?X-Custom value` where the tokens apply to the request header phase — i.e. the '?' field appears before any response-header section split, so respHeaderOps is nil at headers/caddyfile.go:241.
Common situations: Users wanting 'set X only if not already present' on requests forwarded upstream — not supported for request headers; porting response-header snippets to request-header blocks; misunderstanding that `header` mutates both request and response depending on syntax.
Related errors
- consolidating TLS connection policies for server %d: %v
- applying global server options: %v
- server listening on %v is configured for HTTPS and cannot na
- %s (try specifying https:// in the address)
- server listening on %v is HTTP, but attempts to configure TL
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/b9619a5edd8de2c0.
Report an issue: GitHub.