{"record":{"id":"7557993bea749abe","repo":"glanceapp/glance","slug":"body-must-be-a-string-when-body-type-is-string","errorCode":null,"errorMessage":"body must be a string when body-type is 'string'","messagePattern":"body must be a string when body-type is 'string'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/glance/widget-custom-api.go","lineNumber":163,"sourceCode":"\t\t\treq.BodyType = \"json\"\n\t\t}\n\n\t\tif req.BodyType != \"json\" && req.BodyType != \"string\" {\n\t\t\treturn errors.New(\"invalid body type, must be either 'json' or 'string'\")\n\t\t}\n\n\t\tswitch req.BodyType {\n\t\tcase \"json\":\n\t\t\tencoded, err := json.Marshal(req.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"marshaling body: %v\", err)\n\t\t\t}\n\n\t\t\treq.bodyReader = bytes.NewReader(encoded)\n\t\tcase \"string\":\n\t\t\tbodyAsString, ok := req.Body.(string)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"body must be a string when body-type is 'string'\")\n\t\t\t}\n\n\t\t\treq.bodyReader = strings.NewReader(bodyAsString)\n\t\t}\n\n\t} else if req.Method == \"\" {\n\t\treq.Method = http.MethodGet\n\t}\n\n\thttpReq, err := http.NewRequest(strings.ToUpper(req.Method), req.URL, req.bodyReader)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(req.Parameters) > 0 {\n\t\thttpReq.URL.RawQuery = req.Parameters.toQueryString()\n\t}\n","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/glanceapp/glance/blob/91324e8de762702e97b0ac5c8e36271d644d8642/internal/glance/widget-custom-api.go#L145-L181","documentation":"The Custom API widget's .JSON option helper marshals the option value with encoding/json and panics with 'marshaling %s: %v' if json.Marshal fails. The template engine recovers the panic into a render error. Standard YAML-sourced values (strings, numbers, bools, maps, lists) always marshal; failures come from values JSON cannot represent.","triggerScenarios":"An option value that encoding/json rejects — most notably float NaN/Infinity (YAML .nan or .inf) — is passed to .JSON. marshal fails with 'json: unsupported value: NaN' and the panic fires.","commonSituations":"YAML config using .nan/.inf placeholders for 'unknown' numbers that then get embedded into a JSON request body via .JSON; values computed in a template pipeline that produce non-finite floats; options populated from upstream data containing non-finite numbers.","solutions":["Replace .nan/.inf option values with real numbers, strings, or null and handle absence in the template","If a number may be unknown, store it as a string or omit the key and use .FloatOr default","Sanitize computed values with {{ if ... }} guards before passing to .JSON"],"exampleFix":"// before (glance.yml)\noptions:\n  ratio: .nan\n# template: {{ .JSON \"ratio\" }}\n// after\noptions:\n  ratio: 0.0\n# or omit the key and use a default in the template","handlingStrategy":"validation","validationCode":"if math.IsNaN(f) || math.IsInf(f, 0) {\n    return errors.New(\"value not representable in JSON\")\n}\nb, err := json.Marshal(f)","typeGuard":"func jsonMarshalable(v any) bool {\n\t_, err := json.Marshal(v)\n\treturn err == nil\n}","tryCatchPattern":"Wrap the .JSON call site in template logic that only runs for known-safe values; in Go code, pre-check non-finite floats with math.IsNaN/IsInf before marshaling, since encoding/json has no option to tolerate them.","preventionTips":["Never use .nan/.inf in YAML options that flow into .JSON","Represent unknown numbers as null, a string, or omit the key and use .FloatOr defaults","Test custom-api templates against the exact options payload used in production"],"tags":["go","custom-api","json","yaml","panic"],"backgroundTag":null,"analyzedSha":"91324e8de762702e97b0ac5c8e36271d644d8642","analyzedAt":"2026-08-15T14:12:54.279Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}