hashicorp/nomad · error

unexpected ExpirationLeeway type: %v

Error message

unexpected ExpirationLeeway type: %v

What it means

In api/acl.go, when unmarshaling a bound ACL (AuthMethod/BindingRule clock skew config), the JSON field for ExpirationLeeway may arrive as a string or number. If the raw JSON value is neither string nor float64, this error is returned during custom UnmarshalJSON handling of the leeway duration.

Source

Thrown at api/acl.go:956

	}{
		Alias: (*Alias)(c),
	}
	if err := json.Unmarshal(data, &aux); err != nil {
		return err
	}
	var err error
	if aux.ExpirationLeeway != nil {
		switch v := aux.ExpirationLeeway.(type) {
		case string:
			if v != "" {
				if c.ExpirationLeeway, err = time.ParseDuration(v); err != nil {
					return err
				}
			}
		case float64:
			c.ExpirationLeeway = time.Duration(v)
		default:
			return fmt.Errorf("unexpected ExpirationLeeway type: %v", v)
		}
	}
	if aux.NotBeforeLeeway != nil {
		switch v := aux.NotBeforeLeeway.(type) {
		case string:
			if v != "" {
				if c.NotBeforeLeeway, err = time.ParseDuration(v); err != nil {
					return err
				}
			}
		case float64:
			c.NotBeforeLeeway = time.Duration(v)
		default:
			return fmt.Errorf("unexpected NotBeforeLeeway type: %v", v)
		}
	}
	if aux.ClockSkewLeeway != nil {
		switch v := aux.ClockSkewLeeway.(type) {

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Send ExpirationLeeway as a duration string like "5m", "1h30m", or as an integer number of nanoseconds.
  2. Fix client code so it marshals time.Duration values as strings/ints rather than objects.
  3. Confirm the API version supports the field and the struct's custom UnmarshalJSON is invoked (not plain json.Unmarshal into a conflicting type).

Example fix

// before
{"ExpirationLeeway": {"value": 5, "unit": "m"}}
// after
{"ExpirationLeeway": "5m"}
Defensive patterns

Strategy: validation

Validate before calling

// ensure leeway fields are string durations before sending
func validLeeway(v interface{}) bool {
    switch t := v.(type) {
    case string:
        _, err := time.ParseDuration(t)
        return err == nil
    case float64:
        return true
    default:
        return false
    }
}

Type guard

func asDurationString(v interface{}) (string, bool) {
    s, ok := v.(string)
    if !ok {
        return "", false
    }
    if _, err := time.ParseDuration(s); err != nil {
        return "", false
    }
    return s, true
}

Prevention

When it happens

Trigger: POST/PUT to /v1/acl/auth-method (or decoding an auth method response) where the `ExpirationLeeway` field in the JSON payload is an object, bool, null-as-non-pointer, or otherwise not a string duration or numeric nanoseconds.

Common situations: Hand-built JSON payloads sending `{"ExpirationLeeway": "3 hours and 5 minutes"}` or nested objects; SDK consumers marshaling a struct into the aux field with the wrong type; Go time.Duration marshaled as a struct.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/b7714519b2489de6. Report an issue: GitHub.