caddyserver/caddy · error
method not allowed
Error message
method not allowed
What it means
changeConfig is Caddy's internal entry point for the admin API's config endpoints and only accepts mutating methods. GET, HEAD, OPTIONS, CONNECT, and TRACE are rejected up-front with a plain 'method not allowed' error before any lock is taken. In the HTTP handler this maps to 405; when called from Go code it is a guard against using a read method on a write path.
Source
Thrown at caddy.go:165
// If the resulting config is the same as the previous, no reload will
// occur unless forceReload is true. If the config is unchanged and not
// forcefully reloaded, then errConfigUnchanged is returned. This function
// is safe for concurrent use.
// The ifMatchHeader can optionally be given a string of the format:
//
// "<path> <hash>"
//
// where <path> is the absolute path in the config and <hash> is the expected hash of
// the config at that path. If the hash in the ifMatchHeader doesn't match
// the hash of the config, then an APIError with status 412 will be returned.
func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error {
switch method {
case http.MethodGet,
http.MethodHead,
http.MethodOptions,
http.MethodConnect,
http.MethodTrace:
return fmt.Errorf("method not allowed")
}
rawCfgMu.Lock()
defer rawCfgMu.Unlock()
if ifMatchHeader != "" {
// expect the first and last character to be quotes
if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect quoted string"),
}
}
// read out the parts
parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
if len(parts) != 2 {
return APIError{View on GitHub (pinned to 50e54ee279)
Solutions
- Use unsyncedConfigAccess (or the standard admin GET handlers) for reads — changeConfig is for POST/PUT/PATCH/DELETE only.
- Issue requests with a mutating verb: curl -X POST/PUT/PATCH/DELETE as appropriate for /load, /config/, /id/ endpoints.
- If writing a custom admin route, dispatch read verbs to a read handler before calling changeConfig.
Example fix
// before err := changeConfig(http.MethodGet, "/config/", body, "", false) // after cfg, err := unsyncedConfigAccess(http.MethodGet, "/config/", nil, nil)
Defensive patterns
Strategy: validation
Validate before calling
func isMutating(method string) bool {
switch method {
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
return true
}
return false
}
if !isMutating(method) {
return errors.New("use unsyncedConfigAccess for reads")
} Prevention
- Route reads through GET handlers / unsyncedConfigAccess, never changeConfig.
- Check the documented method list per admin endpoint before scripting it.
When it happens
Trigger: Calling changeConfig(http.MethodGet, ...) directly from Go code; a custom admin router or plugin that routes a GET/HEAD/OPTIONS/CONNECT/TRACE request into changeConfig instead of unsyncedConfigAccess; scripted curl using a read-only verb against POST/PUT/PATCH-only endpoints.
Common situations: Plugin authors wiring admin endpoints and reusing changeConfig for reads; curl -X HEAD/OPTIONS against /config/ paths where the adapter expected GET semantics handled elsewhere; version upgrades that tightened method handling.
Related errors
- method not allowed
- request path is missing object ID
- malformed object path
- unknown object ID '%s'
- malformed If-Match header; expect quoted string
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/1f7630f9b960dea8.
Report an issue: GitHub.