grpc/grpc-go · error

malformed duration %q: too many decimals

Error message

malformed duration %q: too many decimals

What it means

Returned by Duration.UnmarshalJSON when the duration string contains more than one decimal point. The number is split on '.' with a limit of 3; producing 3 parts (two dots) is invalid per the protobuf JSON Duration grammar, which permits at most one fractional component. Example: "1.2.3s".

Source

Thrown at internal/serviceconfig/duration.go:76

}

// UnmarshalJSON unmarshals b as a duration JSON string into d.
func (d *Duration) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	if !strings.HasSuffix(s, "s") {
		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 {

View on GitHub (pinned to 03255a9237)

Solutions

  1. Rewrite the value with at most one decimal point, e.g. "1.5s".
  2. Generate durations via serviceconfig.Duration / protojson marshal rather than hand-editing strings.
  3. Lint duration fields for stray dots before applying service config.

Example fix

// before
{"timeout": "1.5.5s"}

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

Strategy: validation

Validate before calling

func validateProtoDuration(s string) error {
    body := strings.TrimSuffix(s, "s")
    if strings.Count(body, ".") > 1 {
        return fmt.Errorf("too many decimals in %q", s)
    }
    var d serviceconfig.Duration
    return d.UnmarshalJSON([]byte(`"` + s + `"`))
}

Type guard

func hasAtMostOneDecimalPoint(s string) bool {
    return strings.Count(strings.TrimSuffix(s, "s"), ".") <= 1
}

Prevention

When it happens

Trigger: Passing a config value like "1.5.5s" or any duration string with multiple '.' separators. The check is `if len(ss) > 2` after strings.SplitN(s, ".", 3) at duration.go:74.

Common situations: Hand-built JSON config with a typo; templating that concatenated values producing an extra dot; localized number formatting leaking into config.

Understand the failure class

Related errors


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