grpc/grpc-go · error

malformed duration

Error message

malformed duration %q: %v

What it means

Fires inside Duration.UnmarshalJSON (duration.go:85) when strconv.ParseInt fails on the whole-number (seconds) portion of a protobuf Duration JSON string. A protobuf Duration is encoded as a quoted string like "3.5s"; the part before any decimal point must be a valid base-10 int64. This wraps the underlying strconv error (e.g. ErrSyntax, ErrRange) so the caller sees both the bad input and the cause.

Solutions

  1. Inspect the %q value in the message: the seconds token before the '.' (or before the 's' if no decimal) is what failed ParseInt.
  2. Correct the offending Duration field to a canonical protobuf string: optional sign, integer seconds, optional '.' with 1-9 fractional digits, trailing 's' (e.g. "1.5s", "-0.5s", "0s").
  3. Validate the config with a JSON schema or by unmarshaling into serviceconfig.Duration in a preflight step before dialing, so the error surfaces at config-load time.
  4. If the value comes from user input, normalize it (strip spaces, reject hex/scientific) before formatting it as a Duration string.

Example fix

// before (service config JSON)
// "initialBackoff": "1_000ms"   // '_' not valid for strconv.ParseInt
// "maxBackoff": "0x10s"         // hex prefix rejected

// after
// "initialBackoff": "1s"
// "maxBackoff": "16s"
Defensive patterns

Strategy: validation

Validate before calling

// Validate a protobuf Duration JSON string before applying config.
import "encoding/json"
import svcconfig "google.golang.org/grpc/internal/serviceconfig"

func validDurationJSON(s string) error {
    var d svcconfig.Duration
    if err := json.Unmarshal([]byte(`"`+s+`"`), &d); err != nil {
        return err
    }
    return nil
}

Type guard

// Narrow a config field to a known-good Duration before use.
func asDuration(s string) (time.Duration, bool) {
    var d svcconfig.Duration
    if err := json.Unmarshal([]byte(`"`+s+`"`), &d); err != nil {
        return 0, false
    }
    return time.Duration(d), true
}

Try / catch

if err := json.Unmarshal(rawConfig, &cfg); err != nil {
    // err will contain "malformed duration ..." for bad Duration fields
    log.Fatalf("invalid service config: %v", err)
}

Prevention

When it happens

Trigger: Unmarshaling JSON into any field typed serviceconfig.Duration where the seconds component is non-numeric or otherwise rejected by strconv.ParseInt(ss[0], 10, 64). Concretely: values like "abc.5s" (non-numeric), "0x10s" (hex prefix), "1_000s" (underscore grouping), " 5s" (leading/trailing whitespace), "1e3s" (scientific notation), or a value whose integer part overflows int64.

Common situations: Hand-authored or templated service-config JSON with typos in timeout/backoff/retry fields; config generated by a tool that emits numbers in a non-canonical form (underscores, scientific notation); values copied from a language that formats large ints differently; a malformed retry policy where InitialBackoff/MaxBackoff strings are wrong.

Understand the failure class

Related errors


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

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