{"record":{"id":"42fafeb40aacb532","repo":"juanfont/headscale","slug":"missing-parameters","errorCode":null,"errorMessage":"missing parameters","messagePattern":"missing parameters","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"cmd/headscale/cli/utils.go","lineNumber":40,"sourceCode":"\t\"github.com/prometheus/common/model\"\n\t\"github.com/pterm/pterm\"\n\t\"github.com/rs/zerolog/log\"\n\t\"github.com/spf13/cobra\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nconst (\n\tHeadscaleDateTimeFormat = \"2006-01-02 15:04:05\"\n\tSocketWritePermissions  = 0o666\n\n\toutputFormatJSON     = \"json\"\n\toutputFormatJSONLine = \"json-line\"\n\toutputFormatYAML     = \"yaml\"\n)\n\nvar (\n\terrAPIKeyNotSet     = errors.New(\"HEADSCALE_CLI_API_KEY environment variable needs to be set\")\n\terrMissingParameter = errors.New(\"missing parameters\")\n\terrResponseStatus   = errors.New(\"unexpected response status\")\n)\n\n// apiError turns a non-2xx response into an error, surfacing the server's\n// RFC7807 problem detail. detail holds the operation context and errors[] the\n// wrapped cause (e.g. \"name is too long\"); both are joined so the server's\n// message text is not lost.\nfunc apiError(statusCode int, problem *clientv1.ErrorModel) error {\n\tif problem == nil {\n\t\treturn fmt.Errorf(\"%w: %d %s\", errResponseStatus, statusCode, http.StatusText(statusCode))\n\t}\n\n\tparts := make([]string, 0, 2)\n\n\tif problem.Detail != nil && *problem.Detail != \"\" {\n\t\tparts = append(parts, *problem.Detail)\n\t}\n","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/cmd/headscale/cli/utils.go#L22-L58","documentation":"HTTP 404 from PingResponseHandler when a HEAD request carries an id that state.CompletePing(pingID) does not recognize — either the ID was never issued, was already consumed (single-use completion), or expired. CompletePing atomically matches and retires the pending ping, so replaying a completed ID also yields this.","triggerScenarios":"Replaying a HEAD /ping-response?id=... after the first call already completed the ping; sending a fabricated or truncated ID; the ping expired server-side (state holds pending pings with TTL) before the client answered; server restarted losing in-memory pending pings.","commonSituations":"Duplicated requests through a retrying proxy or client retry logic; copy-pasting a stale curl command; debug latency where the answer arrives after the ping's lifetime; headscale restart between PingRequest issuance and the client's HEAD.","solutions":["Trigger a fresh ping (e.g. via the debug ping page or a new tailcfg.PingRequest) and answer only once with the newly issued ID.","Remove retry/multiplex logic that replays the HEAD request — one ping ID completes exactly one ping.","If pings routinely expire, reduce latency between request and response or check why the client answers late (DERP relay slowness, clock skew).","After a headscale restart, expect all outstanding ping IDs to be unknown; re-issue them."],"exampleFix":"# before (replaying the same ID -> 404 unknown or expired ping)\ncurl -I 'http://host/.../ping-response?id=old-id'\n\n# after: generate a new ping and use its fresh ID\ncurl 'http://host/debug/ping?node=my-machine'   # returns new pingID\ncurl -I \"http://host/.../ping-response?id=$NEW_PING_ID\"","handlingStrategy":"validation","validationCode":"// Complete each ping exactly once with its freshly issued ID.\nif usedIDs.Contains(pingID) {\n    return errors.New(\"ping ID already consumed; request a new ping\")\n}\nusedIDs.Add(pingID)\n// then HEAD .../ping-response?id=pingID","typeGuard":null,"tryCatchPattern":"resp, err := client.Do(req)\nif err == nil && resp.StatusCode == http.StatusNotFound {\n    // ID unknown/expired/consumed: do NOT retry the same ID; re-issue the ping\n    newID := triggerNewPing()\n    _ = newID\n}","preventionTips":["Never retry or replay a ping-response HEAD; one ID completes one ping.","Answer pings promptly — IDs expire and are lost on headscale restart.","Disable duplicate-request fan-out (retrying proxies, idempotent HTTP layers) for this endpoint."],"tags":["http","ping","expiry","one-time-token","state"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}