caddyserver/caddy · error · caddyhttp.Error

cannot use 'copy_response' outside of reverse_proxy's handle

Error message

cannot use 'copy_response' outside of reverse_proxy's handle_response routes

What it means

`copy_response` is a handler that only works inside a `handle_response` route of `reverse_proxy`, because it relies on the upstream response stored in the request context under `proxyHandleResponseContextCtxKey`. When that context value is absent, Caddy returns HTTP 500 with this message.

Source

Thrown at modules/caddyhttp/reverseproxy/copyresponse.go:65

		New: func() caddy.Module { return new(CopyResponseHandler) },
	}
}

// Provision ensures that h is set up properly before use.
func (h *CopyResponseHandler) Provision(ctx caddy.Context) error {
	h.ctx = ctx
	return nil
}

// ServeHTTP implements the Handler interface.
func (h CopyResponseHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, _ caddyhttp.Handler) error {
	repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
	hrc, ok := req.Context().Value(proxyHandleResponseContextCtxKey).(*handleResponseContext)

	// don't allow this to be used outside of handle_response routes
	if !ok {
		return caddyhttp.Error(http.StatusInternalServerError,
			fmt.Errorf("cannot use 'copy_response' outside of reverse_proxy's handle_response routes"))
	}

	// allow a custom status code to be written; otherwise the
	// status code from the upstream response is written
	if codeStr := h.StatusCode.String(); codeStr != "" {
		intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, ""))
		if err != nil {
			return caddyhttp.Error(http.StatusInternalServerError, err)
		}
		hrc.response.StatusCode = intVal
	}

	// make sure the reverse_proxy handler doesn't try to call
	// finalizeResponse again after we've already done it here.
	hrc.isFinalized = true

	// write the response
	return hrc.handler.finalizeResponse(rw, req, hrc.response, repl, hrc.start, hrc.logger)

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Move copy_response inside a handle_response block of reverse_proxy
  2. Ensure the route order is: reverse_proxy with handle_response, and copy_response within that block
  3. If using JSON config, verify the handler appears only under reverse_proxy's handle_response routes

Example fix

// Caddyfile before
example.com {
	copy_response
	reverse_proxy localhost:9000
}
// Caddyfile after
example.com {
	reverse_proxy localhost:9000 {
		@error status 500
		handle_response @error {
			copy_response
		}
	}
}
Defensive patterns

Strategy: validation

Validate before calling

// JSON config guard: copy_response may only appear under
// reverse_proxy.handlers[].handle_response
func containsOnlyInHandleResponse(cfg *Config) bool {
	for _, r := range cfg.Routes {
		if r.HandlerID == "http.handlers.copy_response" && !r.WithinHandleResponse {
			return false
		}
	}
	return true
}

Prevention

When it happens

Trigger: Configuring `copy_response` as a top-level handler in a normal `handle` route, or inside `reverse_proxy` outside a `handle_response` block. Also programmatically invoking CopyResponseHandler.ServeHTTP on requests that never traversed reverse_proxy's handle_response machinery.

Common situations: Copy-pasting a handle_response fragment into the main route body, or restructuring a Caddyfile and accidentally moving copy_response outside its handle_response block.

Related errors


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