Repository: influxdata/influxdb-client-go
Pull request #281 (2021-11-05 12:30:12)

feat(v3): common HTTP handling code + query
This is a proposal of a general function that should help make other future functions, which will communicate with the server, easier. By future functions I mean those parts of the Client struct: Ready, Health, Delete, Write (also PointWriter).
Most reusable code is handling server errors. 

Retry-After reading is skipped here as it is needed to be handled only in the Write function to handle server back-pressure and thus Write can read this header itself. Or, it can be added later.

Added is also the `Query` function as the first feature using the `makeAPICall` function.


Patch:
diff --git a/influxclient/client.go b/influxclient/client.go
new file mode 100644
index 0000000..7741a0a
--- /dev/null
+++ b/influxclient/client.go
@@ -0,0 +1,169 @@
+// Copyright 2021 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
+// Package influxclient provides client for InfluxDB server.
+package influxclient
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"mime"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+// Params holds the parameters for creating a new client.
+// The only mandatory field is ServerURL. AuthToken is also important
+// if authentication was not done outside this client.
+type Params struct {
+	// ServerURL holds the URL of the InfluxDB server to connect to.
+	// This must be non-empty. E.g. http://localhost:8086
+	ServerURL string
+
+	// AuthToken holds the authorization token for the API.
+	// This can be obtained through the GUI web browser interface.
+	AuthToken string
+
+	// Organization is name or ID of organization where data (buckets, users, tasks, etc.) belongs to
+	Organization string
+
+	// HTTPClient is used to make API requests.
+	//
+	// This can be used to specify a custom TLS configuration
+	// (TLSClientConfig), a custom request timeout (Timeout),
+	// or other customization as required.
+	//
+	// It HTTPClient is nil, http.DefaultClient will be used.
+	HTTPClient *http.Client
+}
+
+// Client implements an InfluxDB client.
+type Client struct {
+	// Configuration params.
+	params Params
+	// Pre-created Authorization HTTP header value.
+	authorization string
+	// Cached base server API URL.
+	apiURL *url.URL
+}
+
+// httpParams holds parameters for creating an HTTP request
+type httpParams struct {
+	// URL of server endpoint
+	endpointURL *url.URL
+	// Params to be added to URL
+	queryParams url.Values
+	// HTTP request method, eg. POST
+	httpMethod string
+	// HTTP request headers
+	headers map[string]string
+	// HTTP POST/PUT body
+	body io.Reader
+}
+
+// New creates new Client with given Params, where ServerURL and AuthToken are mandatory.
+func New(params Params) (*Client, error) {
+	c := &Client{params: params}
+	if params.ServerURL == "" {
+		return nil, errors.New("empty server URL")
+	}
+	if c.params.AuthToken != "" {
+		c.authorization = "Token " + c.params.AuthToken
+	}
+	if c.params.HTTPClient == nil {
+		c.params.HTTPClient = http.DefaultClient
+	}
+
+	serverAddress := params.ServerURL
+	if !strings.HasSuffix(serverAddress, "/") {
+		// For subsequent path parts concatenation, url has to end with '/'
+		serverAddress = params.ServerURL + "/"
+	}
+	var err error
+	// Prepare server API URL
+	c.apiURL, err = url.Parse(serverAddress + "api/v2/")
+	if err != nil {
+		return nil, fmt.Errorf("error parsing server URL: %w", err)
+	}
+	return c, nil
+}
+
+// makeAPICall issues an HTTP request to InfluxDB server API url according to parameters.
+// Additionally, sets Authorization header and User-Agent.
+// It returns http.Response or error. Error can be a *ServerError if server responded with error.
+func (c *Client) makeAPICall(ctx context.Context, params httpParams) (*http.Response, error) {
+	// copy URL
+	urlObj := *params.endpointURL
+	urlObj.RawQuery = params.queryParams.Encode()
+
+	fullURL := urlObj.String()
+
+	req, err := http.NewRequestWithContext(ctx, params.httpMethod, fullURL, params.body)
+	if err != nil {
+		return nil, fmt.Errorf("error calling %s: %v", fullURL, err)
+	}
+	for k, v := range params.headers {
+		req.Header.Set(k, v)
+	}
+	req.Header.Set("User-Agent", userAgent)
+	if c.authorization != "" {
+		req.Header.Add("Authorization", c.authorization)
+	}
+
+	resp, err := c.params.HTTPClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("error calling %s: %v", fullURL, err)
+	}
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return nil, c.resolveHTTPError(resp)
+	}
+
+	return resp, nil
+}
+
+// resolveHTTPError parses server error response and returns error with human-readable message
+func (c *Client) resolveHTTPError(r *http.Response) error {
+	// successful status code range
+	if r.StatusCode >= 200 && r.StatusCode < 300 {
+		return nil
+	}
+
+	var httpError struct {
+		ServerError
+		// Error message of InfluxDB 1 error
+		Error string `json:"error"`
+	}
+
+	httpError.StatusCode = r.StatusCode
+
+	body, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		httpError.Message = fmt.Sprintf("cannot read error response:: %v", err)
+	}
+	ctype, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
+	if ctype == "application/json" {
+		err := json.Unmarshal(body, &httpError)
+		if err != nil {
+			httpError.Message = fmt.Sprintf("cannot decode error response: %v", err)
+		}
+		if httpError.Message == "" && httpError.Code == "" {
+			httpError.Message = httpError.Error
+		}
+	}
+	if httpError.Message == "" {
+		//TODO: "This could be a large piece of unreadable body; we might be able to do better than this"
+		if len(body) > 0 {
+			httpError.Message = string(body)
+		} else {
+			httpError.Message = r.Status
+		}
+	}
+
+	return &httpError.ServerError
+}
diff --git a/influxclient/client_query.go b/influxclient/client_query.go
new file mode 100644
index 0000000..5a737fd
--- /dev/null
+++ b/influxclient/client_query.go
@@ -0,0 +1,180 @@
+// Copyright 2021 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
+package influxclient
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"reflect"
+	"time"
+)
+
+// dialect defines attributes of Flux query response header.
+type dialect struct {
+	Annotations []string `json:"annotations"`
+	Delimiter   string   `json:"delimiter"`
+	Header      bool     `json:"header"`
+}
+
+//  queryBody holds the body for an HTTP query request.
+type queryBody struct {
+	Dialect dialect     `json:"dialect"`
+	Query   string      `json:"query"`
+	Type    string      `json:"type"`
+	Params  interface{} `json:"params,omitempty"`
+}
+
+// defaultDialect is queryBody dialect value with all annotations, name header and comma as delimiter
+var defaultDialect = dialect{
+	Annotations: []string{"datatype", "default", "group"},
+	Delimiter:   ",",
+	Header:      true,
+}
+
+// Query sends the given Flux query to server and returns QueryResultReader for further parsing result.
+// The result must be closed after use.
+//
+// Flux query can contain reference to parameters, which must be passed via queryParams.
+// it can be a struct or map. Param values can be only simple types or time.Time.
+// Name of a struct field or a map key (must be a string) will be a param name.
+//
+// Fields of a struct can be more specified by json annotations:
+//
+//   type Condition struct {
+//      Start  time.Time  `json:"start"`
+//      Field  string     `json:"field"`
+//      Value  float64    `json:"value"`
+//   }
+//
+//   cond  := Condition {
+//	  "iot_center",
+//	  "Temperature",
+//	  "-10m",
+//	  23.0,
+//  }
+//
+// Parameters are then accessed via the params object:
+//
+//  query:= `from(bucket: "environment")
+//	 |> range(start: time(v: params.start))
+//	 |> filter(fn: (r) => r._measurement == "air")
+//	 |> filter(fn: (r) => r._field == params.field)
+//	 |> filter(fn: (r) => r._value > params.value)`
+//
+// And used in the call to Query:
+//
+//  result, err := client.Query(ctx, query, cond);
+//
+// Use QueryResultReader.NextSection() for navigation to the sections in the query result set.
+//
+// Use QueryResultReader.NextRow() for iterating over rows in the section.
+//
+// Read the row raw data using QueryResultReader.Row()
+// or deserialize data into a struct or a slice via QueryResultReader.Decode:
+//
+//  val := &struct {
+//	 Time  time.Time	`csv:"_time"`
+//	 Temp  float64		`csv:"_value"`
+//	 Sensor string		`csv:"sensor"`
+//  }{}
+//  err = result.Decode(val)
+//
+func (c *Client) Query(ctx context.Context, query string, queryParams interface{}) (*QueryResultReader, error) {
+	if err := checkParamsType(queryParams); err != nil {
+		return nil, err
+	}
+	queryURL, _ := c.apiURL.Parse("query")
+
+	q := queryBody{
+		Dialect: defaultDialect,
+		Query:   query,
+		Type:    "flux",
+		Params:  queryParams,
+	}
+	qrJSON, err := json.Marshal(q)
+	if err != nil {
+		return nil, err
+	}
+	resp, err := c.makeAPICall(ctx, httpParams{
+		endpointURL: queryURL,
+		httpMethod:  "POST",
+		headers:     map[string]string{"Content-Type": "application/json"},
+		queryParams: url.Values{"org": []string{c.params.Organization}},
+		body:        bytes.NewReader(qrJSON),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return NewQueryResultReader(resp.Body), nil
+}
+
+// checkParamsType validates the value is struct with simple type fields
+// or a map with key as string and value as a simple type
+func checkParamsType(p interface{}) error {
+	if p == nil {
+		return nil
+	}
+	t := reflect.TypeOf(p)
+	v := reflect.ValueOf(p)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+	if t.Kind() != reflect.Struct && t.Kind() != reflect.Map {
+		return fmt.Errorf("cannot use %v as query params", t)
+	}
+	switch t.Kind() {
+	case reflect.Struct:
+		fields := reflect.VisibleFields(t)
+		for _, f := range fields {
+			fv := v.FieldByIndex(f.Index)
+			t := getFieldType(fv)
+			if !validParamType(t) {
+				return fmt.Errorf("cannot use field '%s' of type '%v' as a query param", f.Name, t)
+			}
+
+		}
+	case reflect.Map:
+		key := t.Key()
+		if key.Kind() != reflect.String {
+			return fmt.Errorf("cannot use map key of type '%v' for query param name", key)
+		}
+		for _, k := range v.MapKeys() {
+			f := v.MapIndex(k)
+			t := getFieldType(f)
+			if !validParamType(t) {
+				return fmt.Errorf("cannot use map value type '%v' as a query param", t)
+			}
+		}
+	}
+	return nil
+}
+
+// getFieldType extracts type of value
+func getFieldType(v reflect.Value) reflect.Type {
+	t := v.Type()
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+	if t.Kind() == reflect.Interface && !v.IsNil() {
+		t = reflect.ValueOf(v.Interface()).Type()
+	}
+	return t
+}
+
+// timeType is the exact type for the Time
+var timeType = reflect.TypeOf(time.Time{})
+
+// validParamType validates that t is primitive type or string or interface
+func validParamType(t reflect.Type) bool {
+	return (t.Kind() > reflect.Invalid && t.Kind() < reflect.Complex64) ||
+		t.Kind() == reflect.String ||
+		t == timeType
+}
diff --git a/influxclient/error.go b/influxclient/error.go
new file mode 100644
index 0000000..ea86a0d
--- /dev/null
+++ b/influxclient/error.go
@@ -0,0 +1,28 @@
+package influxclient
+
+import "fmt"
+
+// ServerError holds InfluxDB server error info
+// ServerError represents an error returned from an InfluxDB API server.
+type ServerError struct {
+	// Code holds the Influx error code, or empty if the code is unknown.
+	Code string `json:"code"`
+	// Message holds the error message.
+	Message string `json:"message"`
+	// StatusCode holds the HTTP response status code.
+	StatusCode int `json:"-"`
+}
+
+// NewServerError returns new with just a message
+func NewServerError(message string) *ServerError {
+	return &ServerError{Message: message}
+}
+
+// Error implements Error interface
+func (e *ServerError) Error() string {
+	if e.Code != "" {
+		return fmt.Sprintf("%s: %s", e.Code, e.Message)
+	} else {
+		return e.Message
+	}
+}
diff --git a/influxclient/version.go b/influxclient/version.go
new file mode 100644
index 0000000..970f8ca
--- /dev/null
+++ b/influxclient/version.go
@@ -0,0 +1,15 @@
+// Copyright 2021 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
+package influxclient
+
+import (
+	"runtime"
+)
+
+// version defines current version
+const version = "3.0.0alpha1"
+
+// userAgent header value
+const userAgent = "influxdb-client-go/" + version + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"
diff --git a/inluxclient/client.go b/inluxclient/client.go
deleted file mode 100644
index fd03403..0000000
--- a/inluxclient/client.go
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright 2021 InfluxData, Inc. All rights reserved.
-// Use of this source code is governed by MIT
-// license that can be found in the LICENSE file.
-
-// Package influxclient provides client for InfluxDB server.
-package influxclient
-
-import (
-	"errors"
-	"fmt"
-	"net/http"
-	"net/url"
-	"strings"
-)
-
-// Params holds the parameters for creating a new client.
-// The only mandatory field is ServerURL. AuthToken is also important
-// if authentication was not done outside this client.
-type Params struct {
-	// ServerURL holds the URL of the InfluxDB server to connect to.
-	// This must be non-empty. E.g. http://localhost:8086
-	ServerURL string
-
-	// AuthToken holds the authorization token for the API.
-	// This can be obtained through the GUI web browser interface.
-	AuthToken string
-
-	// HTTPClient is used to make API requests.
-	//
-	// This can be used to specify a custom TLS configuration
-	// (TLSClientConfig), a custom request timeout (Timeout),
-	// or other customization as required.
-	//
-	// It HTTPClient is nil, http.DefaultClient will be used.
-	HTTPClient *http.Client
-}
-
-// Client implements an InfluxDB client.
-type Client struct {
-	// Configuration params.
-	params Params
-	// Pre-created Authorization HTTP header value.
-	authorization string
-	// Cached base server API URL.
-	apiURL *url.URL
-}
-
-// New creates new Client with given Params, where ServerURL and AuthToken are mandatory.
-func New(params Params) (*Client, error) {
-	c := &Client{params: params}
-	if params.ServerURL == "" {
-		return nil, errors.New("empty server URL")
-	}
-	if c.params.AuthToken != "" {
-		c.authorization = "Token " + c.params.AuthToken
-	}
-	if c.params.HTTPClient == nil {
-		c.params.HTTPClient = http.DefaultClient
-	}
-
-	serverAddress := params.ServerURL
-	if !strings.HasSuffix(serverAddress, "/") {
-		// For subsequent path parts concatenation, url has to end with '/'
-		serverAddress = params.ServerURL + "/"
-	}
-	var err error
-	// Prepare server API URL
-	c.apiURL, err = url.Parse(serverAddress + "api/v2/")
-	if err != nil {
-		return nil, fmt.Errorf("error parsing server URL: %w", err)
-	}
-	return c, nil
-}


Test patch:
diff --git a/influxclient/client_query_test.go b/influxclient/client_query_test.go
new file mode 100644
index 0000000..9c31162
--- /dev/null
+++ b/influxclient/client_query_test.go
@@ -0,0 +1,186 @@
+package influxclient
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestQuery(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "text/csv")
+		w.WriteHeader(200)
+		w.Write([]byte(`#group,false,false,true,true,false,true,true,true,false,false,false
+#default,_result,,,,,,,,,,
+#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,string,string,string,double,double,double
+,result,table,_start,_stop,_time,deviceId,location,sensor,air_hum,air_press,air_temp
+,,0,2021-10-19T14:39:57.464357168Z,2021-10-19T14:54:57.464357168Z,2021-10-19T14:40:21.833564544Z,2663346492,saman-home-room-0-1,BME280,48.8,1022.28,22.73
+,,0,2021-10-19T14:39:57.464357168Z,2021-10-19T14:54:57.464357168Z,2021-10-19T14:41:29.840881203Z,2663346492,saman-home-room-0-1,BME280,49.2,1022.34,22.7`))
+	}))
+	defer ts.Close()
+	client, err := New(Params{ServerURL: ts.URL})
+	require.NoError(t, err)
+	res, err := client.Query(context.Background(), "query", nil)
+	require.NoError(t, err)
+	defer res.Close()
+	i := 0
+	for res.NextSection() && res.Err() == nil {
+		for res.NextRow() {
+			i++
+		}
+	}
+	require.NoError(t, res.Err())
+	assert.Equal(t, 2, i)
+
+}
+
+func TestQueryError(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(400)
+		w.Write([]byte(`{"code":"invalid","message":"compilation failed: error at @1:170-1:171: invalid expression @1:167-1:168: |"}`))
+	}))
+	defer ts.Close()
+	client, err := New(Params{ServerURL: ts.URL})
+	require.NoError(t, err)
+	res, err := client.Query(context.Background(), "query", nil)
+	assert.Nil(t, res)
+	require.Error(t, err)
+	assert.Equal(t, `invalid: compilation failed: error at @1:170-1:171: invalid expression @1:167-1:168: |`, err.Error())
+}
+
+func TestQueryParamsTypes(t *testing.T) {
+	var i int8 = 1
+	var paramsTypeTests = []struct {
+		testName    string
+		params      interface{}
+		expectError string
+	}{
+		{
+			"structWillAllSupportedTypes",
+			struct {
+				B   bool
+				I   int
+				I8  int8
+				I16 int16
+				I32 int32
+				I64 int64
+				U   uint
+				U8  uint8
+				U16 uint16
+				U32 uint32
+				U64 uint64
+				F32 float32
+				F64 float64
+				D   time.Duration
+				T   time.Time
+			}{},
+			"",
+		},
+		{
+			"structWithInvalidFieldEmptyInterface",
+			struct {
+				F interface{}
+			}{},
+			"cannot use field 'F' of type 'interface {}' as a query param",
+		},
+		{
+			"structWithFieldAsValidInterfaceValue",
+			struct {
+				F interface{}
+			}{"string"},
+			"",
+		},
+		{
+			"structAsPointer",
+			&struct {
+				S string
+			}{"a"},
+			"",
+		},
+		{
+			"structWithInvalidFieldAsMap",
+			struct {
+				M map[string]string
+			}{},
+			"cannot use field 'M' of type 'map[string]string' as a query param",
+		},
+		{
+			"structWithFieldAsPointer",
+			struct {
+				P *int8
+			}{&i},
+			"",
+		},
+		{
+			"mapOfBool",
+			map[string]bool{},
+			"",
+		},
+		{
+			"mapOfFloat64",
+			map[string]float64{},
+			"",
+		},
+		{
+			"mapOfString",
+			map[string]string{},
+			"",
+		},
+		{
+			"mapOfTime",
+			map[string]time.Time{},
+			"",
+		},
+		{
+			"mapOfInterfaceEmpty",
+			map[string]interface{}{},
+			"",
+		},
+		{
+			"mapOfInterfaceWithValidValues",
+			map[string]interface{}{"s": "s", "t": time.Now()},
+			"",
+		},
+		{
+			"mapOfInterfaceWithStructInvalid",
+			map[string]interface{}{"s": struct {
+				a int
+			}{1}},
+			"cannot use map value type 'struct { a int }' as a query param",
+		},
+		{
+			"mapOfStructInvalid",
+			map[string]struct {
+				a int
+			}{"a": {1}},
+			"cannot use map value type 'struct { a int }' as a query param",
+		},
+		{
+			"mapWithInvalidKey",
+			map[int]string{},
+			"cannot use map key of type 'int' for query param name",
+		},
+		{
+			"invalidParamsType",
+			0,
+			"cannot use int as query params",
+		},
+	}
+	for _, test := range paramsTypeTests {
+		t.Run(test.testName, func(t *testing.T) {
+			err := checkParamsType(test.params)
+			if test.expectError != "" {
+				require.Error(t, err)
+				require.Equal(t, test.expectError, err.Error())
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
diff --git a/influxclient/client_test.go b/influxclient/client_test.go
new file mode 100644
index 0000000..71fab8f
--- /dev/null
+++ b/influxclient/client_test.go
@@ -0,0 +1,153 @@
+// Copyright 2021 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
+package influxclient
+
+import (
+	"context"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestNew(t *testing.T) {
+	_, err := New(Params{})
+	require.Error(t, err)
+	assert.Equal(t, "empty server URL", err.Error())
+
+	c, err := New(Params{ServerURL: "http://localhost:8086"})
+	require.NoError(t, err)
+	assert.Equal(t, "http://localhost:8086", c.params.ServerURL)
+	assert.Equal(t, "http://localhost:8086/api/v2/", c.apiURL.String())
+	assert.Equal(t, "", c.authorization)
+
+	_, err = New(Params{ServerURL: "localhost\n"})
+	if assert.Error(t, err) {
+		assert.True(t, strings.HasPrefix(err.Error(), "error parsing server URL:"))
+	}
+
+	c, err = New(Params{ServerURL: "http://localhost:8086", AuthToken: "my-token"})
+	require.NoError(t, err)
+	assert.Equal(t, "Token my-token", c.authorization)
+}
+
+func TestURLs(t *testing.T) {
+	urls := []struct {
+		serverURL    string
+		serverAPIURL string
+	}{
+		{"http://host:8086", "http://host:8086/api/v2/"},
+		{"http://host:8086/", "http://host:8086/api/v2/"},
+		{"http://host:8086/path", "http://host:8086/path/api/v2/"},
+		{"http://host:8086/path/", "http://host:8086/path/api/v2/"},
+		{"http://host:8086/path1/path2/path3", "http://host:8086/path1/path2/path3/api/v2/"},
+		{"http://host:8086/path1/path2/path3/", "http://host:8086/path1/path2/path3/api/v2/"},
+	}
+	for _, turl := range urls {
+		t.Run(turl.serverURL, func(t *testing.T) {
+			c, err := New(Params{ServerURL: turl.serverURL})
+			require.NoError(t, err)
+			assert.Equal(t, turl.serverURL, c.params.ServerURL)
+			assert.Equal(t, turl.serverAPIURL, c.apiURL.String())
+		})
+	}
+}
+
+func TestResolveErrorMessage(t *testing.T) {
+	errMsg := "compilation failed: error at @1:170-1:171: invalid expression @1:167-1:168: |"
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(400)
+		_, _ = w.Write([]byte(`{"code":"invalid","message":"` + errMsg + `"}`))
+	}))
+	defer ts.Close()
+	client, err := New(Params{ServerURL: ts.URL})
+	require.NoError(t, err)
+	turl, err := url.Parse(ts.URL)
+	require.NoError(t, err)
+	res, err := client.makeAPICall(context.Background(), httpParams{
+		endpointURL: turl,
+		queryParams: nil,
+		httpMethod:  "GET",
+		headers:     nil,
+		body:        nil,
+	})
+	assert.Nil(t, res)
+	require.Error(t, err)
+	assert.Equal(t, "invalid: "+errMsg, err.Error())
+}
+
+func TestResolveErrorHTML(t *testing.T) {
+	html := `<html><body><h1>Not found</h1></body></html>`
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "text/html")
+		w.WriteHeader(404)
+		_, _ = w.Write([]byte(html))
+	}))
+	defer ts.Close()
+	client, err := New(Params{ServerURL: ts.URL})
+	require.NoError(t, err)
+	turl, err := url.Parse(ts.URL)
+	require.NoError(t, err)
+	res, err := client.makeAPICall(context.Background(), httpParams{
+		endpointURL: turl,
+		queryParams: nil,
+		httpMethod:  "GET",
+		headers:     nil,
+		body:        nil,
+	})
+	assert.Nil(t, res)
+	require.Error(t, err)
+	assert.Equal(t, html, err.Error())
+}
+
+func TestResolveErrorV1(t *testing.T) {
+	errMsg := "compilation failed: error at @1:170-1:171: invalid expression @1:167-1:168: |"
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(400)
+		_, _ = w.Write([]byte(`{"error": "` + errMsg + `"}`))
+	}))
+	defer ts.Close()
+	client, err := New(Params{ServerURL: ts.URL})
+	require.NoError(t, err)
+	turl, err := url.Parse(ts.URL)
+	require.NoError(t, err)
+	res, err := client.makeAPICall(context.Background(), httpParams{
+		endpointURL: turl,
+		queryParams: nil,
+		httpMethod:  "GET",
+		headers:     nil,
+		body:        nil,
+	})
+	assert.Nil(t, res)
+	require.Error(t, err)
+	assert.Equal(t, errMsg, err.Error())
+}
+
+func TestResolveErrorNoError(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(500)
+	}))
+	defer ts.Close()
+	client, err := New(Params{ServerURL: ts.URL})
+	require.NoError(t, err)
+	turl, err := url.Parse(ts.URL)
+	require.NoError(t, err)
+	res, err := client.makeAPICall(context.Background(), httpParams{
+		endpointURL: turl,
+		queryParams: nil,
+		httpMethod:  "GET",
+		headers:     nil,
+		body:        nil,
+	})
+	assert.Nil(t, res)
+	require.Error(t, err)
+	assert.Equal(t, `500 Internal Server Error`, err.Error())
+}
diff --git a/influxclient/examples_test.go b/influxclient/examples_test.go
new file mode 100644
index 0000000..d58fa24
--- /dev/null
+++ b/influxclient/examples_test.go
@@ -0,0 +1,71 @@
+package influxclient_test
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"text/tabwriter"
+	"time"
+
+	"github.com/influxdata/influxdb-client-go/influxclient"
+)
+
+func ExampleClient_Query() {
+	// Create client
+	client, err := influxclient.New(influxclient.Params{
+		ServerURL:    "https://eu-central-1-1.aws.cloud2.influxdata.com/",
+		AuthToken:    "my-token",
+		Organization: "my-org",
+	})
+
+	// Define query parameters
+	params := struct {
+		Since       string  `json:"since"`
+		GreaterThan float64 `json:"greaterThan"`
+	}{
+		"-10m",
+		23.0,
+	}
+	// Prepare a query
+	query := `from(bucket: "iot_center") 
+		|> range(start: duration(v: params.since)) 
+		|> filter(fn: (r) => r._measurement == "environment")
+		|> filter(fn: (r) => r._field == "Temperature")
+		|> filter(fn: (r) => r._value > params.greaterThan)`
+
+	// Execute query
+	res, err := client.Query(context.Background(), query, params)
+	if err != nil {
+		panic(err)
+	}
+
+	// Make sure query result is always closed
+	defer res.Close()
+
+	// Declare custom type for data
+	val := &struct {
+		Time   time.Time `csv:"_time"`
+		Temp   float64   `csv:"_value"`
+		Sensor string    `csv:"sensor"`
+	}{}
+
+	tw := tabwriter.NewWriter(os.Stdout, 10, 4, 2, ' ', 0)
+	fmt.Fprintf(tw, "Time\tTemp\tSensor\n")
+
+	// Iterate over result set
+	for res.NextSection() {
+		for res.NextRow() {
+			err = res.Decode(val)
+			if err != nil {
+				fmt.Fprintf(tw, "%v\n", err)
+				continue
+			}
+			fmt.Fprintf(tw, "%s\t%.2f\t%s\n", val.Time.String(), val.Temp, val.Sensor)
+		}
+	}
+	tw.Flush()
+	if res.Err() != nil {
+		panic(res.Err())
+	}
+
+}
diff --git a/inluxclient/query.go b/influxclient/query.go
similarity index 100%
rename from inluxclient/query.go
rename to influxclient/query.go
diff --git a/inluxclient/query_test.go b/influxclient/query_test.go
similarity index 99%
rename from inluxclient/query_test.go
rename to influxclient/query_test.go
index 94fb1c2..9cf7db2 100644
--- a/inluxclient/query_test.go
+++ b/influxclient/query_test.go
@@ -12,7 +12,7 @@ import (
 	"time"
 
 	"github.com/influxdata/influxdb-client-go/annotatedcsv"
-	influxclient "github.com/influxdata/influxdb-client-go/inluxclient"
+	influxclient "github.com/influxdata/influxdb-client-go/influxclient"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
diff --git a/inluxclient/client_test.go b/inluxclient/client_test.go
deleted file mode 100644
index 0bf5850..0000000
--- a/inluxclient/client_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2021 InfluxData, Inc. All rights reserved.
-// Use of this source code is governed by MIT
-// license that can be found in the LICENSE file.
-
-package influxclient
-
-import (
-	"github.com/stretchr/testify/assert"
-	"strings"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestNew(t *testing.T) {
-	_, err := New(Params{})
-	require.Error(t, err)
-	assert.Equal(t, "empty server URL", err.Error())
-
-	c, err := New(Params{ServerURL: "http://localhost:8086"})
-	require.NoError(t, err)
-	assert.Equal(t, "http://localhost:8086", c.params.ServerURL)
-	assert.Equal(t, "http://localhost:8086/api/v2/", c.apiURL.String())
-	assert.Equal(t, "", c.authorization)
-
-	_, err = New(Params{ServerURL: "localhost\n"})
-	if assert.Error(t, err) {
-		assert.True(t, strings.HasPrefix(err.Error(), "error parsing server URL:"))
-	}
-
-	c, err = New(Params{ServerURL: "http://localhost:8086", AuthToken: "my-token"})
-	require.NoError(t, err)
-	assert.Equal(t, "Token my-token", c.authorization)
-}
-
-func TestURLs(t *testing.T) {
-	urls := []struct {
-		serverURL    string
-		serverAPIURL string
-	}{
-		{"http://host:8086", "http://host:8086/api/v2/"},
-		{"http://host:8086/", "http://host:8086/api/v2/"},
-		{"http://host:8086/path", "http://host:8086/path/api/v2/"},
-		{"http://host:8086/path/", "http://host:8086/path/api/v2/"},
-		{"http://host:8086/path1/path2/path3", "http://host:8086/path1/path2/path3/api/v2/"},
-		{"http://host:8086/path1/path2/path3/", "http://host:8086/path1/path2/path3/api/v2/"},
-	}
-	for _, url := range urls {
-		t.Run(url.serverURL, func(t *testing.T) {
-			c, err := New(Params{ServerURL: url.serverURL})
-			require.NoError(t, err)
-			assert.Equal(t, url.serverURL, c.params.ServerURL)
-			assert.Equal(t, url.serverAPIURL, c.apiURL.String())
-		})
-	}
-}