Skip to content

Commit

Permalink
feat: expose http response headers in ServerError (#100)
Browse files Browse the repository at this point in the history
* feat: (WIP) adds response headers to ServerError

* test: refactor TestHttpErrorWithHeaders

* chore: fix assert in new test

* docs: adds examples for working with HTTP response headers in ServerError.

* docs: update CHANGELOG.md

* chore: improve assert error type
  • Loading branch information
karel-rehor authored Sep 9, 2024
1 parent ece5518 commit eab0a49
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## 0.10.0 [unreleased]

### Features

1. [#100](https://github.com/InfluxCommunity/influxdb3-go/pull/100): Expose HTTP Response headers in `ServerError`

### Bug Fixes

1. [#94](https://github.com/InfluxCommunity/influxdb3-go/pull/94): Resource leak from unclosed `Response`
Expand Down
56 changes: 56 additions & 0 deletions examples/General/httpErrorHandled.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"context"
"errors"
"fmt"
"log"
"os"

"github.com/InfluxCommunity/influxdb3-go/influxdb3"
)

// Demonstrates working with HTTP response headers in ServerError
func main() {
// Retrieve credentials from environment variables.
url := os.Getenv("INFLUX_URL")
token := os.Getenv("INFLUX_TOKEN")
database := os.Getenv("INFLUX_DATABASE")

// Instantiate a client using your credentials.
client, err := influxdb3.New(influxdb3.ClientConfig{
Host: url,
Token: token,
Database: database,
})
if err != nil {
panic(err)
}

// Close the client when finished and raise any errors.
defer func(client *influxdb3.Client) {
err := client.Close()
if err != nil {
panic(err)
}
}(client)

// Attempt to write line protocol synchronously
// N.B. faulty line protocol used here, because it
// guarantees a server error, but errors can be thrown
// for other reasons, such as 503 temporary unavailable
// or even 429 too many requests.
err = client.Write(context.Background(),
[]byte("air,sensor=HRF03,device_ID=42 humidity=67.1,temperature="))

if err != nil {
logMessage := "WARNING write error: " + err.Error()
logMessage += "\n ServerError.Headers:\n"
var svErr *influxdb3.ServerError
errors.As(err, &svErr)
for key, value := range svErr.Headers {
logMessage += fmt.Sprintf(" %s: %s\n", key, value)
}
log.Println(logMessage)
}
}
2 changes: 2 additions & 0 deletions influxdb3/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ func (c *Client) resolveHTTPError(r *http.Response) error {
}
}

httpError.Headers = r.Header

return &httpError.ServerError
}

Expand Down
26 changes: 26 additions & 0 deletions influxdb3/client_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"context"
"fmt"
"os"
"strconv"
"testing"
"time"

Expand Down Expand Up @@ -282,3 +283,28 @@ func TestQuerySchemaInfluxQL(t *testing.T) {
require.NoError(t, err)
assert.NotNil(t, iterator.Raw())
}

func TestWriteError(t *testing.T) {
url := os.Getenv("TESTING_INFLUXDB_URL")
token := os.Getenv("TESTING_INFLUXDB_TOKEN")
database := os.Getenv("TESTING_INFLUXDB_DATABASE")

client, err := influxdb3.New(influxdb3.ClientConfig{
Host: url,
Token: token,
Database: database,
})
require.NoError(t, err)

err = client.Write(context.Background(), []byte("test,type=negative val="))
require.Error(t, err)
assert.NotPanics(t, func() { _ = err.(*influxdb3.ServerError) })
assert.Regexp(t, "[0-9a-f]{16}", err.(*influxdb3.ServerError).Headers["Trace-Id"][0])
b, perr := strconv.ParseBool(err.(*influxdb3.ServerError).Headers["Trace-Sampled"][0])
require.NoError(t, perr)
assert.False(t, b)
assert.NotNil(t, err.(*influxdb3.ServerError).Headers["Strict-Transport-Security"])
assert.Regexp(t, "[0-9a-f]{32}", err.(*influxdb3.ServerError).Headers["X-Influxdb-Request-Id"][0])
assert.NotNil(t, err.(*influxdb3.ServerError).Headers["X-Influxdb-Build"][0])

}
3 changes: 3 additions & 0 deletions influxdb3/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package influxdb3

import (
"fmt"
"net/http"
)

// ServerError represents an error returned from an InfluxDB API server.
Expand All @@ -36,6 +37,8 @@ type ServerError struct {
StatusCode int `json:"-"`
// RetryAfter holds the value of Retry-After header if sent by server, otherwise zero
RetryAfter int `json:"-"`
// Headers hold the response headers
Headers http.Header `json:"headers"`
}

// NewServerError returns new with just a message
Expand Down
22 changes: 22 additions & 0 deletions influxdb3/example_write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package influxdb3

import (
"context"
"errors"
"log"
"strings"
"time"
Expand Down Expand Up @@ -123,3 +124,24 @@ func ExampleClient_WriteData() {
log.Fatal()
}
}

func ExampleClient_severError() {
client, err := NewFromEnv()
if err != nil {
log.Fatal()
}
defer client.Close()

err = client.Write(context.Background(),
[]byte("air,sensor=HRF03,device_ID=42 humidity=67.1,temperature="))

if err != nil {
log.Printf("WARN write failed: %s", err.Error())
var svErr *ServerError
errors.As(err, &svErr)
log.Printf(" ServerError headers:")
for key, val := range svErr.Headers {
log.Printf(" %s = %s", key, val)
}
}
}
34 changes: 34 additions & 0 deletions influxdb3/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,40 @@ func TestHttpError(t *testing.T) {
assert.ErrorContains(t, err, "error calling")
}

func TestHttpErrorWithHeaders(t *testing.T) {
traceID := "123456789ABCDEF0"
tsVersion := "v0.0.1"
build := "TestServer"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Trace-Id", traceID)
w.Header().Set("X-Influxdb-Build", build)
w.Header().Set("X-Influxdb-Version", tsVersion)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte("{ \"message\": \"Test Response\" }"))
if err != nil {
assert.FailNow(t, err.Error())
}
}))
defer ts.Close()
tc, err := New(ClientConfig{
Host: ts.URL,
Token: "my-token",
Database: "my-database",
})
require.NoError(t, err)
err = tc.WriteData(context.Background(), []any{})
require.Error(t, err)
var serr *ServerError
require.ErrorAs(t, err, &serr)
assert.Equal(t, 400, serr.StatusCode)
assert.Equal(t, "Test Response", serr.Message)
assert.Len(t, serr.Headers, 6)
assert.Equal(t, traceID, serr.Headers["Trace-Id"][0])
assert.Equal(t, build, serr.Headers["X-Influxdb-Build"][0])
assert.Equal(t, tsVersion, serr.Headers["X-Influxdb-Version"][0])
}

func TestWriteDatabaseNotSet(t *testing.T) {
p := NewPointWithMeasurement("cpu")
p.SetTag("host", "local")
Expand Down

0 comments on commit eab0a49

Please sign in to comment.