From 4c2929900a688bcac42be99e9c0fc4d2b44eb6e8 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Wed, 29 Apr 2026 15:10:47 +0300 Subject: [PATCH] fix: handle top-level error responses in multi search - detect and return HTTPError when API responds with code/error in body - add tests for Perform and PerformWithContentType top-level errors --- typesense/multi_search.go | 25 ++++++++++++ typesense/multi_search_test.go | 61 +++++++++++++++++++++++++++++ typesense/test/multi_search_test.go | 27 +++++++++++++ 3 files changed, 113 insertions(+) diff --git a/typesense/multi_search.go b/typesense/multi_search.go index 19e1cf7c..980e1138 100644 --- a/typesense/multi_search.go +++ b/typesense/multi_search.go @@ -23,6 +23,9 @@ func (m *multiSearch) Perform(ctx context.Context, commonSearchParams *api.Multi if err != nil { return nil, err } + if err := multiSearchTopLevelError(response); err != nil { + return nil, err + } if response.JSON200 == nil { return nil, &HTTPError{Status: response.StatusCode(), Body: response.Body} } @@ -41,8 +44,30 @@ func (m *multiSearch) PerformWithContentType(ctx context.Context, commonSearchPa if err != nil { return nil, err } + if err := multiSearchTopLevelError(response); err != nil { + return nil, err + } if response.Body == nil { return nil, &HTTPError{Status: response.StatusCode(), Body: response.Body} } return response, nil } + +func multiSearchTopLevelError(response *api.MultiSearchResponse) error { + if response == nil || len(response.Body) == 0 { + return nil + } + + var errorResponse struct { + Code *int `json:"code"` + Error *string `json:"error"` + } + if err := json.Unmarshal(response.Body, &errorResponse); err != nil { + return nil + } + if errorResponse.Code == nil || errorResponse.Error == nil { + return nil + } + + return &HTTPError{Status: *errorResponse.Code, Body: response.Body} +} diff --git a/typesense/multi_search_test.go b/typesense/multi_search_test.go index 27638b11..a050d32f 100644 --- a/typesense/multi_search_test.go +++ b/typesense/multi_search_test.go @@ -277,6 +277,67 @@ func TestMultiSearchOnHttpStatusErrorCodeReturnsError(t *testing.T) { assert.NotNil(t, err) } +func TestMultiSearchOnTopLevelErrorResponseReturnsError(t *testing.T) { + expectedParams := newMultiSearchParams() + expectedBody := newMultiSearchBodyParams() + responseBody := []byte(`{"code":404,"error":"` + "`non-existent`" + ` collection not found."}`) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockAPIClient := mocks.NewMockAPIClientInterface(ctrl) + + mockAPIClient.EXPECT(). + MultiSearchWithResponse(gomock.Not(gomock.Nil()), expectedParams, api.MultiSearchJSONRequestBody(expectedBody)). + Return(&api.MultiSearchResponse{ + HTTPResponse: &http.Response{ + StatusCode: http.StatusOK, + }, + Body: responseBody, + JSON200: &api.MultiSearchResult{ + Results: nil, + }, + }, nil).Times(1) + + client := NewClient(WithAPIClient(mockAPIClient)) + params := newMultiSearchParams() + _, err := client.MultiSearch.Perform(context.Background(), params, newMultiSearchBodyParams()) + + var httpErr *HTTPError + assert.ErrorAs(t, err, &httpErr) + assert.Equal(t, 404, httpErr.Status) + assert.Equal(t, responseBody, httpErr.Body) +} + +func TestMultiSearchWithContentTypeOnTopLevelErrorResponseReturnsError(t *testing.T) { + expectedParams := newMultiSearchParams() + expectedBody := newMultiSearchBodyParams() + expectedContentType := "application/x-json-stream" + responseBody := []byte(`{"code":422,"error":"Only upto 250 hits can be fetched per page."}`) + expectedReqBody, err := json.Marshal(expectedBody) + assert.Nil(t, err) + reqReader := bytes.NewReader(expectedReqBody) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockAPIClient := mocks.NewMockAPIClientInterface(ctrl) + + mockAPIClient.EXPECT(). + MultiSearchWithBodyWithResponse(gomock.Not(gomock.Nil()), expectedParams, expectedContentType, reqReader). + Return(&api.MultiSearchResponse{ + HTTPResponse: &http.Response{ + StatusCode: http.StatusOK, + }, + Body: responseBody, + }, nil).Times(1) + + client := NewClient(WithAPIClient(mockAPIClient)) + params := newMultiSearchParams() + _, err = client.MultiSearch.PerformWithContentType(context.Background(), params, newMultiSearchBodyParams(), expectedContentType) + + var httpErr *HTTPError + assert.ErrorAs(t, err, &httpErr) + assert.Equal(t, 422, httpErr.Status) + assert.Equal(t, responseBody, httpErr.Body) +} + func TestMultiSearchOnApiClientError(t *testing.T) { expectedParams := newMultiSearchParams() expectedBody := newMultiSearchBodyParams() diff --git a/typesense/test/multi_search_test.go b/typesense/test/multi_search_test.go index 90912f4e..7d7f9fe2 100644 --- a/typesense/test/multi_search_test.go +++ b/typesense/test/multi_search_test.go @@ -5,9 +5,11 @@ package test import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" + "github.com/typesense/typesense-go/v4/typesense" "github.com/typesense/typesense-go/v4/typesense/api" "github.com/typesense/typesense-go/v4/typesense/api/pointer" ) @@ -86,6 +88,31 @@ func TestMultiSearch(t *testing.T) { } } +func TestMultiSearchUnionTopLevelErrorReturnsHTTPError(t *testing.T) { + _, err := typesenseClient.MultiSearch.Perform( + context.Background(), + &api.MultiSearchParams{ + Q: pointer.String("query"), + }, + api.MultiSearchSearchesParameter{ + Searches: []api.MultiSearchCollectionParameters{ + { + Collection: pointer.String("non-existent"), + }, + }, + Union: pointer.True(), + }, + ) + require.Error(t, err) + + var httpErr *typesense.HTTPError + require.True(t, errors.As(err, &httpErr)) + require.Equal(t, 404, httpErr.Status) + require.Contains(t, string(httpErr.Body), `"code"`) + require.Contains(t, string(httpErr.Body), `"error"`) + require.Contains(t, string(httpErr.Body), "collection not found") +} + func TestMultiSearchGroupBy(t *testing.T) { collectionName1 := createNewCollection(t, "companies") documents := []interface{}{