grpc/grpc-go · error

malformed duration %q: contains no numbers

Error message

malformed duration %q: contains no numbers

What it means

This error occurs in Duration.UnmarshalJSON when neither the whole-seconds part nor the fractional part of the duration string contains any digits. The parser requires at least one numeric component. For example, a bare ".s" or just "s" triggers this because hasDigits stays false through both parsing branches.

Source

Thrown at internal/serviceconfig/duration.go:108

			return fmt.Errorf("out of range: %q", s)
		}
		hasDigits = true
	}
	if len(ss) == 2 && len(ss[1]) > 0 {
		if len(ss[1]) > 9 {
			return fmt.Errorf("malformed duration %q: too many digits after decimal", s)
		}
		var err error
		if ns, err = strconv.ParseInt(ss[1], 10, 64); err != nil {
			return fmt.Errorf("malformed duration %q: %v", s, err)
		}
		for i := 9; i > len(ss[1]); i-- {
			ns *= 10
		}
		hasDigits = true
	}
	if !hasDigits {
		return fmt.Errorf("malformed duration %q: contains no numbers", s)
	}

	if neg {
		sec *= -1
		ns *= -1
	}

	// Maximum/minimum seconds/nanoseconds representable by Go's time.Duration.
	const maxSeconds = math.MaxInt64 / int64(time.Second)
	const maxNanosAtMaxSeconds = math.MaxInt64 % int64(time.Second)
	const minSeconds = math.MinInt64 / int64(time.Second)
	const minNanosAtMinSeconds = math.MinInt64 % int64(time.Second)

	if sec > maxSeconds || (sec == maxSeconds && ns >= maxNanosAtMaxSeconds) {
		*d = Duration(math.MaxInt64)
	} else if sec < minSeconds || (sec == minSeconds && ns <= minNanosAtMinSeconds) {
		*d = Duration(math.MinInt64)
	} else {

View on GitHub (pinned to 03255a9237)

Solutions

  1. Provide a complete duration string with at least one numeric digit, e.g. "0s", "1s", or "0.5s".
  2. Audit the source generating the JSON (control plane, config template) to ensure duration fields are never empty.
  3. Add pre-validation on the JSON payload before unmarshaling into serviceconfig types.

Example fix

// before (broken): no digits at all
"timeout": "s"

// after (valid): at least the seconds field
"timeout": "0s"
Defensive patterns

Strategy: validation

Validate before calling

// Ensure a duration string has at least one digit before unmarshaling
func hasDurationDigit(s string) bool {
    if !strings.HasSuffix(s, "s") { return false }
    body := strings.TrimSuffix(s, "s")
    if len(body) > 0 && body[0] == '-' { body = body[1:] }
    parts := strings.SplitN(body, ".", 2)
    return parts[0] != "" || (len(parts) == 2 && parts[1] != "")
}

Try / catch

var d serviceconfig.Duration
if err := json.Unmarshal(raw, &d); err != nil {
    if strings.Contains(err.Error(), "contains no numbers") {
        d = serviceconfig.Duration(0) // default
    } else { return err }
}

Prevention

When it happens

Trigger: A JSON field unmarshaled into serviceconfig.Duration contains a value like "s", ".s", "-.s", or an empty-ish string that ends with 's' but has no digits before or after the decimal point.

Common situations: Missing/blank duration values in xDS service config, template substitution errors producing empty duration strings, or deserialization of a struct with zero-value or uninitialized duration fields serialized as bare units.

Understand the failure class

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/ef2f5ce4f44fdb95. Report an issue: GitHub.