Skip to content

Commit 017c8c8

Browse files
authored
Merge pull request #404 from influxdata/feat/httpErrorHeaders
feat: HTTP headers in Error type
2 parents b3496ca + 5858f61 commit 017c8c8

File tree

9 files changed

+233
-4
lines changed

9 files changed

+233
-4
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
## 2.14.0 [Unreleased]
1+
## 2.14 [unreleased]
2+
3+
### Features
4+
5+
- [#404](https://github.com/influxdata/influxdb-client-go/pull/404) Expose HTTP response headers in the Error type to aid analysis and debugging of error results. Add selected response headers to the error log.
6+
7+
Also, unified errors returned by WriteAPI, which now always returns `http.Error`
28

39
### Fixes
410
- [#403](https://github.com/influxdata/influxdb-client-go/pull/403) Custom checks de/serialization to allow calling server Check API

api/examples_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/influxdata/influxdb-client-go/v2/api"
14+
apiHttp "github.com/influxdata/influxdb-client-go/v2/api/http"
1415
"github.com/influxdata/influxdb-client-go/v2/api/write"
1516
"github.com/influxdata/influxdb-client-go/v2/domain"
1617
influxdb2 "github.com/influxdata/influxdb-client-go/v2/internal/examples"
@@ -123,6 +124,7 @@ func ExampleWriteAPI_errors() {
123124
go func() {
124125
for err := range errorsCh {
125126
fmt.Printf("write error: %s\n", err.Error())
127+
fmt.Printf("trace-id: %s\n", err.(*apiHttp.Error).Header.Get("Trace-ID"))
126128
}
127129
}()
128130
// write some points

api/http/error.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package http
66

77
import (
88
"fmt"
9+
"net/http"
910
"strconv"
1011
)
1112

@@ -16,6 +17,7 @@ type Error struct {
1617
Message string
1718
Err error
1819
RetryAfter uint
20+
Header http.Header
1921
}
2022

2123
// Error fulfils error interface
@@ -37,6 +39,25 @@ func (e *Error) Unwrap() error {
3739
return nil
3840
}
3941

42+
// HeaderToString generates a string value from the Header property. Useful in logging.
43+
func (e *Error) HeaderToString(selected []string) string {
44+
headerString := ""
45+
if len(selected) == 0 {
46+
for key := range e.Header {
47+
k := http.CanonicalHeaderKey(key)
48+
headerString += fmt.Sprintf("%s: %s\r\n", k, e.Header.Get(k))
49+
}
50+
} else {
51+
for _, candidate := range selected {
52+
c := http.CanonicalHeaderKey(candidate)
53+
if e.Header.Get(c) != "" {
54+
headerString += fmt.Sprintf("%s: %s\n", c, e.Header.Get(c))
55+
}
56+
}
57+
}
58+
return headerString
59+
}
60+
4061
// NewError returns newly created Error initialised with nested error and default values
4162
func NewError(err error) *Error {
4263
return &Error{
@@ -45,5 +66,6 @@ func NewError(err error) *Error {
4566
Message: "",
4667
Err: err,
4768
RetryAfter: 0,
69+
Header: http.Header{},
4870
}
4971
}

api/http/error_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2020-2024 InfluxData, Inc. All rights reserved.
2+
// Use of this source code is governed by MIT
3+
// license that can be found in the LICENSE file.
4+
5+
package http
6+
7+
import (
8+
"fmt"
9+
ihttp "net/http"
10+
11+
"github.com/stretchr/testify/assert"
12+
13+
"testing"
14+
)
15+
16+
func TestWriteErrorHeaderToString(t *testing.T) {
17+
header := ihttp.Header{
18+
"Date": []string{"2024-08-07T12:00:00.009"},
19+
"Content-Length": []string{"12"},
20+
"Content-Type": []string{"application/json", "encoding UTF-8"},
21+
"X-Test-Value1": []string{"SaturnV"},
22+
"X-Test-Value2": []string{"Apollo11"},
23+
"Retry-After": []string{"2044"},
24+
"Trace-Id": []string{"123456789ABCDEF0"},
25+
}
26+
27+
err := Error{
28+
StatusCode: ihttp.StatusBadRequest,
29+
Code: "bad request",
30+
Message: "this is just a test",
31+
Err: nil,
32+
RetryAfter: 2044,
33+
Header: header,
34+
}
35+
36+
fullString := err.HeaderToString([]string{})
37+
38+
// write order is not guaranteed
39+
assert.Contains(t, fullString, "Date: 2024-08-07T12:00:00.009")
40+
assert.Contains(t, fullString, "Content-Length: 12")
41+
assert.Contains(t, fullString, "Content-Type: application/json")
42+
assert.Contains(t, fullString, "X-Test-Value1: SaturnV")
43+
assert.Contains(t, fullString, "X-Test-Value2: Apollo11")
44+
assert.Contains(t, fullString, "Retry-After: 2044")
45+
assert.Contains(t, fullString, "Trace-Id: 123456789ABCDEF0")
46+
47+
filterString := err.HeaderToString([]string{"date", "trace-id", "x-test-value1", "x-test-value2"})
48+
49+
// write order will follow filter arguments
50+
assert.Equal(t, filterString,
51+
"Date: 2024-08-07T12:00:00.009\nTrace-Id: 123456789ABCDEF0\nX-Test-Value1: SaturnV\nX-Test-Value2: Apollo11\n",
52+
)
53+
assert.NotContains(t, filterString, "Content-Type: application/json")
54+
assert.NotContains(t, filterString, "Retry-After: 2044")
55+
}
56+
57+
func TestErrorIfaceError(t *testing.T) {
58+
tests := []struct {
59+
name string
60+
statusCode int
61+
err error
62+
code string
63+
message string
64+
expected string
65+
}{
66+
{name: "TestNestedErrorNotNilCode0Message0",
67+
statusCode: 418,
68+
err: fmt.Errorf("original test message"),
69+
code: "",
70+
message: "",
71+
expected: "original test message"},
72+
{name: "TestNestedErrorNotNilCodeXMessageX",
73+
statusCode: 418,
74+
err: fmt.Errorf("original test message"),
75+
code: "bad request",
76+
message: "is this a teapot?",
77+
expected: "original test message"},
78+
{name: "TestNestedErrorNilCodeXMessageX",
79+
statusCode: 418,
80+
err: nil,
81+
code: "bad request",
82+
message: "is this a teapot?",
83+
expected: "bad request: is this a teapot?"},
84+
{name: "TestNesterErrorNilCodeXMessage0",
85+
statusCode: 418,
86+
err: nil,
87+
code: "I'm a teapot",
88+
message: "",
89+
expected: "Unexpected status code 418"},
90+
}
91+
92+
for _, test := range tests {
93+
t.Run(test.name, func(t *testing.T) {
94+
err := Error{
95+
StatusCode: test.statusCode,
96+
Code: test.code,
97+
Message: test.message,
98+
Err: test.err,
99+
RetryAfter: 0,
100+
Header: ihttp.Header{},
101+
}
102+
assert.Equal(t, test.expected, err.Error())
103+
})
104+
}
105+
}

api/http/service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ func (s *service) parseHTTPError(r *http.Response) *Error {
151151

152152
perror := NewError(nil)
153153
perror.StatusCode = r.StatusCode
154+
perror.Header = r.Header
154155

155156
if v := r.Header.Get("Retry-After"); v != "" {
156157
r, err := strconv.ParseUint(v, 10, 32)

api/write_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import (
88
"fmt"
99
"io"
1010
"math"
11+
ihttp "net/http"
12+
"net/http/httptest"
1113
"runtime"
14+
"strconv"
1215
"strings"
1316
"sync"
1417
"testing"
@@ -265,3 +268,47 @@ func TestFlushWithRetries(t *testing.T) {
265268
// two remained
266269
assert.Equal(t, 2, len(service.Lines()))
267270
}
271+
272+
func TestWriteApiErrorHeaders(t *testing.T) {
273+
calls := 0
274+
var mu sync.Mutex
275+
server := httptest.NewServer(ihttp.HandlerFunc(func(w ihttp.ResponseWriter, r *ihttp.Request) {
276+
mu.Lock()
277+
defer mu.Unlock()
278+
calls++
279+
w.Header().Set("X-Test-Val1", "Not All Correct")
280+
w.Header().Set("X-Test-Val2", "Atlas LV-3B")
281+
w.Header().Set("X-Call-Count", strconv.Itoa(calls))
282+
w.WriteHeader(ihttp.StatusBadRequest)
283+
_, _ = w.Write([]byte(`{ "code": "bad request", "message": "test header" }`))
284+
}))
285+
defer server.Close()
286+
svc := http.NewService(server.URL, "my-token", http.DefaultOptions())
287+
writeAPI := NewWriteAPI("my-org", "my-bucket", svc, write.DefaultOptions().SetBatchSize(5))
288+
defer writeAPI.Close()
289+
errCh := writeAPI.Errors()
290+
var wg sync.WaitGroup
291+
var recErr error
292+
wg.Add(1)
293+
go func() {
294+
for i := 0; i < 3; i++ {
295+
recErr = <-errCh
296+
assert.NotNil(t, recErr, "errCh should not run out of values")
297+
assert.Len(t, recErr.(*http.Error).Header, 6)
298+
assert.NotEqual(t, "", recErr.(*http.Error).Header.Get("Date"))
299+
assert.NotEqual(t, "", recErr.(*http.Error).Header.Get("Content-Length"))
300+
assert.NotEqual(t, "", recErr.(*http.Error).Header.Get("Content-Type"))
301+
assert.Equal(t, strconv.Itoa(i+1), recErr.(*http.Error).Header.Get("X-Call-Count"))
302+
assert.Equal(t, "Not All Correct", recErr.(*http.Error).Header.Get("X-Test-Val1"))
303+
assert.Equal(t, "Atlas LV-3B", recErr.(*http.Error).Header.Get("X-Test-Val2"))
304+
}
305+
wg.Done()
306+
}()
307+
points := test.GenPoints(15)
308+
for i := 0; i < 15; i++ {
309+
writeAPI.WritePoint(points[i])
310+
}
311+
writeAPI.waitForFlushing()
312+
wg.Wait()
313+
assert.Equal(t, calls, 3)
314+
}

client_e2e_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"time"
1919

2020
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
21+
"github.com/influxdata/influxdb-client-go/v2/api/http"
2122
"github.com/influxdata/influxdb-client-go/v2/domain"
2223
"github.com/influxdata/influxdb-client-go/v2/internal/test"
2324
"github.com/influxdata/influxdb-client-go/v2/log"
@@ -368,3 +369,16 @@ func TestWriteCustomBatch(t *testing.T) {
368369
}
369370
assert.Equal(t, 10, l)
370371
}
372+
373+
func TestHttpHeadersInError(t *testing.T) {
374+
client := influxdb2.NewClientWithOptions(serverURL, authToken, influxdb2.DefaultOptions().SetLogLevel(0))
375+
err := client.WriteAPIBlocking("my-org", "my-bucket").WriteRecord(context.Background(), "asdf")
376+
assert.Error(t, err)
377+
assert.Len(t, err.(*http.Error).Header, 6)
378+
assert.NotEqual(t, err.(*http.Error).Header.Get("Date"), "")
379+
assert.NotEqual(t, err.(*http.Error).Header.Get("Content-Length"), "")
380+
assert.NotEqual(t, err.(*http.Error).Header.Get("Content-Type"), "")
381+
assert.NotEqual(t, err.(*http.Error).Header.Get("X-Platform-Error-Code"), "")
382+
assert.Contains(t, err.(*http.Error).Header.Get("X-Influxdb-Version"), "v")
383+
assert.Equal(t, err.(*http.Error).Header.Get("X-Influxdb-Build"), "OSS")
384+
}

internal/write/service.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,24 @@ func (w *Service) HandleWrite(ctx context.Context, batch *Batch) error {
196196
w.retryAttempts++
197197
log.Debugf("Write proc: next wait for write is %dms\n", w.retryDelay)
198198
} else {
199-
log.Errorf("Write error: %s\n", perror.Error())
199+
logMessage := fmt.Sprintf("Write error: %s", perror.Error())
200+
logHeaders := perror.HeaderToString([]string{
201+
"date",
202+
"trace-id",
203+
"trace-sampled",
204+
"X-Influxdb-Build",
205+
"X-Influxdb-Request-ID",
206+
"X-Influxdb-Version",
207+
})
208+
if len(logHeaders) > 0 {
209+
logMessage += fmt.Sprintf("\nSelected Response Headers:\n%s", logHeaders)
210+
}
211+
log.Error(logMessage)
200212
}
201-
return fmt.Errorf("write failed (attempts %d): %w", batchToWrite.RetryAttempts, perror)
213+
log.Errorf("Write failed (retry attempts %d): Status Code %d",
214+
batchToWrite.RetryAttempts,
215+
perror.StatusCode)
216+
return perror
202217
}
203218
}
204219

internal/write/service_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ func TestMaxRetryTime(t *testing.T) {
339339
err = srv.HandleWrite(ctx, b)
340340
require.NotNil(t, err)
341341
// 1st Batch expires and writing 2nd trows error
342-
assert.Equal(t, "write failed (attempts 1): Unexpected status code 429", err.Error())
342+
assert.Equal(t, "Unexpected status code 429", err.Error())
343343
assert.Equal(t, 1, srv.retryQueue.list.Len())
344344

345345
//wait until remaining accumulated retryDelay has passed, because there hasn't been a successful write yet
@@ -702,3 +702,20 @@ func TestIgnoreErrors(t *testing.T) {
702702
err = srv.HandleWrite(ctx, b)
703703
assert.Error(t, err)
704704
}
705+
706+
func TestHttpErrorHeaders(t *testing.T) {
707+
server := httptest.NewServer(ihttp.HandlerFunc(func(w ihttp.ResponseWriter, r *ihttp.Request) {
708+
w.Header().Set("X-Test-Val1", "Not All Correct")
709+
w.Header().Set("X-Test-Val2", "Atlas LV-3B")
710+
w.WriteHeader(ihttp.StatusBadRequest)
711+
_, _ = w.Write([]byte(`{ "code": "bad request", "message": "test header" }`))
712+
}))
713+
defer server.Close()
714+
svc := NewService("my-org", "my-bucket", http.NewService(server.URL, "", http.DefaultOptions()),
715+
write.DefaultOptions())
716+
err := svc.HandleWrite(context.Background(), NewBatch("1", 20))
717+
assert.Error(t, err)
718+
assert.Equal(t, "400 Bad Request: { \"code\": \"bad request\", \"message\": \"test header\" }", err.Error())
719+
assert.Equal(t, "Not All Correct", err.(*http.Error).Header.Get("X-Test-Val1"))
720+
assert.Equal(t, "Atlas LV-3B", err.(*http.Error).Header.Get("X-Test-Val2"))
721+
}

0 commit comments

Comments
 (0)