Skip to content

Commit

Permalink
feat: create write/Error wrapper to better handle http/Error with Hea…
Browse files Browse the repository at this point in the history
…ders.
  • Loading branch information
karel-rehor committed Aug 8, 2024
1 parent 4e3ff5f commit af74d7a
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 22 deletions.
2 changes: 1 addition & 1 deletion api/http/error_test.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
64 changes: 64 additions & 0 deletions api/write/error.go
Original file line number Diff line number Diff line change
@@ -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 ""
}
66 changes: 66 additions & 0 deletions api/write/error_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
16 changes: 9 additions & 7 deletions api/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}()
Expand Down
11 changes: 1 addition & 10 deletions internal/write/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down
8 changes: 4 additions & 4 deletions internal/write/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))
}

0 comments on commit af74d7a

Please sign in to comment.