{"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/03255a9237b6eb32710f6bc4f2de9a675b99fe36/internal/serviceconfig/duration.go#L67-L103","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the seconds component is a base-10 integer within int64 range.","Strip any non-digit characters (units, placeholders) from the value before writing config.","Use protojson/serviceconfig.Duration marshaling to produce well-formed values."],"exampleFix":"// before\n{\"timeout\": \"5m\"}\n\n// after\n{\"timeout\": \"300s\"}","handlingStrategy":"validation","validationCode":"func validateProtoDuration(s string) error {\n    body := strings.TrimSuffix(s, \"s\")\n    parts := strings.SplitN(body, \".\", 2)\n    if len(parts[0]) > 0 {\n        if _, err := strconv.ParseInt(parts[0], 10, 64); err != nil {\n            return fmt.Errorf(\"non-integer seconds in %q: %w\", s, err)\n        }\n    }\n    var d serviceconfig.Duration\n    return d.UnmarshalJSON([]byte(`\"` + s + `\"`))\n}","typeGuard":"func secondsPartIsInt64(s string) bool {\n    body := strings.TrimSuffix(s, \"s\")\n    sec := strings.SplitN(body, \".\", 2)[0]\n    if sec == \"\" { return true }\n    _, err := strconv.ParseInt(sec, 10, 64)\n    return err == nil\n}","tryCatchPattern":null,"preventionTips":["Never write Go time.ParseDuration tokens (5m, 1h) into protobuf JSON config.","Marshal durations programmatically to avoid placeholder leakage.","Unit-test config files through serviceconfig.Duration before deploy."],"tags":["grpc","service-config","duration","json-parsing","validation"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}