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

  1. 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.
  2. Remove retry/multiplex logic that replays the HEAD request — one ping ID completes exactly one ping.
  3. If pings routinely expire, reduce latency between request and response or check why the client answers late (DERP relay slowness, clock skew).
  4. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/42fafeb40aacb532. Report an issue: GitHub.