juanfont/headscale · error
unmarshalling user JSON: %w (raw: %s)
Error message
unmarshalling user JSON: %w (raw: %s)
What it means
This error is returned by extractUserID in the headscale dev tool (cmd/dev/main.go) when json.Unmarshal fails on the output of the headscale 'users create' CLI command. The dev driver expects the command to print a JSON object with an "id" field; any non-JSON bytes on stdout make the unmarshal fail. The raw bytes are included in the message to make the mismatch visible.
Source
Thrown at cmd/dev/main.go:289
// runHS executes a headscale CLI command and returns its stdout.
func runHS(ctx context.Context, bin, config string, args ...string) ([]byte, error) {
fullArgs := append([]string{"-c", config}, args...)
cmd := exec.CommandContext(ctx, bin, fullArgs...)
cmd.Stderr = os.Stderr
return cmd.Output()
}
// extractUserID parses the JSON output of "users create" and returns the
// user ID.
func extractUserID(data []byte) (uint64, error) {
var user struct {
ID uint64 `json:"id"`
}
err := json.Unmarshal(data, &user)
if err != nil {
return 0, fmt.Errorf("unmarshalling user JSON: %w (raw: %s)", err, data)
}
return user.ID, nil
}
// extractAuthKey parses the JSON output of "preauthkeys create" and
// returns the key string.
func extractAuthKey(data []byte) (string, error) {
var key struct {
Key string `json:"key"`
}
err := json.Unmarshal(data, &key)
if err != nil {
return "", fmt.Errorf("unmarshalling key JSON: %w (raw: %s)", err, data)
}
if key.Key == "" {View on GitHub (pinned to 565fd254d0)
Solutions
- Look at the '(raw: ...)' part of the message to see what was actually printed — it is usually an error string, not JSON
- Re-run the failing 'headscale users create ...' command manually with the same flags to see the underlying error
- Rebuild the headscale binaries (make build) so cmd/dev invokes a binary matching the current code
- Ensure the user being created does not already exist and that the server address/socket configured for the dev tool is reachable
Example fix
// before
out, err := cmd.Output()
if err != nil {
return 0, err
}
id, err := extractUserID(out)
// after
out, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return 0, fmt.Errorf("users create failed: %v (stderr: %s)", err, ee.Stderr)
}
return 0, err
}
id, err := extractUserID(out) Defensive patterns
Strategy: validation
Validate before calling
func isJSON(b []byte) bool { return json.Valid(b) }
if !isJSON(out) {
return 0, fmt.Errorf("users create did not emit JSON (raw: %s)", out)
}
id, err := extractUserID(out) Try / catch
if _, err := extractUserID(out); err != nil {
log.Printf("user id extraction failed: %v", err)
// abort the dev flow; do not retry with the same input
} Prevention
- Treat command stderr separately: use cmd.Output() only after checking the command's own error
- Validate json.Valid before unmarshalling to get a clearer failure message
- Pin the headscale binary version used by cmd/dev to the same commit
When it happens
Trigger: Running cmd/dev workflows where 'headscale users create' is executed and its stdout is piped to extractUserID. Fails when the binary prints an error message, usage help, log lines, or an empty string instead of the expected JSON (e.g., server not built, wrong subcommand, or a version whose output format differs).
Common situations: Building only the server and not the CLI binary used by cmd/dev; running the dev tool against an older/newer headscale binary whose 'users create' output changed; the command failing (e.g., duplicate user) and writing an error to stdout; stray log output polluting stdout.
Related errors
- unmarshalling key JSON: %w (raw: %s)
- creating user: %w
- parsing user: %w
- creating pre-auth key: %w
- parsing pre-auth key: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/7a9c54fd605af87e.
Report an issue: GitHub.