From 5b6a8e6001f17fdf1929cce462493e4254e747e9 Mon Sep 17 00:00:00 2001 From: Bruce MacDonald Date: Mon, 1 Dec 2025 15:10:16 -0800 Subject: [PATCH] api/client: handle non-json streaming errors (#13007) While processing the response stream during a chat or generation if an error is occurred it is parsed and returned to the user. The issue with the existing code is that this assumed the response would be valid JSON, which is not a safe assumption and caused cryptic error messages to be displayed due to parsing failures: `invalid character 'i' looking for beginning of value` This change updates the stream function to return the raw error string if it cant be parsed as JSON. This should help with debugging issues by making sure the actual error reaches the user. --- api/client.go | 9 +++++- api/client_test.go | 78 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/api/client.go b/api/client.go index 0d4c97ba..9a8f89e4 100644 --- a/api/client.go +++ b/api/client.go @@ -226,7 +226,14 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f bts := scanner.Bytes() if err := json.Unmarshal(bts, &errorResponse); err != nil { - return fmt.Errorf("unmarshal: %w", err) + if response.StatusCode >= http.StatusBadRequest { + return StatusError{ + StatusCode: response.StatusCode, + Status: response.Status, + ErrorMessage: string(bts), + } + } + return errors.New(string(bts)) } if response.StatusCode == http.StatusUnauthorized { diff --git a/api/client_test.go b/api/client_test.go index f0034e02..827e41d9 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -55,6 +55,7 @@ func TestClientFromEnvironment(t *testing.T) { type testError struct { message string statusCode int + raw bool // if true, write message as-is instead of JSON encoding } func (e testError) Error() string { @@ -111,6 +112,20 @@ func TestClientStream(t *testing.T) { }, }, }, + { + name: "plain text error response", + responses: []any{ + "internal server error", + }, + wantErr: "internal server error", + }, + { + name: "HTML error page", + responses: []any{ + "404 Not Found", + }, + wantErr: "404 Not Found", + }, } for _, tc := range testCases { @@ -135,6 +150,12 @@ func TestClientStream(t *testing.T) { return } + if str, ok := resp.(string); ok { + fmt.Fprintln(w, str) + flusher.Flush() + continue + } + if err := json.NewEncoder(w).Encode(resp); err != nil { t.Fatalf("failed to encode response: %v", err) } @@ -173,9 +194,10 @@ func TestClientStream(t *testing.T) { func TestClientDo(t *testing.T) { testCases := []struct { - name string - response any - wantErr string + name string + response any + wantErr string + wantStatusCode int }{ { name: "immediate error response", @@ -183,7 +205,8 @@ func TestClientDo(t *testing.T) { message: "test error message", statusCode: http.StatusBadRequest, }, - wantErr: "test error message", + wantErr: "test error message", + wantStatusCode: http.StatusBadRequest, }, { name: "server error response", @@ -191,7 +214,8 @@ func TestClientDo(t *testing.T) { message: "internal error", statusCode: http.StatusInternalServerError, }, - wantErr: "internal error", + wantErr: "internal error", + wantStatusCode: http.StatusInternalServerError, }, { name: "successful response", @@ -203,6 +227,26 @@ func TestClientDo(t *testing.T) { Success: true, }, }, + { + name: "plain text error response", + response: testError{ + message: "internal server error", + statusCode: http.StatusInternalServerError, + raw: true, + }, + wantErr: "internal server error", + wantStatusCode: http.StatusInternalServerError, + }, + { + name: "HTML error page", + response: testError{ + message: "404 Not Found", + statusCode: http.StatusNotFound, + raw: true, + }, + wantErr: "404 Not Found", + wantStatusCode: http.StatusNotFound, + }, } for _, tc := range testCases { @@ -210,11 +254,16 @@ func TestClientDo(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if errResp, ok := tc.response.(testError); ok { w.WriteHeader(errResp.statusCode) - err := json.NewEncoder(w).Encode(map[string]string{ - "error": errResp.message, - }) - if err != nil { - t.Fatal("failed to encode error response:", err) + if !errResp.raw { + err := json.NewEncoder(w).Encode(map[string]string{ + "error": errResp.message, + }) + if err != nil { + t.Fatal("failed to encode error response:", err) + } + } else { + // Write raw message (simulates non-JSON error responses) + fmt.Fprint(w, errResp.message) } return } @@ -241,6 +290,15 @@ func TestClientDo(t *testing.T) { if err.Error() != tc.wantErr { t.Errorf("error message mismatch: got %q, want %q", err.Error(), tc.wantErr) } + if tc.wantStatusCode != 0 { + if statusErr, ok := err.(StatusError); ok { + if statusErr.StatusCode != tc.wantStatusCode { + t.Errorf("status code mismatch: got %d, want %d", statusErr.StatusCode, tc.wantStatusCode) + } + } else { + t.Errorf("expected StatusError, got %T", err) + } + } return }