tailscale/tailscale · error

use POST

Error message

use POST

What it means

Returned as HTTP 405 by disconnectControl when PermitWrite passed but the method is not POST. Disconnecting control is a state-changing action, and LocalAPI pins such actions to POST; GET is rejected so a URL click or prefetch cannot silently detach the node from control.

Source

Thrown at ipn/localapi/localapi.go:729

	}
	if servePprofFunc == nil {
		http.Error(w, "not implemented on this platform", http.StatusServiceUnavailable)
		return
	}
	servePprofFunc(w, r)
}

// disconnectControl is the handler for local API /disconnect-control endpoint that shuts down control client, so that
// node no longer communicates with control. Doing this makes control consider this node inactive. This can be used
// before shutting down a replica of HA subnet router or app connector deployments to ensure that control tells the
// peers to switch over to another replica whilst still maintaining th existing peer connections.
func (h *Handler) disconnectControl(w http.ResponseWriter, r *http.Request) {
	if !h.PermitWrite {
		http.Error(w, "access denied", http.StatusForbidden)
		return
	}
	if r.Method != httpm.POST {
		http.Error(w, "use POST", http.StatusMethodNotAllowed)
		return
	}
	h.b.DisconnectControl()
}

func (h *Handler) reloadConfig(w http.ResponseWriter, r *http.Request) {
	if !h.PermitWrite {
		http.Error(w, "access denied", http.StatusForbidden)
		return
	}
	if r.Method != httpm.POST {
		http.Error(w, "use POST", http.StatusMethodNotAllowed)
		return
	}
	ok, err := h.b.ReloadConfig()
	var res apitype.ReloadConfigResponse
	res.Reloaded = ok
	if err != nil {

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Send an empty POST: curl -X POST .../localapi/v0/disconnect-control.
  2. Keep drain/failover automation on POST and add a lint/check for the method.
  3. Remember the endpoint takes no body and returns 200 with no content on success.

Example fix

# before
$ curl --unix-socket /var/run/tailscale/tailscaled.sock http://local-tailscaled.sock/localapi/v0/disconnect-control
405 use POST

# after
$ curl -X POST --unix-socket /var/run/tailscale/tailscaled.sock http://local-tailscaled.sock/localapi/v0/disconnect-control
Defensive patterns

Strategy: validation

Validate before calling

req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
    "http://local-tailscaled.sock/localapi/v0/disconnect-control", nil)
resp, err := http.DefaultClient.Do(req)

Try / catch

if resp.StatusCode == http.StatusMethodNotAllowed {
    return errors.New("disconnect-control must be invoked with POST")
}

Prevention

When it happens

Trigger: GET /localapi/v0/disconnect-control (browser, curl without -X POST, prefetcher); DELETE or PUT probes from API explorers.

Common situations: Copy-pasting the endpoint into a browser to 'test' it; HTTP clients configured with GET for idempotency checks (health-check tools that probe endpoints with GET); typos in scripts using curl -d without -X POST is actually POST, but curl without any body flag defaults to GET.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/623d37627c801540. Report an issue: GitHub.