juanfont/headscale · warning
missing parameters
Error message
missing parameters
What it means
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.
Source
Thrown at cmd/headscale/cli/utils.go:40
"github.com/prometheus/common/model"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
const (
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
SocketWritePermissions = 0o666
outputFormatJSON = "json"
outputFormatJSONLine = "json-line"
outputFormatYAML = "yaml"
)
var (
errAPIKeyNotSet = errors.New("HEADSCALE_CLI_API_KEY environment variable needs to be set")
errMissingParameter = errors.New("missing parameters")
errResponseStatus = errors.New("unexpected response status")
)
// apiError turns a non-2xx response into an error, surfacing the server's
// RFC7807 problem detail. detail holds the operation context and errors[] the
// wrapped cause (e.g. "name is too long"); both are joined so the server's
// message text is not lost.
func apiError(statusCode int, problem *clientv1.ErrorModel) error {
if problem == nil {
return fmt.Errorf("%w: %d %s", errResponseStatus, statusCode, http.StatusText(statusCode))
}
parts := make([]string, 0, 2)
if problem.Detail != nil && *problem.Detail != "" {
parts = append(parts, *problem.Detail)
}
View on GitHub (pinned to 565fd254d0)
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.
Example fix
# before (replaying the same ID -> 404 unknown or expired ping) curl -I 'http://host/.../ping-response?id=old-id' # after: generate a new ping and use its fresh ID curl 'http://host/debug/ping?node=my-machine' # returns new pingID curl -I "http://host/.../ping-response?id=$NEW_PING_ID"
Defensive patterns
Strategy: validation
Validate before calling
// Complete each ping exactly once with its freshly issued ID.
if usedIDs.Contains(pingID) {
return errors.New("ping ID already consumed; request a new ping")
}
usedIDs.Add(pingID)
// then HEAD .../ping-response?id=pingID Try / catch
resp, err := client.Do(req)
if err == nil && resp.StatusCode == http.StatusNotFound {
// ID unknown/expired/consumed: do NOT retry the same ID; re-issue the ping
newID := triggerNewPing()
_ = newID
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- --name or --identifier flag is required
- HEADSCALE_CLI_API_KEY environment variable needs to be set
- health check timed out
- empty auth key in response
- MOCKOIDC_CLIENT_ID not defined
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/42fafeb40aacb532.
Report an issue: GitHub.