Skip to content

Commit

Permalink
Error source HTTP client middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
marefr committed Oct 10, 2024
1 parent 6e35428 commit b5e125e
Show file tree
Hide file tree
Showing 12 changed files with 481 additions and 274 deletions.
9 changes: 5 additions & 4 deletions backend/data_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"

"github.com/grafana/grafana-plugin-sdk-go/backend/status"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)

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

status := RequestStatusFromQueryDataResponse(resp, innerErr)
requestStatus := RequestStatusFromQueryDataResponse(resp, innerErr)
if innerErr != nil {
return status, innerErr
return requestStatus, innerErr
} else if resp == nil {
return RequestStatusError, errors.New("both response and error are nil, but one must be provided")
}
Expand All @@ -41,7 +42,7 @@ func (a *dataSDKAdapter) QueryData(ctx context.Context, req *pluginv2.QueryDataR
// and if there's no plugin error
var hasPluginError, hasDownstreamError bool
for refID, r := range resp.Responses {
if r.Error == nil || isCancelledError(r.Error) {
if r.Error == nil || status.IsCancelledError(r.Error) {
continue
}

Expand Down Expand Up @@ -81,7 +82,7 @@ func (a *dataSDKAdapter) QueryData(ctx context.Context, req *pluginv2.QueryDataR
}
}

return status, nil
return requestStatus, nil
})
if err != nil {
return nil, err
Expand Down
118 changes: 25 additions & 93 deletions backend/error_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,144 +2,76 @@ package backend

import (
"context"
"errors"
"fmt"
"net/http"

"github.com/grafana/grafana-plugin-sdk-go/backend/status"
)

// ErrorSource type defines the source of the error
type ErrorSource string
type ErrorSource = status.Source

const (
// ErrorSourcePlugin error originates from plugin.
ErrorSourcePlugin ErrorSource = "plugin"
ErrorSourcePlugin = status.SourcePlugin

// ErrorSourceDownstream error originates from downstream service.
ErrorSourceDownstream ErrorSource = "downstream"
ErrorSourceDownstream = status.SourceDownstream

// DefaultErrorSource is the default [ErrorSource] that should be used when it is not explicitly set.
DefaultErrorSource ErrorSource = ErrorSourcePlugin
DefaultErrorSource = status.SourcePlugin
)

func (es ErrorSource) IsValid() bool {
return es == ErrorSourceDownstream || es == ErrorSourcePlugin
// ErrorSourceFromHTTPError returns an [ErrorSource] based on provided error.
func ErrorSourceFromHTTPError(err error) ErrorSource {
return status.SourceFromHTTPError(err)
}

// ErrorSourceFromStatus returns an [ErrorSource] based on provided HTTP status code.
// ErrorSourceFromHTTPStatus returns an [ErrorSource] based on provided HTTP status code.
func ErrorSourceFromHTTPStatus(statusCode int) ErrorSource {
switch statusCode {
case http.StatusMethodNotAllowed,
http.StatusNotAcceptable,
http.StatusPreconditionFailed,
http.StatusRequestEntityTooLarge,
http.StatusRequestHeaderFieldsTooLarge,
http.StatusRequestURITooLong,
http.StatusExpectationFailed,
http.StatusUpgradeRequired,
http.StatusRequestedRangeNotSatisfiable,
http.StatusNotImplemented:
return ErrorSourcePlugin
}

return ErrorSourceDownstream
return status.SourceFromHTTPStatus(statusCode)
}

type errorWithSourceImpl struct {
source ErrorSource
err error
// IsDownstreamError return true if provided error is an error with downstream source or
// a timeout error or a cancelled error.
func IsDownstreamError(err error) bool {
return status.IsDownstreamError(err)
}

func IsDownstreamError(err error) bool {
e := errorWithSourceImpl{
source: ErrorSourceDownstream,
}
if errors.Is(err, e) {
return true
}

type errorWithSource interface {
ErrorSource() ErrorSource
}

// nolint:errorlint
if errWithSource, ok := err.(errorWithSource); ok && errWithSource.ErrorSource() == ErrorSourceDownstream {
return true
}

if isHTTPTimeoutError(err) || isCancelledError(err) {
return true
}

return false
// IsDownstreamError return true if provided error is an error with downstream source or
// a HTTP timeout error or a cancelled error or a connection reset/refused error or dns not found error.
func IsDownstreamHTTPError(err error) bool {
return status.IsDownstreamHTTPError(err)
}

func DownstreamError(err error) error {
return errorWithSourceImpl{
source: ErrorSourceDownstream,
err: err,
}
return status.DownstreamError(err)
}

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

func (e errorWithSourceImpl) ErrorSource() ErrorSource {
return e.source
}

func (e errorWithSourceImpl) Error() string {
return fmt.Errorf("%s error: %w", e.source, e.err).Error()
}

// Implements the interface used by [errors.Is].
func (e errorWithSourceImpl) Is(err error) bool {
if errWithSource, ok := err.(errorWithSourceImpl); ok {
return errWithSource.ErrorSource() == e.source
}

return false
}

func (e errorWithSourceImpl) Unwrap() error {
return e.err
}

type errorSourceCtxKey struct{}

// errorSourceFromContext returns the error source stored in the context.
// If no error source is stored in the context, [DefaultErrorSource] is returned.
func errorSourceFromContext(ctx context.Context) ErrorSource {
value, ok := ctx.Value(errorSourceCtxKey{}).(*ErrorSource)
if ok {
return *value
}
return DefaultErrorSource
return status.SourceFromContext(ctx)
}

// initErrorSource initialize the status source for the context.
// initErrorSource initialize the error source for the context.
func initErrorSource(ctx context.Context) context.Context {
s := DefaultErrorSource
return context.WithValue(ctx, errorSourceCtxKey{}, &s)
return status.InitSource(ctx)
}

// WithErrorSource mutates the provided context by setting the error source to
// s. If the provided context does not have a error source, the context
// will not be mutated and an error returned. This means that [initErrorSource]
// has to be called before this function.
func WithErrorSource(ctx context.Context, s ErrorSource) error {
v, ok := ctx.Value(errorSourceCtxKey{}).(*ErrorSource)
if !ok {
return errors.New("the provided context does not have a status source")
}
*v = s
return nil
return status.WithSource(ctx, s)
}

// WithDownstreamErrorSource mutates the provided context by setting the error source to
// [ErrorSourceDownstream]. If the provided context does not have a error source, the context
// will not be mutated and an error returned. This means that [initErrorSource] has to be
// called before this function.
func WithDownstreamErrorSource(ctx context.Context) error {
return WithErrorSource(ctx, ErrorSourceDownstream)
return status.WithDownstreamSource(ctx)
}
142 changes: 0 additions & 142 deletions backend/error_source_test.go

This file was deleted.

24 changes: 24 additions & 0 deletions backend/httpclient/error_source_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package httpclient

import (
"net/http"

"github.com/grafana/grafana-plugin-sdk-go/backend/status"
)

// ErrorSourceMiddlewareName is the middleware name used by ErrorSourceMiddleware.
const ErrorSourceMiddlewareName = "ErrorSource"

// ErrorSourceMiddleware inspect the response error and wraps it in a [status.DownstreamError] if [status.IsDownstreamHTTPError] returns true.
func ErrorSourceMiddleware() Middleware {
return NamedMiddlewareFunc(ErrorSourceMiddlewareName, func(_ Options, next http.RoundTripper) http.RoundTripper {
return RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
res, err := next.RoundTrip(req)
if err != nil && status.IsDownstreamHTTPError(err) {
return res, status.DownstreamError(err)
}

return res, err
})
})
}
1 change: 1 addition & 0 deletions backend/httpclient/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ func DefaultMiddlewares() []Middleware {
BasicAuthenticationMiddleware(),
CustomHeadersMiddleware(),
ContextualMiddleware(),
ErrorSourceMiddleware(),
}
}

Expand Down
3 changes: 2 additions & 1 deletion backend/httpclient/http_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ func TestNewClient(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, client)

require.Len(t, usedMiddlewares, 4)
require.Len(t, usedMiddlewares, 5)
require.Equal(t, TracingMiddlewareName, usedMiddlewares[0].(MiddlewareName).MiddlewareName())
require.Equal(t, BasicAuthenticationMiddlewareName, usedMiddlewares[1].(MiddlewareName).MiddlewareName())
require.Equal(t, CustomHeadersMiddlewareName, usedMiddlewares[2].(MiddlewareName).MiddlewareName())
require.Equal(t, ContextualMiddlewareName, usedMiddlewares[3].(MiddlewareName).MiddlewareName())
require.Equal(t, ErrorSourceMiddlewareName, usedMiddlewares[4].(MiddlewareName).MiddlewareName())
})

t.Run("New() with opts middleware should return expected http.Client", func(t *testing.T) {
Expand Down
Loading

0 comments on commit b5e125e

Please sign in to comment.