{"record":{"id":"5ab2fe5e0a317a4f","repo":"grpc/grpc-go","slug":"malformed-duration-q-v","errorCode":null,"errorMessage":"malformed duration %q: %v","messagePattern":"malformed duration %q: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/serviceconfig/duration.go","lineNumber":85,"sourceCode":"\t\treturn fmt.Errorf(\"malformed duration %q: missing seconds unit\", s)\n\t}\n\tneg := false\n\tif s[0] == '-' {\n\t\tneg = true\n\t\ts = s[1:]\n\t}\n\tss := strings.SplitN(s[:len(s)-1], \".\", 3)\n\tif len(ss) > 2 {\n\t\treturn fmt.Errorf(\"malformed duration %q: too many decimals\", s)\n\t}\n\t// hasDigits is set if either the whole or fractional part of the number is\n\t// present, since both are optional but one is required.\n\thasDigits := false\n\tvar sec, ns int64\n\tif len(ss[0]) > 0 {\n\t\tvar err error\n\t\tif sec, err = strconv.ParseInt(ss[0], 10, 64); err != nil {\n\t\t\treturn fmt.Errorf(\"malformed duration %q: %v\", s, err)\n\t\t}\n\t\t// Maximum seconds value per the durationpb spec.\n\t\tconst maxProtoSeconds = 315_576_000_000\n\t\tif sec > maxProtoSeconds {\n\t\t\treturn fmt.Errorf(\"out of range: %q\", s)\n\t\t}\n\t\thasDigits = true\n\t}\n\tif len(ss) == 2 && len(ss[1]) > 0 {\n\t\tif len(ss[1]) > 9 {\n\t\t\treturn fmt.Errorf(\"malformed duration %q: too many digits after decimal\", s)\n\t\t}\n\t\tvar err error\n\t\tif ns, err = strconv.ParseInt(ss[1], 10, 64); err != nil {\n\t\t\treturn fmt.Errorf(\"malformed duration %q: %v\", s, err)\n\t\t}\n\t\tfor i := 9; i > len(ss[1]); i-- {\n\t\t\tns *= 10","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/serviceconfig/duration.go#L67-L103","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (service config JSON)\n// \"initialBackoff\": \"1_000ms\"   // '_' not valid for strconv.ParseInt\n// \"maxBackoff\": \"0x10s\"         // hex prefix rejected\n\n// after\n// \"initialBackoff\": \"1s\"\n// \"maxBackoff\": \"16s\"","handlingStrategy":"validation","validationCode":"// Validate a protobuf Duration JSON string before applying config.\nimport \"encoding/json\"\nimport svcconfig \"google.golang.org/grpc/internal/serviceconfig\"\n\nfunc validDurationJSON(s string) error {\n    var d svcconfig.Duration\n    if err := json.Unmarshal([]byte(`\"`+s+`\"`), &d); err != nil {\n        return err\n    }\n    return nil\n}","typeGuard":"// Narrow a config field to a known-good Duration before use.\nfunc asDuration(s string) (time.Duration, bool) {\n    var d svcconfig.Duration\n    if err := json.Unmarshal([]byte(`\"`+s+`\"`), &d); err != nil {\n        return 0, false\n    }\n    return time.Duration(d), true\n}","tryCatchPattern":"if err := json.Unmarshal(rawConfig, &cfg); err != nil {\n    // err will contain \"malformed duration ...\" for bad Duration fields\n    log.Fatalf(\"invalid service config: %v\", err)\n}","preventionTips":["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."],"tags":["config","duration","json","service-config","protobuf"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}