juanfont/headscale · error

listing updated clients: %w

Error message

listing updated clients: %w

What it means

Thrown inside a backoff.Retry loop in Scenario.AddAndLoginClient when s.ListTailscaleClients(username) returns an error while polling for a newly created tailscale node to appear. The list call executes `headscalectl listnodes` (or equivalent) inside the headscale container, so any container exec failure, CLI error, or malformed output surfaces here. The retry wrapper keeps invoking it for up to 10s, so this error only propagates if listing fails every attempt.

Source

Thrown at integration/helpers.go:1169

	// Get the original client list
	originalClients, err := s.ListTailscaleClients(username)
	if err != nil {
		return nil, fmt.Errorf("listing original clients: %w", err)
	}

	// Create the new node
	err = s.CreateTailscaleNodesInUser(username, version, 1, tsOpts...)
	if err != nil {
		return nil, fmt.Errorf("creating tailscale node: %w", err)
	}

	// Wait for the new node to appear in the client list
	var newClient TailscaleClient

	_, err = backoff.Retry(t.Context(), func() (struct{}, error) {
		updatedClients, err := s.ListTailscaleClients(username)
		if err != nil {
			return struct{}{}, fmt.Errorf("listing updated clients: %w", err)
		}

		if len(updatedClients) != len(originalClients)+1 {
			return struct{}{}, fmt.Errorf("expected %d clients, got %d", len(originalClients)+1, len(updatedClients)) //nolint:err113
		}

		newClient, err = FindNewClient(originalClients, updatedClients)
		if err != nil {
			return struct{}{}, fmt.Errorf("finding new client: %w", err)
		}

		return struct{}{}, nil
	}, backoff.WithBackOff(backoff.NewConstantBackOff(500*time.Millisecond)), backoff.WithMaxElapsedTime(10*time.Second))
	if err != nil {
		return nil, fmt.Errorf("timeout waiting for new client: %w", err)
	}

	// Get the user and create preauth key

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check hs-*.stderr.log in control_logs/<runID>/ to see why the headscale CLI failed inside the container
  2. Verify the headscale container is still running (docker ps) and did not OOM or panic during the test
  3. Re-run with `go run ./cmd/hi doctor` to validate the Docker environment before the test
  4. If the container starts slowly, ensure earlier wait-for-healthy logic completed before AddAndLoginClient is called

Example fix

// No code fix; the wrapping backoff.Retry already retries for 10s.
// If listing fails the whole time, inspect container logs:
//   docker logs <headscale-container> --tail 100
Defensive patterns

Strategy: retry

Validate before calling

// Pre-condition: headscale container healthy before adding clients
if err := headscale.WaitForRunning(30 * time.Second); err != nil {
    t.Fatalf("headscale not ready: %v", err)
}

Try / catch

// The backoff.Retry already retries; treat final error as fatal for the test:
client, err := scenario.AddAndLoginClient(t, user, ver)
if err != nil {
    t.Fatalf("AddAndLoginClient failed (check hs-*.stderr.log): %v", err)
}

Prevention

When it happens

Trigger: Calling Scenario.AddAndLoginClient after CreateTailscaleNodesInUser; each 500ms retry invokes Scenario.ListTailscaleClients(username), which shells into the headscale container and parses node-list JSON. Fails when the headscale container is not running, the exec times out, the CLI exits non-zero, or node JSON cannot be decoded.

Common situations: Head scale container crashed mid-test; Docker daemon under load causing exec timeouts; headscale not yet listening when the first list attempts run; a broken/unbuilt headscale binary inside the container returning errors on every invocation.

Related errors


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