Skip to content

Commit f85cf5c

Browse files
authored
fix: No data for format=dot queries, should not result in a 500 (#4675)
1 parent f213eb9 commit f85cf5c

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

pkg/querier/http.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ func (q *QueryHandlers) Render(w http.ResponseWriter, req *http.Request) {
171171
httputil.Error(w, connect.NewError(connect.CodeInternal, err))
172172
return
173173
}
174+
// Check if profile has any data - return empty string if no data
175+
if resp.Msg == nil || len(resp.Msg.Sample) == 0 {
176+
w.Header().Set("Content-Type", "text/plain")
177+
w.WriteHeader(http.StatusOK)
178+
return
179+
}
174180
if err = pprofToDotProfile(w, resp.Msg, int(dotProfileMaxNodes)); err != nil {
175181
httputil.Error(w, connect.NewError(connect.CodeInternal, err))
176182
}

pkg/querier/http_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package querier
22

33
import (
4+
"context"
45
"fmt"
56
"net/http"
7+
"net/http/httptest"
68
"net/url"
79
"testing"
810
"time"
911

12+
"connectrpc.com/connect"
1013
"github.com/prometheus/common/model"
1114
"github.com/stretchr/testify/require"
1215

16+
profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
17+
querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1"
1318
typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
1419
)
1520

@@ -40,3 +45,91 @@ func Test_ParseQuery(t *testing.T) {
4045

4146
require.Equal(t, `{foo="bar",bar=~"buzz"}`, queryRequest.LabelSelector)
4247
}
48+
49+
// mockQuerierClient is a mock implementation of QuerierServiceClient
50+
type mockQuerierClient struct {
51+
selectMergeProfileFunc func(context.Context, *connect.Request[querierv1.SelectMergeProfileRequest]) (*connect.Response[profilev1.Profile], error)
52+
}
53+
54+
func (m *mockQuerierClient) ProfileTypes(context.Context, *connect.Request[querierv1.ProfileTypesRequest]) (*connect.Response[querierv1.ProfileTypesResponse], error) {
55+
return nil, nil
56+
}
57+
58+
func (m *mockQuerierClient) LabelValues(context.Context, *connect.Request[typesv1.LabelValuesRequest]) (*connect.Response[typesv1.LabelValuesResponse], error) {
59+
return nil, nil
60+
}
61+
62+
func (m *mockQuerierClient) LabelNames(context.Context, *connect.Request[typesv1.LabelNamesRequest]) (*connect.Response[typesv1.LabelNamesResponse], error) {
63+
return nil, nil
64+
}
65+
66+
func (m *mockQuerierClient) Series(context.Context, *connect.Request[querierv1.SeriesRequest]) (*connect.Response[querierv1.SeriesResponse], error) {
67+
return nil, nil
68+
}
69+
70+
func (m *mockQuerierClient) SelectMergeStacktraces(context.Context, *connect.Request[querierv1.SelectMergeStacktracesRequest]) (*connect.Response[querierv1.SelectMergeStacktracesResponse], error) {
71+
return nil, nil
72+
}
73+
74+
func (m *mockQuerierClient) SelectMergeSpanProfile(context.Context, *connect.Request[querierv1.SelectMergeSpanProfileRequest]) (*connect.Response[querierv1.SelectMergeSpanProfileResponse], error) {
75+
return nil, nil
76+
}
77+
78+
func (m *mockQuerierClient) SelectMergeProfile(ctx context.Context, req *connect.Request[querierv1.SelectMergeProfileRequest]) (*connect.Response[profilev1.Profile], error) {
79+
if m.selectMergeProfileFunc != nil {
80+
return m.selectMergeProfileFunc(ctx, req)
81+
}
82+
return nil, nil
83+
}
84+
85+
func (m *mockQuerierClient) SelectSeries(context.Context, *connect.Request[querierv1.SelectSeriesRequest]) (*connect.Response[querierv1.SelectSeriesResponse], error) {
86+
return nil, nil
87+
}
88+
89+
func (m *mockQuerierClient) Diff(context.Context, *connect.Request[querierv1.DiffRequest]) (*connect.Response[querierv1.DiffResponse], error) {
90+
return nil, nil
91+
}
92+
93+
func (m *mockQuerierClient) GetProfileStats(context.Context, *connect.Request[typesv1.GetProfileStatsRequest]) (*connect.Response[typesv1.GetProfileStatsResponse], error) {
94+
return nil, nil
95+
}
96+
97+
func (m *mockQuerierClient) AnalyzeQuery(context.Context, *connect.Request[querierv1.AnalyzeQueryRequest]) (*connect.Response[querierv1.AnalyzeQueryResponse], error) {
98+
return nil, nil
99+
}
100+
101+
func Test_RenderDotFormatEmptyProfile(t *testing.T) {
102+
// Create a mock client that returns an empty profile
103+
mockClient := &mockQuerierClient{
104+
selectMergeProfileFunc: func(ctx context.Context, req *connect.Request[querierv1.SelectMergeProfileRequest]) (*connect.Response[profilev1.Profile], error) {
105+
// Return an empty profile (no samples)
106+
return connect.NewResponse(&profilev1.Profile{
107+
Sample: []*profilev1.Sample{}, // Empty samples
108+
}), nil
109+
},
110+
}
111+
112+
handlers := NewHTTPHandlers(mockClient)
113+
114+
// Create a request with format=dot
115+
q := url.Values{
116+
"query": []string{`memory:alloc_space:bytes:space:bytes{}`},
117+
"from": []string{"now-1h"},
118+
"until": []string{"now"},
119+
"format": []string{"dot"},
120+
}
121+
122+
req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost/render?%s", q.Encode()), nil)
123+
require.NoError(t, err)
124+
125+
// Create a response recorder
126+
rr := httptest.NewRecorder()
127+
128+
// Call the handler
129+
handlers.Render(rr, req)
130+
131+
// Verify we get a 200 OK with empty body instead of 500 (Internal Server Error)
132+
require.Equal(t, http.StatusOK, rr.Code, "Expected 200 OK for empty profile, got %d", rr.Code)
133+
require.Equal(t, "", rr.Body.String(), "Expected empty body for empty profile")
134+
require.Equal(t, "text/plain", rr.Header().Get("Content-Type"), "Expected text/plain content type")
135+
}

0 commit comments

Comments
 (0)