{"record":{"id":"623d37627c801540","repo":"tailscale/tailscale","slug":"use-post","errorCode":null,"errorMessage":"use POST","messagePattern":"use POST","errorType":"http","errorClass":null,"httpStatus":405,"severity":"error","filePath":"ipn/localapi/localapi.go","lineNumber":729,"sourceCode":"\t}\n\tif servePprofFunc == nil {\n\t\thttp.Error(w, \"not implemented on this platform\", http.StatusServiceUnavailable)\n\t\treturn\n\t}\n\tservePprofFunc(w, r)\n}\n\n// disconnectControl is the handler for local API /disconnect-control endpoint that shuts down control client, so that\n// node no longer communicates with control. Doing this makes control consider this node inactive. This can be used\n// before shutting down a replica of HA subnet router or app connector deployments to ensure that control tells the\n// peers to switch over to another replica whilst still maintaining th existing peer connections.\nfunc (h *Handler) disconnectControl(w http.ResponseWriter, r *http.Request) {\n\tif !h.PermitWrite {\n\t\thttp.Error(w, \"access denied\", http.StatusForbidden)\n\t\treturn\n\t}\n\tif r.Method != httpm.POST {\n\t\thttp.Error(w, \"use POST\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\th.b.DisconnectControl()\n}\n\nfunc (h *Handler) reloadConfig(w http.ResponseWriter, r *http.Request) {\n\tif !h.PermitWrite {\n\t\thttp.Error(w, \"access denied\", http.StatusForbidden)\n\t\treturn\n\t}\n\tif r.Method != httpm.POST {\n\t\thttp.Error(w, \"use POST\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tok, err := h.b.ReloadConfig()\n\tvar res apitype.ReloadConfigResponse\n\tres.Reloaded = ok\n\tif err != nil {","sourceCodeStart":711,"sourceCodeEnd":747,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/ipn/localapi/localapi.go#L711-L747","documentation":"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.","triggerScenarios":"GET /localapi/v0/disconnect-control (browser, curl without -X POST, prefetcher); DELETE or PUT probes from API explorers.","commonSituations":"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.","solutions":["Send an empty POST: curl -X POST .../localapi/v0/disconnect-control.","Keep drain/failover automation on POST and add a lint/check for the method.","Remember the endpoint takes no body and returns 200 with no content on success."],"exampleFix":"# before\n$ curl --unix-socket /var/run/tailscale/tailscaled.sock http://local-tailscaled.sock/localapi/v0/disconnect-control\n405 use POST\n\n# after\n$ curl -X POST --unix-socket /var/run/tailscale/tailscaled.sock http://local-tailscaled.sock/localapi/v0/disconnect-control","handlingStrategy":"validation","validationCode":"req, _ := http.NewRequestWithContext(ctx, http.MethodPost,\n    \"http://local-tailscaled.sock/localapi/v0/disconnect-control\", nil)\nresp, err := http.DefaultClient.Do(req)","typeGuard":null,"tryCatchPattern":"if resp.StatusCode == http.StatusMethodNotAllowed {\n    return errors.New(\"disconnect-control must be invoked with POST\")\n}","preventionTips":["Keep an endpoint table (path -> method) for LocalAPI calls used by ops scripts.","Never paste action endpoints into browsers/prefetchable contexts; they are POST-only by design."],"tags":["tailscale","localapi","http-method","http-405","post-required"],"backgroundTag":"http-405-method-not-allowed","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}