{"record":{"id":"ac5173c48b2bba16","repo":"grpc/grpc-go","slug":"malformed-duration-q-missing-seconds-unit","errorCode":null,"errorMessage":"malformed duration %q: missing seconds unit","messagePattern":"malformed duration %q: missing seconds unit","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/serviceconfig/duration.go","lineNumber":67,"sourceCode":"\t}\n\n\t// Generated output always contains 0, 3, 6, or 9 fractional digits,\n\t// depending on required precision.\n\tstr := fmt.Sprintf(\"%s%d.%09d\", sign, sec, ns)\n\tstr = strings.TrimSuffix(str, \"000\")\n\tstr = strings.TrimSuffix(str, \"000\")\n\tstr = strings.TrimSuffix(str, \".000\")\n\treturn []byte(fmt.Sprintf(\"\\\"%ss\\\"\", str)), nil\n}\n\n// UnmarshalJSON unmarshals b as a duration JSON string into d.\nfunc (d *Duration) UnmarshalJSON(b []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(b, &s); err != nil {\n\t\treturn err\n\t}\n\tif !strings.HasSuffix(s, \"s\") {\n\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)","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/serviceconfig/duration.go#L49-L85","documentation":"serviceconfig.Duration.UnmarshalJSON (duration.go:60-68) parses a protobuf-JSON duration string, which must end with a single 's' (seconds unit), e.g. \"1.5s\", \"0.250s\", \"-3s\". If the suffix is missing, the string is rejected. This type backs gRPC service-config duration fields (timeouts, hedging, retry backoff).","triggerScenarios":"Triggered when JSON unmarshaling into a serviceconfig.Duration (or any struct embedding it) receives a string without a trailing 's'. Examples: \"100ms\", \"5\", \"1.5\" — none end in 's'.","commonSituations":"Hand-written service config JSON uses Go time-string syntax (\"500ms\") instead of protobuf-JSON syntax (\"0.500s\"), a config field is set as a bare number, or a translation layer omitted the unit.","solutions":["Express durations in protobuf-JSON form with a trailing 's': \"0.500s\", \"3s\", \"1.5s\".","Convert from Go time strings programmatically before serializing: use time.ParseDuration then format as seconds with up to 9 fractional digits + 's'.","Validate config files with a JSON schema that enforces the '^-?\\d+(\\.\\d{1,9})?s$' shape."],"exampleFix":"// before (service config JSON):\n//   { \"retryPolicy\": { \"initialBackoff\": \"500ms\" } }\n//   // error: malformed duration \"500ms\": missing seconds unit\n\n// after:\n//   { \"retryPolicy\": { \"initialBackoff\": \"0.500s\" } }","handlingStrategy":"validation","validationCode":"package main\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"time\"\n)\n\nvar durationRE = regexp.MustCompile(`^-?\\d+(\\.\\d{1,9})?s$`)\n\nfunc formatDuration(d time.Duration) string {\n\t// Produce a protobuf-JSON duration string.\n\treturn time.Duration(d).String() // then convert below\n}\n\nfunc validateDurationJSON(s string) error {\n\tif !durationRE.MatchString(s) {\n\t\treturn fmt.Errorf(\"duration %q must end with 's' and have <=9 fractional digits\", s)\n\t}\n\treturn nil\n}\n\nfunc _unused() { _ = formatDuration }\n\n// func main() { _ = validateDurationJSON(\"0.5s\") }","typeGuard":null,"tryCatchPattern":"// JSON unmarshal into serviceconfig.Duration returns this error.\n//\n//   var d serviceconfig.Duration\n//   if err := json.Unmarshal([]byte(raw), &d); err != nil {\n//       if strings.Contains(err.Error(), \"missing seconds unit\") {\n//           // append the trailing 's' and retry, or surface to the user.\n//       }\n//   }","preventionTips":["Always use the 'NN.NNs' (seconds) form in service config JSON, not 'NNms'.","Generate service config from typed structs and use Duration.MarshalJSON rather than hand-writing strings.","Validate config files against a schema with regex '^-?\\d+(\\.\\d{1,9})?s$'."],"tags":["serviceconfig","duration","json","parsing","configuration","grpc"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}