juanfont/headscale · error

parsing duration: %w

Error message

parsing duration: %w

What it means

Returned by expirationFromFlag when model.ParseDuration rejects the --expiration flag value. The parser expects a Prometheus-style duration: a number followed by a unit such as s, m, h, d, w, y (e.g. "90d", "1h"). An empty, unitless ("90"), misspelled, or negative value produces a parse error that is wrapped with this message.

Source

Thrown at cmd/headscale/cli/utils.go:334

	out, err := formatOutput(result, override, format)
	if err != nil {
		return err
	}

	fmt.Println(out)

	return nil
}

// expirationFromFlag parses the --expiration flag as a Prometheus-style
// duration (e.g. "90d", "1h") and returns an absolute time.
func expirationFromFlag(cmd *cobra.Command) (time.Time, error) {
	durationStr, _ := cmd.Flags().GetString("expiration")

	duration, err := model.ParseDuration(durationStr)
	if err != nil {
		return time.Time{}, fmt.Errorf("parsing duration: %w", err)
	}

	return time.Now().UTC().Add(time.Duration(duration)), nil
}

// confirmAction returns true when the user confirms a prompt, or when
// --force is set.  Callers decide what to do when it returns false.
func confirmAction(cmd *cobra.Command, prompt string) bool {
	force, _ := cmd.Flags().GetBool("force")
	if force {
		return true
	}

	return util.YesNo(prompt)
}

// renderTable prints a human-readable pterm table with the given header row
// and data rows, using the shared header styling.

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Suffix the value with a supported unit: --expiration 90d (also m, h, w, y are accepted by the Prometheus-style parser)
  2. Check for stray characters: spaces, commas, or a missing/extra unit suffix
  3. If the flag is optional in your workflow, verify you are not passing an empty string explicitly (--expiration "")
  4. See the command's help output for the expected duration format

Example fix

# before
headscale preauthkeys create --user myuser --expiration 90
# error: parsing duration: ...

# after
headscale preauthkeys create --user myuser --expiration 90d
Defensive patterns

Strategy: validation

Validate before calling

// Validate a Prometheus-style duration before passing it to --expiration.
func validDuration(s string) bool {
	if s == "" {
		return false
	}
	re := regexp.MustCompile(`^([0-9]+(?:\.[0-9]+)?)(ms|s|m|h|d|w|y)$`)
	return re.MatchString(s)
}

// usage:
// if !validDuration(exp) { return fmt.Errorf("invalid --expiration %q: use e.g. 90d", exp) }

Try / catch

In Go: check the error returned by the CLI command; unwrap with errors.Unwrap to distinguish parse failures from other command errors and print the offending flag value.

Prevention

When it happens

Trigger: Running a CLI command that accepts --expiration (e.g. preauthkeys create) with a value like "90" (no unit), "1day" (invalid unit), "-5h" (negative), "1.5 d" (space), or an empty string; model.ParseDuration returns an error which is wrapped and returned to the command.

Common situations: Users coming from tools that accept plain day counts (expiration=90) instead of unit-suffixed durations; scripts migrated from older headscale versions or other CLIs with different duration syntax; typos in automation/CI flags.

Related errors


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