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

  1. Use unsyncedConfigAccess (or the standard admin GET handlers) for reads — changeConfig is for POST/PUT/PATCH/DELETE only.
  2. Issue requests with a mutating verb: curl -X POST/PUT/PATCH/DELETE as appropriate for /load, /config/, /id/ endpoints.
  3. 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

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


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