Skip to content

Commit 64eea53

Browse files
authored
Error source HTTP client middleware (#1106)
Adds a new default Error source HTTP client middleware that will wrap any HTTP response error in a downstream error if identified as a downstream HTTP error. Moves backend.ErrorSource related types to a new experimental package status while keeping backward compatibility using type aliases in the backend package referencing the new status package.
1 parent 6e35428 commit 64eea53

14 files changed

+588
-269
lines changed

backend/data_adapter.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77

8+
"github.com/grafana/grafana-plugin-sdk-go/experimental/status"
89
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
910
)
1011

@@ -29,9 +30,9 @@ func (a *dataSDKAdapter) QueryData(ctx context.Context, req *pluginv2.QueryDataR
2930
var innerErr error
3031
resp, innerErr = a.queryDataHandler.QueryData(ctx, parsedReq)
3132

32-
status := RequestStatusFromQueryDataResponse(resp, innerErr)
33+
requestStatus := RequestStatusFromQueryDataResponse(resp, innerErr)
3334
if innerErr != nil {
34-
return status, innerErr
35+
return requestStatus, innerErr
3536
} else if resp == nil {
3637
return RequestStatusError, errors.New("both response and error are nil, but one must be provided")
3738
}
@@ -41,7 +42,7 @@ func (a *dataSDKAdapter) QueryData(ctx context.Context, req *pluginv2.QueryDataR
4142
// and if there's no plugin error
4243
var hasPluginError, hasDownstreamError bool
4344
for refID, r := range resp.Responses {
44-
if r.Error == nil || isCancelledError(r.Error) {
45+
if r.Error == nil || status.IsCancelledError(r.Error) {
4546
continue
4647
}
4748

@@ -81,7 +82,7 @@ func (a *dataSDKAdapter) QueryData(ctx context.Context, req *pluginv2.QueryDataR
8182
}
8283
}
8384

84-
return status, nil
85+
return requestStatus, nil
8586
})
8687
if err != nil {
8788
return nil, err

backend/error_source.go

Lines changed: 22 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2,144 +2,71 @@ package backend
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
7-
"net/http"
6+
7+
"github.com/grafana/grafana-plugin-sdk-go/experimental/status"
88
)
99

1010
// ErrorSource type defines the source of the error
11-
type ErrorSource string
11+
type ErrorSource = status.Source
1212

1313
const (
1414
// ErrorSourcePlugin error originates from plugin.
15-
ErrorSourcePlugin ErrorSource = "plugin"
15+
ErrorSourcePlugin = status.SourcePlugin
1616

1717
// ErrorSourceDownstream error originates from downstream service.
18-
ErrorSourceDownstream ErrorSource = "downstream"
18+
ErrorSourceDownstream = status.SourceDownstream
1919

2020
// DefaultErrorSource is the default [ErrorSource] that should be used when it is not explicitly set.
21-
DefaultErrorSource ErrorSource = ErrorSourcePlugin
21+
DefaultErrorSource = status.SourcePlugin
2222
)
2323

24-
func (es ErrorSource) IsValid() bool {
25-
return es == ErrorSourceDownstream || es == ErrorSourcePlugin
26-
}
27-
28-
// ErrorSourceFromStatus returns an [ErrorSource] based on provided HTTP status code.
24+
// ErrorSourceFromHTTPStatus returns an [ErrorSource] based on provided HTTP status code.
2925
func ErrorSourceFromHTTPStatus(statusCode int) ErrorSource {
30-
switch statusCode {
31-
case http.StatusMethodNotAllowed,
32-
http.StatusNotAcceptable,
33-
http.StatusPreconditionFailed,
34-
http.StatusRequestEntityTooLarge,
35-
http.StatusRequestHeaderFieldsTooLarge,
36-
http.StatusRequestURITooLong,
37-
http.StatusExpectationFailed,
38-
http.StatusUpgradeRequired,
39-
http.StatusRequestedRangeNotSatisfiable,
40-
http.StatusNotImplemented:
41-
return ErrorSourcePlugin
42-
}
43-
44-
return ErrorSourceDownstream
45-
}
46-
47-
type errorWithSourceImpl struct {
48-
source ErrorSource
49-
err error
26+
return status.SourceFromHTTPStatus(statusCode)
5027
}
5128

29+
// IsDownstreamError return true if provided error is an error with downstream source or
30+
// a timeout error or a cancelled error.
5231
func IsDownstreamError(err error) bool {
53-
e := errorWithSourceImpl{
54-
source: ErrorSourceDownstream,
55-
}
56-
if errors.Is(err, e) {
57-
return true
58-
}
59-
60-
type errorWithSource interface {
61-
ErrorSource() ErrorSource
62-
}
63-
64-
// nolint:errorlint
65-
if errWithSource, ok := err.(errorWithSource); ok && errWithSource.ErrorSource() == ErrorSourceDownstream {
66-
return true
67-
}
68-
69-
if isHTTPTimeoutError(err) || isCancelledError(err) {
70-
return true
71-
}
32+
return status.IsDownstreamError(err)
33+
}
7234

73-
return false
35+
// IsDownstreamError return true if provided error is an error with downstream source or
36+
// a HTTP timeout error or a cancelled error or a connection reset/refused error or dns not found error.
37+
func IsDownstreamHTTPError(err error) bool {
38+
return status.IsDownstreamHTTPError(err)
7439
}
7540

7641
func DownstreamError(err error) error {
77-
return errorWithSourceImpl{
78-
source: ErrorSourceDownstream,
79-
err: err,
80-
}
42+
return status.DownstreamError(err)
8143
}
8244

8345
func DownstreamErrorf(format string, a ...any) error {
8446
return DownstreamError(fmt.Errorf(format, a...))
8547
}
8648

87-
func (e errorWithSourceImpl) ErrorSource() ErrorSource {
88-
return e.source
89-
}
90-
91-
func (e errorWithSourceImpl) Error() string {
92-
return fmt.Errorf("%s error: %w", e.source, e.err).Error()
93-
}
94-
95-
// Implements the interface used by [errors.Is].
96-
func (e errorWithSourceImpl) Is(err error) bool {
97-
if errWithSource, ok := err.(errorWithSourceImpl); ok {
98-
return errWithSource.ErrorSource() == e.source
99-
}
100-
101-
return false
102-
}
103-
104-
func (e errorWithSourceImpl) Unwrap() error {
105-
return e.err
106-
}
107-
108-
type errorSourceCtxKey struct{}
109-
110-
// errorSourceFromContext returns the error source stored in the context.
111-
// If no error source is stored in the context, [DefaultErrorSource] is returned.
11249
func errorSourceFromContext(ctx context.Context) ErrorSource {
113-
value, ok := ctx.Value(errorSourceCtxKey{}).(*ErrorSource)
114-
if ok {
115-
return *value
116-
}
117-
return DefaultErrorSource
50+
return status.SourceFromContext(ctx)
11851
}
11952

120-
// initErrorSource initialize the status source for the context.
53+
// initErrorSource initialize the error source for the context.
12154
func initErrorSource(ctx context.Context) context.Context {
122-
s := DefaultErrorSource
123-
return context.WithValue(ctx, errorSourceCtxKey{}, &s)
55+
return status.InitSource(ctx)
12456
}
12557

12658
// WithErrorSource mutates the provided context by setting the error source to
12759
// s. If the provided context does not have a error source, the context
12860
// will not be mutated and an error returned. This means that [initErrorSource]
12961
// has to be called before this function.
13062
func WithErrorSource(ctx context.Context, s ErrorSource) error {
131-
v, ok := ctx.Value(errorSourceCtxKey{}).(*ErrorSource)
132-
if !ok {
133-
return errors.New("the provided context does not have a status source")
134-
}
135-
*v = s
136-
return nil
63+
return status.WithSource(ctx, s)
13764
}
13865

13966
// WithDownstreamErrorSource mutates the provided context by setting the error source to
14067
// [ErrorSourceDownstream]. If the provided context does not have a error source, the context
14168
// will not be mutated and an error returned. This means that [initErrorSource] has to be
14269
// called before this function.
14370
func WithDownstreamErrorSource(ctx context.Context) error {
144-
return WithErrorSource(ctx, ErrorSourceDownstream)
71+
return status.WithDownstreamSource(ctx)
14572
}

backend/error_source_test.go

Lines changed: 9 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,18 @@
1-
package backend
1+
package backend_test
22

33
import (
4-
"context"
5-
"errors"
6-
"fmt"
7-
"net"
8-
"os"
94
"testing"
105

11-
"github.com/stretchr/testify/assert"
6+
"github.com/grafana/grafana-plugin-sdk-go/backend"
127
"github.com/stretchr/testify/require"
13-
"google.golang.org/grpc/codes"
14-
"google.golang.org/grpc/status"
158
)
169

1710
func TestErrorSource(t *testing.T) {
18-
var es ErrorSource
19-
require.False(t, es.IsValid())
20-
require.True(t, ErrorSourceDownstream.IsValid())
21-
require.True(t, ErrorSourcePlugin.IsValid())
22-
}
23-
24-
func TestIsDownstreamError(t *testing.T) {
25-
tcs := []struct {
26-
name string
27-
err error
28-
expected bool
29-
}{
30-
{
31-
name: "nil",
32-
err: nil,
33-
expected: false,
34-
},
35-
{
36-
name: "downstream error",
37-
err: DownstreamError(nil),
38-
expected: true,
39-
},
40-
{
41-
name: "timeout network error",
42-
err: newFakeNetworkError(true, false),
43-
expected: true,
44-
},
45-
{
46-
name: "wrapped timeout network error",
47-
err: fmt.Errorf("oh no. err %w", newFakeNetworkError(true, false)),
48-
expected: true,
49-
},
50-
{
51-
name: "temporary timeout network error",
52-
err: newFakeNetworkError(true, true),
53-
expected: true,
54-
},
55-
{
56-
name: "non-timeout network error",
57-
err: newFakeNetworkError(false, false),
58-
expected: false,
59-
},
60-
{
61-
name: "os.ErrDeadlineExceeded",
62-
err: os.ErrDeadlineExceeded,
63-
expected: true,
64-
},
65-
{
66-
name: "os.ErrDeadlineExceeded",
67-
err: fmt.Errorf("error: %w", os.ErrDeadlineExceeded),
68-
expected: true,
69-
},
70-
{
71-
name: "wrapped os.ErrDeadlineExceeded",
72-
err: errors.Join(fmt.Errorf("oh no"), os.ErrDeadlineExceeded),
73-
expected: true,
74-
},
75-
{
76-
name: "other error",
77-
err: fmt.Errorf("other error"),
78-
expected: false,
79-
},
80-
{
81-
name: "context.Canceled",
82-
err: context.Canceled,
83-
expected: true,
84-
},
85-
{
86-
name: "wrapped context.Canceled",
87-
err: fmt.Errorf("error: %w", context.Canceled),
88-
expected: true,
89-
},
90-
{
91-
name: "joined context.Canceled",
92-
err: errors.Join(fmt.Errorf("oh no"), context.Canceled),
93-
expected: true,
94-
},
95-
{
96-
name: "gRPC canceled error",
97-
err: status.Error(codes.Canceled, "canceled"),
98-
expected: true,
99-
},
100-
{
101-
name: "wrapped gRPC canceled error",
102-
err: fmt.Errorf("error: %w", status.Error(codes.Canceled, "canceled")),
103-
expected: true,
104-
},
105-
{
106-
name: "joined gRPC canceled error",
107-
err: errors.Join(fmt.Errorf("oh no"), status.Error(codes.Canceled, "canceled")),
108-
expected: true,
109-
},
110-
}
111-
for _, tc := range tcs {
112-
t.Run(tc.name, func(t *testing.T) {
113-
assert.Equalf(t, tc.expected, IsDownstreamError(tc.err), "IsDownstreamError(%v)", tc.err)
114-
})
115-
}
116-
}
117-
118-
var _ net.Error = &fakeNetworkError{}
119-
120-
type fakeNetworkError struct {
121-
timeout bool
122-
temporary bool
123-
}
124-
125-
func newFakeNetworkError(timeout, temporary bool) *fakeNetworkError {
126-
return &fakeNetworkError{
127-
timeout: timeout,
128-
temporary: temporary,
129-
}
130-
}
131-
132-
func (d *fakeNetworkError) Error() string {
133-
return "dummy timeout error"
134-
}
135-
136-
func (d *fakeNetworkError) Timeout() bool {
137-
return d.timeout
138-
}
139-
140-
func (d *fakeNetworkError) Temporary() bool {
141-
return d.temporary
11+
var s backend.ErrorSource
12+
require.False(t, s.IsValid())
13+
require.Equal(t, "plugin", s.String())
14+
require.True(t, backend.ErrorSourceDownstream.IsValid())
15+
require.Equal(t, "downstream", backend.ErrorSourceDownstream.String())
16+
require.True(t, backend.ErrorSourcePlugin.IsValid())
17+
require.Equal(t, "plugin", backend.ErrorSourcePlugin.String())
14218
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package httpclient
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/grafana/grafana-plugin-sdk-go/experimental/status"
7+
)
8+
9+
// ErrorSourceMiddlewareName is the middleware name used by ErrorSourceMiddleware.
10+
const ErrorSourceMiddlewareName = "ErrorSource"
11+
12+
// ErrorSourceMiddleware inspect the response error and wraps it in a [status.DownstreamError] if [status.IsDownstreamHTTPError] returns true.
13+
func ErrorSourceMiddleware() Middleware {
14+
return NamedMiddlewareFunc(ErrorSourceMiddlewareName, func(_ Options, next http.RoundTripper) http.RoundTripper {
15+
return RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
16+
res, err := next.RoundTrip(req)
17+
if err != nil && status.IsDownstreamHTTPError(err) {
18+
return res, status.DownstreamError(err)
19+
}
20+
21+
return res, err
22+
})
23+
})
24+
}

0 commit comments

Comments
 (0)