{"record":{"id":"e2a33d383c3cd8de","repo":"grpc/grpc-go","slug":"malformed-duration-q-too-many-digits-after-decim","errorCode":null,"errorMessage":"malformed duration %q: too many digits after decimal","messagePattern":"malformed duration %q: too many digits after decimal","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/serviceconfig/duration.go","lineNumber":96,"sourceCode":"\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\n\t\t}\n\t\thasDigits = true\n\t}\n\tif !hasDigits {\n\t\treturn fmt.Errorf(\"malformed duration %q: contains no numbers\", s)\n\t}\n\n\tif neg {\n\t\tsec *= -1\n\t\tns *= -1\n\t}","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/internal/serviceconfig/duration.go#L78-L114","documentation":"Returned by Duration.UnmarshalJSON when the fractional (post-decimal) portion of the duration string has more than 9 digits. Nanosecond precision is the maximum the protobuf Duration spec and Go's time.Duration support, so additional digits are rejected rather than silently truncated.","triggerScenarios":"A value like \"1.1234567890s\" (10 fractional digits) triggers `if len(ss[1]) > 9` at duration.go:95. Typical when a high-precision timestamp or arbitrary float is written as a duration.","commonSituations":"Tooling that emits full float64 precision; copy/paste of a nanosecond timestamp with sub-ns digits; localized formatting that pads decimals.","solutions":["Trim the fractional part to at most 9 digits, e.g. \"1.123456789s\".","Round the source value to nanosecond precision before serializing.","Generate durations via serviceconfig.Duration.MarshalJSON which always emits 3/6/9 digits."],"exampleFix":"// before\n{\"timeout\": \"0.1234567890s\"}\n\n// after\n{\"timeout\": \"0.123456789s\"}","handlingStrategy":"validation","validationCode":"func validateProtoDuration(s string) error {\n    body := strings.TrimSuffix(s, \"s\")\n    parts := strings.SplitN(body, \".\", 2)\n    if len(parts) == 2 && len(parts[1]) > 9 {\n        return fmt.Errorf(\"too many fractional digits in %q\", s)\n    }\n    var d serviceconfig.Duration\n    return d.UnmarshalJSON([]byte(`\"` + s + `\"`))\n}","typeGuard":"func fractionalDigitsAtMost9(s string) bool {\n    body := strings.TrimSuffix(s, \"s\")\n    parts := strings.SplitN(body, \".\", 2)\n    return len(parts) != 2 || len(parts[1]) <= 9\n}","tryCatchPattern":null,"preventionTips":["Round source values to nanosecond precision before serializing to protobuf JSON.","Generate durations through serviceconfig.Duration.MarshalJSON to guarantee ≤9 digits.","Lint config for over-precise fractional values."],"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"}