From af74d7a1624a852ccf45609d96e4e4378f4b2e6c Mon Sep 17 00:00:00 2001 From: karel rehor Date: Thu, 8 Aug 2024 14:59:07 +0200 Subject: [PATCH] feat: create write/Error wrapper to better handle http/Error with Headers. --- api/http/error_test.go | 2 +- api/write/error.go | 64 +++++++++++++++++++++++++++++++++ api/write/error_test.go | 66 ++++++++++++++++++++++++++++++++++ api/write_test.go | 16 +++++---- internal/write/service.go | 11 +----- internal/write/service_test.go | 8 ++--- 6 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 api/write/error.go create mode 100644 api/write/error_test.go diff --git a/api/http/error_test.go b/api/http/error_test.go index b21b2de4..ec5dc180 100644 --- a/api/http/error_test.go +++ b/api/http/error_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 InfluxData, Inc. All rights reserved. +// Copyright 2020-2024 InfluxData, Inc. All rights reserved. // Use of this source code is governed by MIT // license that can be found in the LICENSE file. diff --git a/api/write/error.go b/api/write/error.go new file mode 100644 index 00000000..f8c11865 --- /dev/null +++ b/api/write/error.go @@ -0,0 +1,64 @@ +// Copyright 2020-2024 InfluxData, Inc. All rights reserved. +// Use of this source code is governed by MIT +// license that can be found in the LICENSE file. + +package write + +import ( + "errors" + "fmt" + "github.com/influxdata/influxdb-client-go/v2/api/http" + iHttp "net/http" + "net/textproto" +) + +// Error wraps an error that may have occurred during a write call. Most often this will be an http.Error. +type Error struct { + origin error + message string +} + +// NewError returns a new created Error instance wrapping the original error. +func NewError(origin error, message string) *Error { + return &Error{ + origin: origin, + message: message, + } +} + +// Error fulfills the error interface +func (e *Error) Error() string { + return fmt.Sprintf("%s:\n %s", e.message, e.origin) +} + +func (e *Error) Unwrap() error { + return e.origin +} + +// HTTPHeader returns the Header of a wrapped http.Error. If the original error is not http.Error returns standard error. +func (e *Error) HTTPHeader() (iHttp.Header, error) { + var err *http.Error + ok := errors.As(e.origin, &err) + if ok { + return err.Header, nil + } + return nil, fmt.Errorf(fmt.Sprintf("Origin error: (%s) is not of type *http.Error.\n", e.origin.Error())) +} + +// GetHeaderValues returns the values from a Header key. If original error is not http.Error return standard error. +func (e *Error) GetHeaderValues(key string) ([]string, error) { + var err *http.Error + if errors.As(e.origin, &err) { + return err.Header.Values(textproto.CanonicalMIMEHeaderKey(key)), nil + } + return nil, fmt.Errorf(fmt.Sprintf("Origin error: (%s) is not of type http.Header.\n", e.origin.Error())) +} + +// GetHeader returns the first value from a header key. If origin is not http.Error or if no match is found returns "". +func (e *Error) GetHeader(key string) string { + var err *http.Error + if errors.As(e.origin, &err) { + return err.Header.Get(textproto.CanonicalMIMEHeaderKey(key)) + } + return "" +} diff --git a/api/write/error_test.go b/api/write/error_test.go new file mode 100644 index 00000000..6d433532 --- /dev/null +++ b/api/write/error_test.go @@ -0,0 +1,66 @@ +// Copyright 2020-2024 InfluxData, Inc. All rights reserved. +// Use of this source code is governed by MIT +// license that can be found in the LICENSE file. + +package write + +import ( + "errors" + "fmt" + "github.com/influxdata/influxdb-client-go/v2/api/http" + "github.com/stretchr/testify/assert" + ihttp "net/http" + "testing" +) + +func TestNewErrorNotHttpError(t *testing.T) { + err := NewError(fmt.Errorf("origin error"), "error message") + var errTest *http.Error + assert.False(t, errors.As(err, &errTest)) + header, okh := err.HTTPHeader() + assert.Nil(t, header) + assert.Error(t, okh) + values, okv := err.GetHeaderValues("Date") + assert.Nil(t, values) + assert.Error(t, okv) + assert.Equal(t, "", err.GetHeader("Date")) + assert.Equal(t, "error message:\n origin error", err.Error()) +} + +func TestNewErrorHttpError(t *testing.T) { + header := ihttp.Header{ + "Date": []string{"2024-08-07T12:00:00.009"}, + "Content-Length": []string{"12"}, + "Content-Type": []string{"application/json", "encoding UTF-8"}, + "X-Test-Value1": []string{"SaturnV"}, + "X-Test-Value2": []string{"Apollo11"}, + "Retry-After": []string{"2044"}, + "Trace-Id": []string{"123456789ABCDEF0"}, + } + + err := NewError(&http.Error{ + StatusCode: ihttp.StatusBadRequest, + Code: "bad request", + Message: "this is just a test", + Err: nil, + RetryAfter: 2044, + Header: header, + }, "should be httpError") + + var errTest *http.Error + assert.True(t, errors.As(err.Unwrap(), &errTest)) + header, okh := err.HTTPHeader() + assert.NotNil(t, header) + assert.Nil(t, okh) + date, okd := err.GetHeaderValues("Date") + assert.Equal(t, []string{"2024-08-07T12:00:00.009"}, date) + assert.Nil(t, okd) + cType, okc := err.GetHeaderValues("Content-Type") + assert.Equal(t, []string{"application/json", "encoding UTF-8"}, cType) + assert.Nil(t, okc) + assert.Equal(t, "2024-08-07T12:00:00.009", err.GetHeader("Date")) + assert.Equal(t, "SaturnV", err.GetHeader("X-Test-Value1")) + assert.Equal(t, "Apollo11", err.GetHeader("X-Test-Value2")) + assert.Equal(t, "123456789ABCDEF0", err.GetHeader("Trace-Id")) + assert.Equal(t, "should be httpError:\n bad request: this is just a test", err.Error()) +} diff --git a/api/write_test.go b/api/write_test.go index 4fda86bd..034a8f71 100644 --- a/api/write_test.go +++ b/api/write_test.go @@ -294,13 +294,15 @@ func TestWriteApiErrorHeaders(t *testing.T) { for i := 0; i < 3; i++ { recErr = <-errCh assert.NotNil(t, recErr, "errCh should not run out of values") - assert.Len(t, recErr.(*http.Error).Header, 6) - assert.NotEqual(t, "", recErr.(*http.Error).Header.Get("Date")) - assert.NotEqual(t, "", recErr.(*http.Error).Header.Get("Content-Length")) - assert.NotEqual(t, "", recErr.(*http.Error).Header.Get("Content-Type")) - assert.Equal(t, strconv.Itoa(i+1), recErr.(*http.Error).Header.Get("X-Call-Count")) - assert.Equal(t, "Not All Correct", recErr.(*http.Error).Header.Get("X-Test-Val1")) - assert.Equal(t, "Atlas LV-3B", recErr.(*http.Error).Header.Get("X-Test-Val2")) + header, okh := recErr.(*write.Error).HTTPHeader() + assert.Nil(t, okh) + assert.Len(t, header, 6) + assert.NotEqual(t, "", recErr.(*write.Error).GetHeader("Date")) + assert.NotEqual(t, "", recErr.(*write.Error).GetHeader("Content-Length")) + assert.NotEqual(t, "", recErr.(*write.Error).GetHeader("Content-Type")) + assert.Equal(t, strconv.Itoa(i+1), recErr.(*write.Error).GetHeader("X-Call-Count")) + assert.Equal(t, "Not All Correct", recErr.(*write.Error).GetHeader("X-Test-Val1")) + assert.Equal(t, "Atlas LV-3B", recErr.(*write.Error).GetHeader("X-Test-Val2")) } wg.Done() }() diff --git a/internal/write/service.go b/internal/write/service.go index e39acffb..4aa9b2ed 100644 --- a/internal/write/service.go +++ b/internal/write/service.go @@ -210,16 +210,7 @@ func (w *Service) HandleWrite(ctx context.Context, batch *Batch) error { } log.Error(logMessage) } - return &http2.Error{ - StatusCode: int(perror.StatusCode), - Code: perror.Code, - Message: fmt.Errorf( - "write failed (attempts %d): %w", batchToWrite.RetryAttempts, perror, - ).Error(), - Err: perror.Err, - RetryAfter: perror.RetryAfter, - Header: perror.Header, - } + return write.NewError(perror, fmt.Sprintf("write failed (retry attempts %d)", batchToWrite.RetryAttempts)) } } diff --git a/internal/write/service_test.go b/internal/write/service_test.go index ed0b077e..8638c82f 100644 --- a/internal/write/service_test.go +++ b/internal/write/service_test.go @@ -339,7 +339,7 @@ func TestMaxRetryTime(t *testing.T) { err = srv.HandleWrite(ctx, b) require.NotNil(t, err) // 1st Batch expires and writing 2nd trows error - assert.Equal(t, "write failed (attempts 1): Unexpected status code 429", err.Error()) + assert.Equal(t, "write failed (retry attempts 1):\n Unexpected status code 429", err.Error()) assert.Equal(t, 1, srv.retryQueue.list.Len()) //wait until remaining accumulated retryDelay has passed, because there hasn't been a successful write yet @@ -715,7 +715,7 @@ func TestHttpErrorHeaders(t *testing.T) { write.DefaultOptions()) err := svc.HandleWrite(context.Background(), NewBatch("1", 20)) assert.Error(t, err) - assert.Equal(t, "400 Bad Request: write failed (attempts 0): 400 Bad Request: { \"code\": \"bad request\", \"message\": \"test header\" }", err.Error()) - assert.Equal(t, "Not All Correct", err.(*http.Error).Header.Get("X-Test-Val1")) - assert.Equal(t, "Atlas LV-3B", err.(*http.Error).Header.Get("X-Test-Val2")) + assert.Equal(t, "write failed (retry attempts 0):\n 400 Bad Request: { \"code\": \"bad request\", \"message\": \"test header\" }", err.Error()) + assert.Equal(t, "Not All Correct", err.(*write.Error).GetHeader("X-Test-Val1")) + assert.Equal(t, "Atlas LV-3B", err.(*write.Error).GetHeader("X-Test-Val2")) }