grpc/grpc-go · error

malformed duration %q: %v

Error message

malformed duration %q: %v

What it means

Returned by Duration.UnmarshalJSON when strconv.ParseInt fails on the integer-seconds portion of the duration. This happens when the seconds part is non-numeric, empty in an invalid position, or exceeds int64 range. The wrapped error is the strconv error, surfaced as part of the malformed-duration message.

Source

Thrown at internal/serviceconfig/duration.go:85

		return fmt.Errorf("malformed duration %q: missing seconds unit", s)
	}
	neg := false
	if s[0] == '-' {
		neg = true
		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

View on GitHub (pinned to 03255a9237)

Solutions

  1. Ensure the seconds component is a base-10 integer within int64 range.
  2. Strip any non-digit characters (units, placeholders) from the value before writing config.
  3. Use protojson/serviceconfig.Duration marshaling to produce well-formed values.

Example fix

// before
{"timeout": "5m"}

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

Strategy: validation

Validate before calling

func validateProtoDuration(s string) error {
    body := strings.TrimSuffix(s, "s")
    parts := strings.SplitN(body, ".", 2)
    if len(parts[0]) > 0 {
        if _, err := strconv.ParseInt(parts[0], 10, 64); err != nil {
            return fmt.Errorf("non-integer seconds in %q: %w", s, err)
        }
    }
    var d serviceconfig.Duration
    return d.UnmarshalJSON([]byte(`"` + s + `"`))
}

Type guard

func secondsPartIsInt64(s string) bool {
    body := strings.TrimSuffix(s, "s")
    sec := strings.SplitN(body, ".", 2)[0]
    if sec == "" { return true }
    _, err := strconv.ParseInt(sec, 10, 64)
    return err == nil
}

Prevention

When it happens

Trigger: A value like "xs" (non-numeric seconds), "999999999999999999999999s" (overflows int64), or "" patterns that pass the suffix/decimal checks but fail integer parsing at duration.go:84.

Common situations: Config templating substituted a placeholder or unit suffix into the seconds field; copy/paste of a Go duration like "5m" where 'm' lands in the seconds slot; numeric overflow from mis-scaled milliseconds.

Understand the failure class

Related errors


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