Repository: influxdata/influxdb-client-go (go)

Issue:
Adding URL prefix to client
I'm currently trying to use this client to write to a VictoriaMetrics instance. They say that v2 API calls work correctly (https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#url-format), but every time I try to write using this client, it appears the client is ignoring the prefix I set.

For example, if I set `http://localhost:8480/insert/0:0/influx` as my serverUrl, this client appears to use `http://localhost:8480/api/v2/write` instead of `http://localhost:8480/insert/0:0/influx/api/v2/write` as I would expect.

Am I doing something wrong in the client? My actual instantiation:

```
    client := influxdb2.NewClientWithOptions(
        fmt.Sprintf("http://%v:%v/insert/0:0/influx", hostname, port),
        "",
        influxdb2.DefaultOptions().SetBatchSize(BatchSize),
    )
    writeApi := client.WriteApi("", "telegraf")
```

The error I receive:

```
Jul 09 14:34:06 <> vminsert-prod[6281]: 2020-07-09T14:34:06.206Z        warn        VictoriaMetrics/lib/httpserver/httpserver.go:204        unsupported path requested: "/api/v2/write"
```

A similar error I received using the v1 client:

```root@<>:~# journalctl -xeu victoriametrics-vminsert | grep "write/write"
Jul 08 22:42:20 <> vminsert-prod[6281]: 2020-07-08T22:42:20.815Z        warn        VictoriaMetrics/lib/httpserver/httpserver.go:204        unsupported path requested: "/insert/0:0/influx/write/write"
```

In this case, we can see it used the full path I had defined:

```
c, err := client.NewHTTPClient(client.HTTPConfig{
                Addr: fmt.Sprintf("http://%v:%v/insert/0:0/influx/write", hostname, port),
        })
```

 Which leads me to believe the client is stripping any url prefix.

