semaphoreui/semaphore · error

invalid JWT TTL

Error message

invalid JWT TTL %q: %w

What it means

TemplateJWTParams.ParsedTTL converts the TTL string (e.g. '5m', '2h') into a time.Duration using time.ParseDuration. If the string is not a valid Go duration, it returns 'invalid JWT TTL %q' wrapped with the parse error. Validate calls this to reject misconfigured JWT parameters.

Solutions

  1. Use a valid Go duration string with a unit suffix, e.g. '5m', '30s', '2h'
  2. Convert bare numbers to include a unit: '300' becomes '300s'
  3. Add a template-validation step (calling Validate/ParsedTTL) in CI to catch bad TTLs before deployment

Example fix

// before
jwt_params:
  ttl: "300"
// after
jwt_params:
  ttl: "5m"
Defensive patterns

Strategy: validation

Validate before calling

if p.TTL != "" {
    if _, err := time.ParseDuration(p.TTL); err != nil {
        return fmt.Errorf("jwt ttl %q must be a Go duration like 30s, 5m, 2h", p.TTL)
    }
}

Try / catch

if err := params.Validate(); err != nil {
    var derr error
    _, derr = params.ParsedTTL()
    if derr != nil && strings.Contains(derr.Error(), "invalid JWT TTL") {
        return fmt.Errorf("fix jwt_params.ttl (use 30s/5m/2h format): %w", derr)
    }
    return err
}

Prevention

When it happens

Trigger: Defining a template's jwt_params with a TTL like '300' (no unit), '5min', or '1h30' and then running template validation.

Common situations: Authors writing bare numbers expecting seconds; using human aliases like '5min' or '1d' that Go's ParseDuration doesn't accept ('min' works, 'd' does not); copying TTL formats from other systems (cron/ISO 8601).

Understand the failure class

Background: "invalid duration" / "failed to parse duration": why your timeout, interval, or TTL string is rejected and which formats each library accepts — this error's family across 32 libraries.

Related errors


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/a5811dabf99e04d8. Report an issue: GitHub.

Appendix: source

Thrown at db/TemplateJWT.go:66

	}
}

// Value implements driver.Valuer so TemplateJWTParams can be written to the database.
func (p *TemplateJWTParams) Value() (driver.Value, error) {
	if p == nil {
		return nil, nil
	}
	return json.Marshal(p)
}

// ParsedTTL returns the parsed TTL or zero when unset.
func (p *TemplateJWTParams) ParsedTTL() (time.Duration, error) {
	if p == nil || p.TTL == "" {
		return 0, nil
	}
	d, err := time.ParseDuration(p.TTL)
	if err != nil {
		return 0, fmt.Errorf("invalid JWT TTL %q: %w", p.TTL, err)
	}
	return d, nil
}

// Validate enforces some sanity checks on the JWT parameters to prevent misconfiguration and abuse.
func (p *TemplateJWTParams) Validate() error {
	if p == nil || !p.Enabled {
		return nil
	}

	if len(p.Audience) > maxJWTAudienceEntries {
		return common_errors.NewValidationError(fmt.Sprintf("JWT audience must contain at most %d entries", maxJWTAudienceEntries))
	}
	if slices.Contains(p.Audience, "") {
		return common_errors.NewValidationError("JWT audience entries must not be empty")
	}

	if p.TTL != "" {

View on GitHub (pinned to 1774ccb71a)