{"record":{"id":"5723ad951ff61a8e","repo":"wavetermdev/waveterm","slug":"failed-to-decode-tsunami-response-w","errorCode":null,"errorMessage":"failed to decode tsunami response: %w","messagePattern":"failed to decode tsunami response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/aiusechat/tools_tsunami.go","lineNumber":75,"sourceCode":"\n\t\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to make request to tsunami: %w\", err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn nil, fmt.Errorf(\"tsunami returned status %d\", resp.StatusCode)\n\t\t}\n\n\t\tvar result any\n\t\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to decode tsunami response: %w\", err)\n\t\t}\n\n\t\treturn result, nil\n\t}\n}\n\nfunc makeTsunamiPostCallback(status *blockcontroller.BlockControllerRuntimeStatus, apiPath string) func(any, *uctypes.UIMessageDataToolUse) (any, error) {\n\treturn func(input any, toolUseData *uctypes.UIMessageDataToolUse) (any, error) {\n\t\tif status.TsunamiPort == 0 {\n\t\t\treturn nil, fmt.Errorf(\"tsunami port not available\")\n\t\t}\n\n\t\turl := fmt.Sprintf(\"http://localhost:%d%s\", status.TsunamiPort, apiPath)\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\n\t\tvar reqBody []byte","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/aiusechat/tools_tsunami.go#L57-L93","documentation":"This error wraps a JSON decoding failure when the AI tool callback reads the response body from a local Tsunami widget's HTTP API (GET /api/data or /api/config). The Tsunami server returned HTTP 200, but its body was not valid JSON (or was empty/truncated), so json.Decoder.Decode failed. The underlying decoder error is preserved via %w so the root cause (e.g. 'unexpected end of JSON input' or 'invalid character') is visible.","triggerScenarios":"Calling the tsunami_getdata_<blockid> or tsunami_getconfig_<blockid> tool while the widget's HTTP endpoint responds 200 with a non-JSON body: empty response, HTML error page served with status 200, truncated body, or the widget writing plain text.","commonSituations":"A Tsunami widget version that returns plain text or an empty body on /api/data; a reverse proxy or dev server intercepting the port and returning an HTML page; the widget process crashing mid-response; Content-Type mismatches after widget framework upgrades.","solutions":["Check the wrapped error (%w) to see the exact JSON syntax problem and dump the raw body with curl http://localhost:<TsunamiPort>/api/data to inspect what the widget actually returns.","Verify the widget is a genuine Tsunami widget serving JSON and that TsunamiPort in the block runtime status points at the right process (not another service bound to the same port).","Update or restart the Tsunami widget so its /api/data and /api/config endpoints emit valid JSON with 200.","If the endpoint can legitimately return an empty body, fix the widget/server to return 'null' or '{}' so decoding succeeds."],"exampleFix":"// before: client fails on empty/non-JSON 200 body from widget; // after: widget side always emits JSON, e.g. w.Header().Set(\"Content-Type\", \"application/json\"); json.NewEncoder(w).Encode(data) (or write \"null\" for empty results)","handlingStrategy":"try-catch","validationCode":"resp, err := http.Get(fmt.Sprintf(\"http://localhost:%d/api/data\", port)); if err != nil || resp.StatusCode != 200 { /* skip tool call */ }; if !strings.Contains(resp.Header.Get(\"Content-Type\"), \"json\") { /* likely non-JSON body; skip */ }","typeGuard":"func isJSONContentType(ct string) bool { return strings.Contains(strings.ToLower(ct), \"application/json\") }","tryCatchPattern":"result, err := toolCallback(nil, toolUseData); if err != nil { var jsonErr *json.SyntaxError; if errors.As(err, &jsonErr) { /* invalid JSON from widget: dump raw body, restart/upgrade widget */ }; return fmt.Errorf(\"tsunami getdata failed: %w\", err) }","preventionTips":["Always inspect the wrapped error with errors.As for *json.SyntaxError/*json.UnmarshalTypeError to distinguish malformed vs truncated bodies.","Curl the widget endpoint manually to confirm it returns JSON with Content-Type: application/json.","Keep Tsunami widgets updated so /api/data and /api/config always emit valid JSON.","Treat empty 200 bodies as failures and surface them early in tool diagnostics."],"tags":["json","http","decoding","tsunami"],"backgroundTag":"invalid-json-response","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}