Pull request:
fix: Allow connecting to a server on a URL path (#149)
Closes #149

Properly concatenating URL path to the final API endpoint

- [X] CHANGELOG.md updated
- [X] Rebased/mergeable
- [X] Tests pass


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67d0008..534eb50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.4.0 [in progress]
+### Bug fixes 
+1. [#152](https://github.com/influxdata/influxdb-client-go/pull/152) Allow connecting to server on a URL path
+
 ## 1.3.0 [2020-06-19]
 ### Features
 1. [#131](https://github.com/influxdata/influxdb-client-go/pull/131) Labels API
diff --git a/client.go b/client.go
index 0f8719c..537b8a1 100644
--- a/client.go
+++ b/client.go
@@ -9,12 +9,13 @@ package influxdb2
 import (
 	"context"
 	"errors"
-	"github.com/influxdata/influxdb-client-go/api"
-	"github.com/influxdata/influxdb-client-go/internal/log"
+	"strings"
 	"sync"
 
+	"github.com/influxdata/influxdb-client-go/api"
 	"github.com/influxdata/influxdb-client-go/domain"
 	ihttp "github.com/influxdata/influxdb-client-go/internal/http"
+	"github.com/influxdata/influxdb-client-go/internal/log"
 )
 
 // Client provides API to communicate with InfluxDBServer.
@@ -85,7 +86,12 @@ func NewClient(serverUrl string, authToken string) Client {
 // Authentication token can be empty in case of connecting to newly installed InfluxDB server, which has not been set up yet.
 // In such case Setup will set authentication token
 func NewClientWithOptions(serverUrl string, authToken string, options *Options) Client {
-	service := ihttp.NewService(serverUrl, "Token "+authToken, options.httpOptions)
+	normServerURL := serverUrl
+	if !strings.HasSuffix(normServerURL, "/") {
+		// For subsequent path parts concatenation, url has to end with '/'
+		normServerURL = serverUrl + "/"
+	}
+	service := ihttp.NewService(normServerURL, "Token "+authToken, options.httpOptions)
 	client := &clientImpl{
 		serverUrl:   serverUrl,
 		options:     options,
diff --git a/internal/http/service.go b/internal/http/service.go
index 2f7bede..f445745 100644
--- a/internal/http/service.go
+++ b/internal/http/service.go
@@ -49,7 +49,7 @@ func NewService(serverUrl, authorization string, httpOptions *http2.Options) Ser
 	apiUrl, err := url.Parse(serverUrl)
 	serverApiUrl := serverUrl
 	if err == nil {
-		apiUrl, err = apiUrl.Parse("/api/v2/")
+		apiUrl, err = apiUrl.Parse("api/v2/")
 		if err == nil {
 			serverApiUrl = apiUrl.String()
 		}


Test patch:
diff --git a/client_test.go b/client_test.go
index f5b9fdf..13b1242 100644
--- a/client_test.go
+++ b/client_test.go
@@ -6,16 +6,45 @@ package influxdb2
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 	"time"
 
 	http2 "github.com/influxdata/influxdb-client-go/internal/http"
+	iwrite "github.com/influxdata/influxdb-client-go/internal/write"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
+func TestUrls(t *testing.T) {
+	urls := []struct {
+		serverURL      string
+		serverAPIURL   string
+		writeURLPrefix string
+	}{
+		{"http://host:9999", "http://host:9999/api/v2/", "http://host:9999/api/v2/write"},
+		{"http://host:9999/", "http://host:9999/api/v2/", "http://host:9999/api/v2/write"},
+		{"http://host:9999/path", "http://host:9999/path/api/v2/", "http://host:9999/path/api/v2/write"},
+		{"http://host:9999/path/", "http://host:9999/path/api/v2/", "http://host:9999/path/api/v2/write"},
+		{"http://host:9999/path1/path2/path3", "http://host:9999/path1/path2/path3/api/v2/", "http://host:9999/path1/path2/path3/api/v2/write"},
+		{"http://host:9999/path1/path2/path3/", "http://host:9999/path1/path2/path3/api/v2/", "http://host:9999/path1/path2/path3/api/v2/write"},
+	}
+	for _, url := range urls {
+		t.Run(url.serverURL, func(t *testing.T) {
+			c := NewClient(url.serverURL, "x")
+			ci := c.(*clientImpl)
+			assert.Equal(t, url.serverURL, ci.serverUrl)
+			assert.Equal(t, url.serverAPIURL, ci.httpService.ServerApiUrl())
+			ws := iwrite.NewService("org", "bucket", ci.httpService, c.Options().WriteOptions())
+			wu, err := ws.WriteUrl()
+			require.Nil(t, err)
+			assert.Equal(t, url.writeURLPrefix+"?bucket=bucket&org=org&precision=ns", wu)
+		})
+	}
+}
+
 func TestUserAgent(t *testing.T) {
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		time.Sleep(100 * time.Millisecond)
@@ -42,7 +71,7 @@ func TestServerError429(t *testing.T) {
 		w.Header().Set("Retry-After", "1")
 		w.Header().Set("Content-Type", "application/json")
 		w.WriteHeader(http.StatusTooManyRequests)
-		w.Write([]byte(`{"code":"too many requests", "message":"exceeded rate limit"}`))
+		_, _ = w.Write([]byte(`{"code":"too many requests", "message":"exceeded rate limit"}`))
 	}))
 
 	defer server.Close()
@@ -56,11 +85,27 @@ func TestServerError429(t *testing.T) {
 	assert.Equal(t, "exceeded rate limit", perror.Message)
 }
 
+func TestServerOnPath(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/proxy/0:0/influx/api/v2/write" {
+			w.WriteHeader(http.StatusNoContent)
+		} else {
+			w.WriteHeader(http.StatusInternalServerError)
+			_, _ = w.Write([]byte(fmt.Sprintf(`{"code":"internal server error", "message":"%s"}`, r.URL.Path)))
+		}
+	}))
+
+	defer server.Close()
+	c := NewClient(server.URL+"/proxy/0:0/influx/", "x")
+	err := c.WriteApiBlocking("o", "b").WriteRecord(context.Background(), "a,a=a a=1i")
+	require.Nil(t, err)
+}
+
 func TestServerErrorNonJSON(t *testing.T) {
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		time.Sleep(100 * time.Millisecond)
 		w.WriteHeader(http.StatusInternalServerError)
-		w.Write([]byte(`internal server error`))
+		_, _ = w.Write([]byte(`internal server error`))
 	}))
 
 	defer server.Close()
@@ -79,7 +124,7 @@ func TestServerErrorInflux1_8(t *testing.T) {
 		w.Header().Set("X-Influxdb-Error", "bruh moment")
 		w.Header().Set("Content-Type", "application/json")
 		w.WriteHeader(404)
-		w.Write([]byte(`{"error": "bruh moment"}`))
+		_, _ = w.Write([]byte(`{"error": "bruh moment"}`))
 	}))
 
 	defer server.Close()