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 }