Skip to content

Commit

Permalink
backend: parse redpanda admin api errors
Browse files Browse the repository at this point in the history
  • Loading branch information
weeco committed Feb 28, 2024
1 parent c6ac52d commit 328c9d5
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 0 deletions.
110 changes: 110 additions & 0 deletions backend/pkg/api/connect/errors/redpanda.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2024 Redpanda Data, Inc.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.md
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0

package errors

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

"connectrpc.com/connect"
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/adminapi"

v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/dataplane/v1alpha1"
)

// NewConnectErrorFromRedpandaAdminAPIError enhances error handling by providing
// more insightful connect.Error messages to users. It unwraps errors from the
// Redpanda Admin API, allowing for better feedback. It decodes HTTP response
// errors, extracts relevant information, and constructs detailed error
// messages. Additionally, it handles timeouts gracefully by generating specific
// error messages for canceled requests.
func NewConnectErrorFromRedpandaAdminAPIError(err error) *connect.Error {
var httpErr *adminapi.HTTPResponseError
if errors.As(err, &httpErr) {
connectCode := CodeFromHTTPStatus(httpErr.Response.StatusCode)

adminApiErr, err := httpErr.DecodeGenericErrorBody()
if err != nil {
return NewConnectError(
connectCode,
errors.New(httpErr.Error()),
NewErrorInfo(
v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String(), KeyVal{
Key: "adminapi_status_code",
Value: strconv.Itoa(httpErr.Response.StatusCode),
},
),
)
}

// Bubble up original Redpanda adminapi error message
return NewConnectError(
connectCode,
errors.New(adminApiErr.Message),
NewErrorInfo(
v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String(), KeyVal{
Key: "adminapi_status_code",
Value: strconv.Itoa(httpErr.Response.StatusCode),
},
),
)
}

// Write a proper error for requests that timed-out
if errors.Is(err, context.Canceled) {
return NewConnectError(
connect.CodeCanceled,
errors.New("the request to the Redpanda admin API timed out"),
NewErrorInfo(v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String()),
)
}

return NewConnectError(
connect.CodeInternal,
err,
NewErrorInfo(v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String()),
)
}

// CodeFromHTTPStatus converts an HTTP response status code into the
// corresponding connect.Code. If no status code matches, CodeUnknown will be
// returned.
func CodeFromHTTPStatus(status int) connect.Code {
switch status {
case http.StatusOK:
return 0
case 499:
return connect.CodeCanceled
case http.StatusInternalServerError:
return connect.CodeInternal
case http.StatusBadRequest:
return connect.CodeInvalidArgument
case http.StatusGatewayTimeout:
return connect.CodeDeadlineExceeded
case http.StatusNotFound:
return connect.CodeNotFound
case http.StatusConflict:
return connect.CodeAlreadyExists
case http.StatusForbidden:
return connect.CodePermissionDenied
case http.StatusUnauthorized:
return connect.CodeUnauthenticated
case http.StatusTooManyRequests:
return connect.CodeResourceExhausted
case http.StatusNotImplemented:
return connect.CodeUnimplemented
case http.StatusServiceUnavailable:
return connect.CodeUnavailable
default:
return connect.CodeUnknown
}
}
116 changes: 116 additions & 0 deletions backend/pkg/api/connect/errors/redpanda_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2024 Redpanda Data, Inc.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.md
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0

package errors

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

"connectrpc.com/connect"
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/adminapi"
"github.com/stretchr/testify/assert"

v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/dataplane/v1alpha1"
)

// HTTPResponseErrorInterface abstracts adminapi.HTTPResponseError for mocking.
//
//go:generate mockgen -destination=./mocks/http_response_error.go -package=mocks github.com/redpanda-data/console/backend/pkg/api/connect/errors HTTPResponseErrorInterface
type HTTPResponseErrorInterface interface {
Error() string
DecodeGenericErrorBody() (*adminapi.GenericErrorBody, error)
GetResponse() *http.Response
}

// Define the test cases
func TestNewConnectErrorFromRedpandaAdminAPIError(t *testing.T) {
tests := []struct {
name string
inputError error
expectedResult *connect.Error
}{
{
name: "HTTPResponseError with undecodable body",
inputError: func() error {
/*
mockErr := new(MockHTTPResponseError)
mockErr.On("Error").Return("mock error message")
mockErr.Response = &http.Response{StatusCode: http.StatusInternalServerError}
return mockErr*/
return nil
}(),
expectedResult: NewConnectError(
CodeFromHTTPStatus(http.StatusInternalServerError),
errors.New("mock error message"),
NewErrorInfo(
v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String(),
KeyVal{
Key: "adminapi_status_code",
Value: strconv.Itoa(http.StatusInternalServerError),
},
),
),
},
{
name: "HTTPResponseError with decodable body",
inputError: func() error {
/*
mockErr := new(MockHTTPResponseError)
mockErr.On("Error").Return("mock error message")
mockErr.On("DecodeGenericErrorBodyBody").Return(
&adminapi.GenericErrorBody{
Message: "decoded message",
Code: http.StatusNotFound,
}, nil,
)
mockErr.Response = &http.Response{StatusCode: http.StatusOK}
return mockErr
*/
return nil
}(),
expectedResult: NewConnectError(
connect.CodeNotFound,
errors.New("decoded message"),
NewErrorInfo(
v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String(),
KeyVal{
Key: "adminapi_status_code",
Value: strconv.Itoa(http.StatusNotFound),
},
),
),
},
{
name: "Context canceled error",
inputError: func() error {
return fmt.Errorf("some random msg with a wrapped cancelled context err: %w", context.Canceled)
}(),
expectedResult: NewConnectError(
connect.CodeCanceled,
errors.New("the request to the Redpanda admin API timed out"),
NewErrorInfo(v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String()),
),
},
}

// Run the test cases
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
result := NewConnectErrorFromRedpandaAdminAPIError(tt.inputError)
assert.Equal(t, tt.expectedResult, result)
},
)
}
}

0 comments on commit 328c9d5

Please sign in to comment.