grpc/grpc-go · error

out of range: %q

Error message

out of range: %q

What it means

Returned by Duration.UnmarshalJSON when the parsed seconds value exceeds the protobuf maximum of 315,576,000,000 seconds (~10,000 years). This enforces the protobuf Duration spec upper bound separately from Go's time.Duration overflow handling; the value is rejected outright rather than clamped.

Source

Thrown at internal/serviceconfig/duration.go:90

		s = s[1:]
	}
	ss := strings.SplitN(s[:len(s)-1], ".", 3)
	if len(ss) > 2 {
		return fmt.Errorf("malformed duration %q: too many decimals", s)
	}
	// hasDigits is set if either the whole or fractional part of the number is
	// present, since both are optional but one is required.
	hasDigits := false
	var sec, ns int64
	if len(ss[0]) > 0 {
		var err error
		if sec, err = strconv.ParseInt(ss[0], 10, 64); err != nil {
			return fmt.Errorf("malformed duration %q: %v", s, err)
		}
		// Maximum seconds value per the durationpb spec.
		const maxProtoSeconds = 315_576_000_000
		if sec > maxProtoSeconds {
			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)

View on GitHub (pinned to 03255a9237)

Solutions

  1. Convert the value to seconds using the correct source unit (divide ms by 1000, ns by 1e9).
  2. Cap intentionally-large values at or below 315576000000s.
  3. Validate config durations programmatically before sending to gRPC.

Example fix

// before
{"timeout": "86400000s"}  // meant 86400000 ms

// after
{"timeout": "86400s"}
Defensive patterns

Strategy: validation

Validate before calling

const maxProtoSeconds = int64(315_576_000_000)

func validateProtoDuration(s string) error {
    body := strings.TrimSuffix(s, "s")
    secStr := strings.SplitN(body, ".", 2)[0]
    if secStr != "" {
        sec, err := strconv.ParseInt(secStr, 10, 64)
        if err != nil { return err }
        if sec > maxProtoSeconds { return fmt.Errorf("seconds %d exceeds protobuf max %d", sec, maxProtoSeconds) }
    }
    return nil
}

Type guard

func withinProtoSecondsRange(s string) bool {
    body := strings.TrimSuffix(s, "s")
    secStr := strings.SplitN(body, ".", 2)[0]
    if secStr == "" { return true }
    sec, err := strconv.ParseInt(secStr, 10, 64)
    return err == nil && sec <= 315_576_000_000
}

Prevention

When it happens

Trigger: A duration value whose seconds field is greater than maxProtoSeconds (defined at duration.go:88). Example: "400000000000s". Common when milliseconds are mistakenly written as seconds.

Common situations: Unit confusion — writing 86400000 meaning milliseconds but parsed as seconds (≈2750 years over the cap); accidentally sending a nanosecond value as seconds; schema generator emitting epoch-style numbers.

Related errors


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