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

  1. Use the '?' modifier only where response headers are set, e.g. after a matcher or within `header <matcher> ?Field value` response context.
  2. For conditional request headers, use a matcher (e.g. `@missing not header X-Custom *`) plus `request_header` inside a matched route.
  3. 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

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


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