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
- Inspect the %q value in the message: the seconds token before the '.' (or before the 's' if no decimal) is what failed ParseInt.
- 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").
- 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.
- 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
- Generate Duration JSON from time.Duration via the library's MarshalJSON rather than hand-formatting.
- Add a config preflight that unmarshals into typed structs and fails fast before dialing.
- Reject non-canonical numeric forms (underscores, hex, scientific notation) at the input boundary.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- malformed duration : contains no numbers
- malformed duration : too many digits after decimal
- out of range
- invalid loadBalancingConfig: entry
- duplicated name
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 *= 10View on GitHub (pinned to 0c51461d27)