Repository: go-openapi/runtime
Pull request #209 (2021-08-24 06:32:59)

Allows base paths to include query parameters
This is a test case and a fix for https://github.com/go-openapi/runtime/issues/207.

When the base path of an operation contains a query string, those parameters are:

1. extracted before appending any path parameters to the path
2. merged with the query parameters passed by the client

In the case of a conflict between the parameters passed in the API call and the ones declared in the base path, the former prevails. This gives end control to clients.

jsilland: The linter flagged that I was ignoring the return value from `SetQueryParam` — which I think wasn't flagged in my setup because the actual return value from that function is always `nil`. I've added a case to handle that on the calling side.

Patch:
diff --git a/client/request.go b/client/request.go
index b7ea8b8..07ec972 100644
--- a/client/request.go
+++ b/client/request.go
@@ -278,7 +278,17 @@ DoneChoosingBodySource:
 	if r.pathPattern != "" && r.pathPattern != "/" && r.pathPattern[len(r.pathPattern)-1] == '/' {
 		reinstateSlash = true
 	}
-	urlPath := path.Join(basePath, r.pathPattern)
+
+	// In case the basePath includes hardcoded query parameters, parse those out before
+	// constructing the final path. The parameters themselves will be merged with the
+	// ones set by the client, with the priority given to the latter.
+	basePathURL, err := url.Parse(basePath)
+	if err != nil {
+		return nil, err
+	}
+	basePathQueryParams := basePathURL.Query()
+
+	urlPath := path.Join(basePathURL.Path, r.pathPattern)
 	for k, v := range r.pathParams {
 		urlPath = strings.Replace(urlPath, "{"+k+"}", url.PathEscape(v), -1)
 	}
@@ -291,6 +301,19 @@ DoneChoosingBodySource:
 		return nil, err
 	}
 
+	originalParams := r.GetQueryParams()
+
+	// Merge the query parameters extracted from the basePath with the ones set by
+	// the client in this struct. In case of conflict, the client wins.
+	for k, v := range basePathQueryParams {
+		_, present := originalParams[k]
+		if !present {
+			if err = r.SetQueryParam(k, v...); err != nil {
+				return nil, err
+			}
+		}
+	}
+
 	req.URL.RawQuery = r.query.Encode()
 	req.Header = r.header
 


Test patch:
diff --git a/client/request_test.go b/client/request_test.go
index 9979fd8..86d0b0f 100644
--- a/client/request_test.go
+++ b/client/request_test.go
@@ -517,6 +517,39 @@ func TestBuildRequest_BuildHTTP_EscapedPath(t *testing.T) {
 	}
 }
 
+func TestBuildRequest_BuildHTTP_BasePathWithParameters(t *testing.T) {
+	reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error {
+		_ = req.SetBodyParam(nil)
+		_ = req.SetQueryParam("hello", "world")
+		_ = req.SetPathParam("id", "1234")
+		return nil
+	})
+	r, _ := newRequest("POST", "/flats/{id}/", reqWrtr)
+
+	req, err := r.BuildHTTP(runtime.JSONMime, "/basepath?foo=bar", testProducers, nil)
+	if assert.NoError(t, err) && assert.NotNil(t, req) {
+		assert.Equal(t, "world", req.URL.Query().Get("hello"))
+		assert.Equal(t, "bar", req.URL.Query().Get("foo"))
+		assert.Equal(t, "/basepath/flats/1234/", req.URL.Path)
+	}
+}
+
+func TestBuildRequest_BuildHTTP_BasePathWithConflictingParameters(t *testing.T) {
+	reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error {
+		_ = req.SetBodyParam(nil)
+		_ = req.SetQueryParam("hello", "world")
+		_ = req.SetPathParam("id", "1234")
+		return nil
+	})
+	r, _ := newRequest("POST", "/flats/{id}/", reqWrtr)
+
+	req, err := r.BuildHTTP(runtime.JSONMime, "/basepath?hello=kitty", testProducers, nil)
+	if assert.NoError(t, err) && assert.NotNil(t, req) {
+		assert.Equal(t, "world", req.URL.Query().Get("hello"))
+		assert.Equal(t, "/basepath/flats/1234/", req.URL.Path)
+	}
+}
+
 type testReqFn func(*testing.T, *http.Request)
 
 type testRoundTripper struct {