Repository: avenga/couper
Pull request #93 (2021-01-14 19:45:28)

Fix query encoding


Patch:
diff --git a/handler/proxy.go b/handler/proxy.go
index 8eb0d2ed..76a1efe4 100644
--- a/handler/proxy.go
+++ b/handler/proxy.go
@@ -453,7 +453,11 @@ func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
 
 	// apply query params in hierarchical and logical order: delete, set, add
 	if attrOk && req != nil && beresp == nil { // just one way -> origin
-		values := req.URL.Query()
+		var modify bool
+
+		u := *req.URL
+		u.RawQuery = strings.ReplaceAll(u.RawQuery, "+", "%2B")
+		values := u.Query()
 
 		// not by name to ensure the order for all params
 		for _, attrs := range allAttributes.JustAllAttributes() {
@@ -466,6 +470,7 @@ func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
 				for _, key := range seetie.ValueToStringSlice(val) {
 					values.Del(key)
 				}
+				modify = true
 			}
 
 			attr, ok = attrs[attrSetQueryParams]
@@ -477,6 +482,7 @@ func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
 				for k, v := range options {
 					values[k] = v
 				}
+				modify = true
 			}
 
 			attr, ok = attrs[attrAddQueryParams]
@@ -492,10 +498,13 @@ func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
 						values[k] = append(values[k], v...)
 					}
 				}
+				modify = true
 			}
 		}
 
-		req.URL.RawQuery = values.Encode()
+		if modify {
+			req.URL.RawQuery = strings.ReplaceAll(values.Encode(), "+", "%20")
+		}
 	}
 
 	if beresp != nil && isCorsRequest(req) {


Test patch:
diff --git a/handler/proxy_test.go b/handler/proxy_test.go
index aaf7e123..fd6f20b1 100644
--- a/handler/proxy_test.go
+++ b/handler/proxy_test.go
@@ -398,7 +398,7 @@ func TestProxy_ServeHTTP_Validation(t *testing.T) {
 	helper := test.New(t)
 	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 		rw.Header().Set("Content-Type", "text/plain")
-		if req.URL.RawQuery == "404=" {
+		if req.URL.RawQuery == "404" {
 			rw.WriteHeader(http.StatusNotFound)
 		}
 		_, err := rw.Write([]byte("from upstream"))
diff --git a/internal/test/test_backend.go b/internal/test/test_backend.go
index d4568297..6d296dc8 100644
--- a/internal/test/test_backend.go
+++ b/internal/test/test_backend.go
@@ -46,6 +46,7 @@ func createAnythingHandler(status int) func(rw http.ResponseWriter, req *http.Re
 			Args, Query                        url.Values
 			Headers                            http.Header
 			Host                               string
+			RawQuery                           string
 			Path                               string
 			Method, RemoteAddr, Url, UserAgent string
 			ResponseStatus                     int
@@ -61,6 +62,7 @@ func createAnythingHandler(status int) func(rw http.ResponseWriter, req *http.Re
 			Path:           req.URL.Path,
 			RemoteAddr:     req.RemoteAddr,
 			Query:          req.URL.Query(),
+			RawQuery:       req.URL.RawQuery,
 			Url:            req.URL.String(),
 			UserAgent:      req.UserAgent(),
 			ResponseStatus: status,
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 054112ff..28cb6154 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -667,6 +667,46 @@ func TestHTTPServer_QueryParams(t *testing.T) {
 	}
 }
 
+func TestHTTPServer_QueryEncoding(t *testing.T) {
+	client := newClient()
+
+	config := "testdata/integration/endpoint_eval/10_couper.hcl"
+
+	type expectation struct {
+		RawQuery string
+	}
+
+	shutdown, _ := newCouper(config, test.New(t))
+
+	t.Run("Query-Encoding", func(subT *testing.T) {
+		helper := test.New(subT)
+
+		req, err := http.NewRequest(http.MethodGet, "http://example.com:8080?a=a%20a&x=x+x", nil)
+		helper.Must(err)
+
+		res, err := client.Do(req)
+		helper.Must(err)
+
+		resBytes, err := ioutil.ReadAll(res.Body)
+		helper.Must(err)
+
+		_ = res.Body.Close()
+
+		var jsonResult expectation
+		err = json.Unmarshal(resBytes, &jsonResult)
+		if err != nil {
+			t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
+		}
+
+		exp := expectation{RawQuery: "a=a%20a&space=a%20b%2Bc&x=x%2Bx"}
+		if !reflect.DeepEqual(jsonResult, exp) {
+			t.Errorf("\nwant: \n%#v\ngot: \n%#v", exp, jsonResult)
+		}
+	})
+
+	shutdown()
+}
+
 func TestHTTPServer_Endpoint_Evaluation(t *testing.T) {
 	client := newClient()
 
diff --git a/server/testdata/integration/endpoint_eval/10_couper.hcl b/server/testdata/integration/endpoint_eval/10_couper.hcl
new file mode 100644
index 00000000..0226c10e
--- /dev/null
+++ b/server/testdata/integration/endpoint_eval/10_couper.hcl
@@ -0,0 +1,18 @@
+server "api" {
+  api {
+    endpoint "/" {
+      backend = "anything"
+    }
+  }
+}
+
+definitions {
+  # backend origin within a definition block gets replaced with the integration test "anything" server.
+  backend "anything" {
+    origin = env.COUPER_TEST_BACKEND_ADDR
+
+    set_query_params = {
+      space = "a b+c"
+    }
+  }
+}