Repository: luraproject/lura

Issue:
Filtered query strings on proxy level appears in request
**Describe the bug**
Filtered query strings on proxy level appears in request

**To Reproduce**
Steps to reproduce the behavior:
Configuration used:
```
{
    "$schema": "https://www.krakend.io/schema/v3.json",
    "version": 3,
    "name": "KrakenD Community API Gateway",
    "port": 80,
    "host": ["http://localhost:80"],
    "timeout": "3000ms",
    "debug_endpoint": true,
    "endpoints": [
        {
            "endpoint": "/test",
            "method": "GET",
            "backend": [
                {
                    "encoding": "json",
                    "method": "GET",
                    "host": ["http://localhost:80"],
                    "input_headers": ["test"],
                    "input_query_strings": ["test"],
                    "url_pattern": "/__debug/"
                }
            ],
            "input_headers": ["test", "test2"],
            "input_query_strings": ["test", "test2"]
        }
    ],
    "sequential_start": false
}
```
Steps to run the software:
1. KrakenD CE docker image/local build (go version go1.24.5 darwin/arm64 for example)
2. Run curl
```
curl --location 'http://localhost:80/test?test=test&test2=test2&test3=test3' \
--header 'test: test' \
--header 'test2: test2' \
--header 'test3: test3'
```

**Expected behavior**
Filtered query strings must not be present in request

**Logs**
```
2025/08/05 19:08:26 KRAKEND DEBUG: [ENDPOINT: /__debug/*] Method: GET
2025/08/05 19:08:26 KRAKEND DEBUG: [ENDPOINT: /__debug/*] URL: /__debug/?test=test&test2=test2
2025/08/05 19:08:26 KRAKEND DEBUG: [ENDPOINT: /__debug/*] Query: map[test:[test] test2:[test2]]
2025/08/05 19:08:26 KRAKEND DEBUG: [ENDPOINT: /__debug/*] Params: [{param /}]
2025/08/05 19:08:26 KRAKEND DEBUG: [ENDPOINT: /__debug/*] Headers: map[Accept-Encoding:[gzip] Test:[test] User-Agent:[Go-http-client/1.1]]
2025/08/05 19:08:26 KRAKEND DEBUG: [ENDPOINT: /__debug/*] Body: 
[GIN] 2025/08/05 - 19:08:26 | 200 |     157.584µs |             ::1 | GET      "/__debug/?test=test&test2=test2"
[GIN] 2025/08/05 - 19:08:26 | 200 |    2.657791ms |             ::1 | GET      "/test?test=test&test2=test2&test3=test3"
```

Discussion:
@EvgeniyGorbunenko: PR with fix
https://github.com/luraproject/lura/pull/764@github-actions: This issue was marked as resolved a long time ago and now has been automatically locked as there has not been any recent activity after it. You can still open a new issue and reference this link.

