grpc/grpc-go · error

malformed duration : contains no numbers

Error message

malformed duration %q: contains no numbers

What it means

Fires at duration.go:108 when neither the seconds part nor the fractional part of a protobuf Duration JSON string contains any digits. The parser allows either side to be empty ("5s" or ".5s") but requires at least one numeric component; a value with digits in neither is meaningless.

Solutions

  1. Check the %q value: it should be just 's' or '.s'.
  2. Provide an explicit numeric value, e.g. "0s" for zero or "1.5s" for a real duration.
  3. Ensure config defaults emit a complete numeric string rather than a bare unit suffix.

Example fix

// before
// "timeout": "s"      // no digits anywhere

// after
// "timeout": "0s"      // or a meaningful value like "5s"
Defensive patterns

Strategy: validation

Validate before calling

func requireDurationValue(s string) error {
    body := strings.TrimSuffix(s, "s")
    body = strings.TrimPrefix(body, "-")
    parts := strings.SplitN(body, ".", 2)
    for _, p := range parts {
        if p != "" && p != strings.Trim(p, " ") {
            if _, err := strconv.Atoi(p); err == nil {
                return nil // at least one numeric part
            }
        }
    }
    return fmt.Errorf("duration %q has no numeric component", s)
}

Try / catch

if err := json.Unmarshal(rawConfig, &cfg); err != nil {
    if strings.Contains(err.Error(), "contains no numbers") {
        // supply a real default like "0s"
    }
}

Prevention

When it happens

Trigger: A Duration string that, after stripping the trailing 's' and any leading '-', yields only a '.' with nothing around it. Examples: "s", ".s", "-.s", ".". Note "1.s" and ".5s" are fine; this fires only when hasDigits stays false.

Common situations: A config template placeholder that was never substituted and resolved to empty; a field that defaulted to the literal string "s"; an empty string concatenated with an 's' suffix by buggy formatting code.

Understand the failure class

Related errors


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

Appendix: 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 0c51461d27)