Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose http response headers in ServerError #100

Merged
merged 6 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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