{"record":{"id":"ef2f5ce4f44fdb95","repo":"grpc/grpc-go","slug":"malformed-duration-q-contains-no-numbers","errorCode":null,"errorMessage":"malformed duration %q: contains no numbers","messagePattern":"malformed duration %q: contains no numbers","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/serviceconfig/duration.go","lineNumber":108,"sourceCode":"\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}\n\n\t// Maximum/minimum seconds/nanoseconds representable by Go's time.Duration.\n\tconst maxSeconds = math.MaxInt64 / int64(time.Second)\n\tconst maxNanosAtMaxSeconds = math.MaxInt64 % int64(time.Second)\n\tconst minSeconds = math.MinInt64 / int64(time.Second)\n\tconst minNanosAtMinSeconds = math.MinInt64 % int64(time.Second)\n\n\tif sec > maxSeconds || (sec == maxSeconds && ns >= maxNanosAtMaxSeconds) {\n\t\t*d = Duration(math.MaxInt64)\n\t} else if sec < minSeconds || (sec == minSeconds && ns <= minNanosAtMinSeconds) {\n\t\t*d = Duration(math.MinInt64)\n\t} else {","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/internal/serviceconfig/duration.go#L90-L126","documentation":"This error occurs in Duration.UnmarshalJSON when neither the whole-seconds part nor the fractional part of the duration string contains any digits. The parser requires at least one numeric component. For example, a bare \".s\" or just \"s\" triggers this because hasDigits stays false through both parsing branches.","triggerScenarios":"A JSON field unmarshaled into serviceconfig.Duration contains a value like \"s\", \".s\", \"-.s\", or an empty-ish string that ends with 's' but has no digits before or after the decimal point.","commonSituations":"Missing/blank duration values in xDS service config, template substitution errors producing empty duration strings, or deserialization of a struct with zero-value or uninitialized duration fields serialized as bare units.","solutions":["Provide a complete duration string with at least one numeric digit, e.g. \"0s\", \"1s\", or \"0.5s\".","Audit the source generating the JSON (control plane, config template) to ensure duration fields are never empty.","Add pre-validation on the JSON payload before unmarshaling into serviceconfig types."],"exampleFix":"// before (broken): no digits at all\n\"timeout\": \"s\"\n\n// after (valid): at least the seconds field\n\"timeout\": \"0s\"","handlingStrategy":"validation","validationCode":"// Ensure a duration string has at least one digit before unmarshaling\nfunc hasDurationDigit(s string) bool {\n    if !strings.HasSuffix(s, \"s\") { return false }\n    body := strings.TrimSuffix(s, \"s\")\n    if len(body) > 0 && body[0] == '-' { body = body[1:] }\n    parts := strings.SplitN(body, \".\", 2)\n    return parts[0] != \"\" || (len(parts) == 2 && parts[1] != \"\")\n}","typeGuard":null,"tryCatchPattern":"var d serviceconfig.Duration\nif err := json.Unmarshal(raw, &d); err != nil {\n    if strings.Contains(err.Error(), \"contains no numbers\") {\n        d = serviceconfig.Duration(0) // default\n    } else { return err }\n}","preventionTips":["Never use empty strings for duration fields in config.","Use \"0s\" as the minimal valid representation of zero duration.","Validate config payloads from external sources (xDS, files) before applying."],"tags":["service-config","duration","json-parsing","configuration","validation"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}