Solution patch:
diff --git a/README.md b/README.md
index 76eb6f9..f503f61 100644
--- a/README.md
+++ b/README.md
@@ -49,11 +49,11 @@ The source code for the [Lura project](https://luraproject.org) framework. It is
 
 Use this repository if you want to **build from source your API Gateway** or if you want to **reuse the components in another application**.
 
-If you need a fully functional API Gateway you can [download the KrakenD binary for your architecture](http://www.krakend.io/download) or [build it yourself](https://github.com/krakendio/krakend-ce).
+If you need a fully functional API Gateway you can [download the KrakenD binary for your architecture](http://www.krakend.io/download) or [build it yourself](https://github.com/krakend/krakend-ce).
 
 
 ## Library Usage
-The Lura project is presented as a **Go library** that you can include in your own Go application to build a powerful proxy or API gateway. For a complete example, check the [KrakenD CE repository](https://github.com/krakendio/krakend-ce).
+The Lura project is presented as a **Go library** that you can include in your own Go application to build a powerful proxy or API gateway. For a complete example, check the [KrakenD CE repository](https://github.com/krakend/krakend-ce).
 
 Of course, you will need [Go installed](https://golang.org/doc/install) in your system to compile the code.
 
diff --git a/go.mod b/go.mod
index 29477f6..2a20e93 100644
--- a/go.mod
+++ b/go.mod
@@ -10,13 +10,13 @@ require (
 	github.com/gin-gonic/gin v1.9.1
 	github.com/go-chi/chi/v5 v5.2.2
 	github.com/gorilla/mux v1.8.1
-	github.com/krakendio/flatmap v1.1.1
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/urfave/negroni/v2 v2.0.2
 	github.com/valyala/fastrand v1.1.0
 )
 
 require (
+	github.com/krakend/flatmap v1.2.0
 	golang.org/x/net v0.41.0
 	golang.org/x/sync v0.15.0
 	golang.org/x/text v0.26.0
diff --git a/go.sum b/go.sum
index e60dd09..c3959a8 100644
--- a/go.sum
+++ b/go.sum
@@ -46,8 +46,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/krakendio/flatmap v1.1.1 h1:rGBNVpBY0pMk6cLOwerVzoKY4HELnpu0xvqB231lOCQ=
-github.com/krakendio/flatmap v1.1.1/go.mod h1:KBuVkiH5BcBFRa5A1HdSHDn8a8LzsyRTKZArX0vqTbo=
+github.com/krakend/flatmap v1.2.0 h1:4NPncAKH7Ca/t878kbGlc/LPWLa+m4sgBhs8aT2Q1SY=
+github.com/krakend/flatmap v1.2.0/go.mod h1:FyCOoggdVlWr31+aQaOFvBxlMgYfCE5yuwInLbW1/jM=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
diff --git a/proxy/factory.go b/proxy/factory.go
index be2df8c..8673d3d 100644
--- a/proxy/factory.go
+++ b/proxy/factory.go
@@ -92,11 +92,13 @@ func (pf defaultFactory) newStack(backend *config.Backend) (p Proxy) {
 	p = NewBackendPluginMiddleware(pf.logger, backend)(p)
 	p = NewGraphQLMiddleware(pf.logger, backend)(p)
 	p = NewFilterHeadersMiddleware(pf.logger, backend)(p)
-	p = NewFilterQueryStringsMiddleware(pf.logger, backend)(p)
 	p = NewLoadBalancedMiddlewareWithSubscriberAndLogger(pf.logger, pf.subscriberFactory(backend))(p)
 	if backend.ConcurrentCalls > 1 {
 		p = NewConcurrentMiddlewareWithLogger(pf.logger, backend)(p)
 	}
 	p = NewRequestBuilderMiddlewareWithLogger(pf.logger, backend)(p)
+	// we need to filter the input query strings before the request is constructed
+	// so the query strings are properly added to the URL:
+	p = NewFilterQueryStringsMiddleware(pf.logger, backend)(p)
 	return
 }
diff --git a/proxy/formatter.go b/proxy/formatter.go
index 9bd53fd..2564552 100644
--- a/proxy/formatter.go
+++ b/proxy/formatter.go
@@ -7,7 +7,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/krakendio/flatmap/tree"
+	"github.com/krakend/flatmap/tree"
 	"github.com/luraproject/lura/v2/config"
 	"github.com/luraproject/lura/v2/logging"
 )
diff --git a/proxy/http.go b/proxy/http.go
index 726c548..df2dc18 100644
--- a/proxy/http.go
+++ b/proxy/http.go
@@ -3,8 +3,10 @@
 package proxy
 
 import (
+	"bytes"
 	"context"
 	"fmt"
+	"io"
 	"net/http"
 	"strconv"
 	"strings"
@@ -45,11 +47,46 @@ func NewHTTPProxyWithHTTPExecutor(remote *config.Backend, re client.HTTPRequestE
 	return NewHTTPProxyDetailed(remote, re, client.GetHTTPStatusHandler(remote), rp)
 }
 
+const (
+	clientHTTPOptions            string = "backend/http/client"
+	clientHTTPOptionRedirectPost string = "send_body_on_redirect"
+)
+
+// redirectPostReaderFactory checks if the clientHTTPOptionRedirectPost is enabled
+// This will read the body and return a bytes.Buffer with the body content, so we
+// delegate to http.NewRequest the population of request.GetBody so a redirect (307
+// and 308) is executed while maintaining the method and the body
+// This is necessary since the request comes from another http.Client and it's not
+// a concrete type that can be copied but just a io.ReaderCloser (*http.body)
+func redirectPostReaderFactory(cfg *config.Backend) func(r io.ReadCloser) io.Reader {
+	emptyFactory := func(r io.ReadCloser) io.Reader { return r }
+	if cfg == nil || cfg.ExtraConfig == nil {
+		return emptyFactory
+	}
+	v, ok := cfg.ExtraConfig[clientHTTPOptions].(map[string]interface{})
+	if !ok {
+		return emptyFactory
+	}
+	if opt, ok := v[clientHTTPOptionRedirectPost].(bool); !ok || !opt {
+		return emptyFactory
+	}
+	return func(r io.ReadCloser) io.Reader {
+		if r == http.NoBody || r == nil {
+			return r
+		}
+		buf := new(bytes.Buffer)
+		buf.ReadFrom(r)
+		r.Close()
+		return buf
+	}
+}
+
 // NewHTTPProxyDetailed creates a http proxy with the injected configuration, HTTPRequestExecutor,
 // Decoder and HTTPResponseParser
-func NewHTTPProxyDetailed(_ *config.Backend, re client.HTTPRequestExecutor, ch client.HTTPStatusHandler, rp HTTPResponseParser) Proxy {
+func NewHTTPProxyDetailed(cfg *config.Backend, re client.HTTPRequestExecutor, ch client.HTTPStatusHandler, rp HTTPResponseParser) Proxy {
+	bodyFactory := redirectPostReaderFactory(cfg)
 	return func(ctx context.Context, request *Request) (*Response, error) {
-		requestToBackend, err := http.NewRequest(strings.ToTitle(request.Method), request.URL.String(), request.Body)
+		requestToBackend, err := http.NewRequest(strings.ToTitle(request.Method), request.URL.String(), bodyFactory(request.Body))
 		if err != nil {
 			return nil, err
 		}



Test patch:
diff --git a/proxy/http_test.go b/proxy/http_test.go
index 749fcf3..1d8bf9f 100644
--- a/proxy/http_test.go
+++ b/proxy/http_test.go
@@ -13,6 +13,7 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"strings"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -170,7 +171,7 @@ func TestNewHTTPProxy_badResponseBody(t *testing.T) {
 
 func TestNewHTTPProxy_badStatusCode(t *testing.T) {
 	expectedMethod := "GET"
-	backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
 		http.Error(w, "booom", 500)
 	}))
 	defer backendServer.Close()
@@ -488,3 +489,69 @@ func TestNewHTTPProxy_noopDecoder(t *testing.T) {
 		t.Error("unexpected content:", content)
 	}
 }
+
+func TestNewHTTPProxy_redirectWithBody(t *testing.T) {
+	var executed atomic.Uint64
+	expectedBody := `{"message":"redirected"}`
+	expectedResponse := `{"message":"ok"}`
+	backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		data, err := io.ReadAll(r.Body)
+		if err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+		if string(data) != expectedBody {
+			t.Errorf("invalid data: %s", string(data))
+			return
+		}
+		executed.Add(1)
+		w.Write([]byte(`{"message":"ok"}`))
+	}))
+	defer backendServer.Close()
+	redirServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		executed.Add(1)
+		http.Redirect(w, r, backendServer.URL, http.StatusPermanentRedirect)
+	}))
+	defer redirServer.Close()
+
+	rpURL, _ := url.Parse(redirServer.URL)
+	backend := config.Backend{
+		Decoder: encoding.JSONDecoder,
+		ExtraConfig: map[string]interface{}{
+			clientHTTPOptions: map[string]interface{}{
+				clientHTTPOptionRedirectPost: true,
+			},
+		},
+	}
+	request := Request{
+		Method: "POST",
+		Path:   "/",
+		URL:    rpURL,
+		Body:   newDummyReadCloser(expectedBody),
+	}
+	mustEnd := time.After(time.Duration(150) * time.Millisecond)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Millisecond)
+	defer cancel()
+	response, err := httpProxy(&backend)(ctx, &request)
+	if err != nil {
+		t.Errorf("The proxy propagated a backend errors: %s\n", err)
+		return
+	}
+	respData, err := json.Marshal(response.Data)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if string(respData) != expectedResponse {
+		t.Errorf("unexpected response data: '%s'", string(respData))
+	}
+	select {
+	case <-mustEnd:
+		t.Errorf("Error: expected response")
+	default:
+	}
+	if executed.Load() != 2 {
+		t.Errorf("number of executions should be 2 not %d", executed.Load())
+	}
+}