-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix behavior for PathPrefixStrip #1638
Conversation
295c36e
to
ba24dc2
Compare
Example of malformed requests:
Note: They are all missing prefixed paths. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution -- looking forward to seeing a long-standing issue getting resolved!
I left a few comments.
middlewares/stripPrefix.go
Outdated
r.Header[forwardedPrefixHeader] = []string{prefix} | ||
r.RequestURI = r.URL.RequestURI() | ||
s.Handler.ServeHTTP(w, r) | ||
orgPrefix := strings.TrimSpace(prefix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose org
stands for original? If so, could we maybe rename it to orig
?
middlewares/stripPrefix.go
Outdated
r.Header[forwardedPrefixHeader] = []string{prefix} | ||
r.RequestURI = r.URL.RequestURI() | ||
s.Handler.ServeHTTP(w, r) | ||
orgPrefix := strings.TrimSpace(prefix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should also have a test for a prefix containing white spaces.
middlewares/stripPrefix.go
Outdated
prefix = orgPrefix | ||
if !strings.HasSuffix(prefix, "/") { | ||
prefix = orgPrefix + "/" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a more elegant and concise way to implement the previous block would be
prefix = strings.TrimSuffix(orgPrefix, "/") + "/"
middlewares/stripPrefix.go
Outdated
if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) { | ||
if !strings.HasPrefix(p, "/") { | ||
r.URL.Path = "/" + p | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly here:
r.URL.Path = "/" + strings.TrimPrefix(p, "/")
middlewares/stripPrefix_test.go
Outdated
) | ||
|
||
func TestStripPrefix(t *testing.T) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No blank line here.
middlewares/stripPrefix_test.go
Outdated
for _, test := range tests { | ||
resp, err := http.Get(server.URL + test.url) | ||
if err != nil { | ||
t.Fatal(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does err
give enough context to understand we ran into an error while sending the GET query?
When in doubt, I'd add a bit of context like
t.Fatalf("failed to send GET request: %s", err)
middlewares/stripPrefix_test.go
Outdated
} | ||
response, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
t.Fatal(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context question/comment from above applies here too.
middlewares/stripPrefix_test.go
Outdated
} | ||
|
||
if test.expectedResponse != string(response) { | ||
t.Errorf("Expected '%s' : '%s'\n", test.expectedResponse, response) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to have a newline in the error string.
middlewares/stripPrefix_test.go
Outdated
} | ||
|
||
if test.expectedResponse != string(response) { | ||
t.Errorf("Expected '%s' : '%s'\n", test.expectedResponse, response) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Go, error strings should name the actual value first and expected second. For instance
t.Errorf("got response '%s', expected '%s'", response, test.expectedResponse)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand, I was just following the other PathPrefixStripRegex example.
I'll add these fixes shortly.
middlewares/stripPrefix_test.go
Outdated
|
||
{url: "/c/api/123/", expectedCode: 200, expectedResponse: "/"}, | ||
{url: "/c/api/123/test3", expectedCode: 200, expectedResponse: "/test3"}, | ||
{url: "/c/api/abc/test4", expectedCode: 200, expectedResponse: "/c/api/abc/test4"}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we need all existing test cases here. For instance, what's the difference between /stat
and a/api
, or /stat/
and /c/api/123/
?
While it's not terribly bad to have too many test cases, ideally there should be one representative case per code path only.
@emilevauge @ldez what do you think, should we have this fix be part of 1.3? |
@timoreimann Updated with your requested changes. Let me know if there's anything else we need (rebase, etc). |
e956c3f
to
fcc4066
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good job 👍
middlewares/stripPrefix_test.go
Outdated
} | ||
|
||
suites := []struct { | ||
server *httptest.Server |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To enhance readability, create 2 types at the below of the file:
type stripPrefixTestSuite struct {
server *httptest.Server
desc string
tests []stripPrefixTestCase
}
type stripPrefixTestCase struct {
url string
expectedStatusCode int
expectedBody string
}
middlewares/stripPrefix_test.go
Outdated
|
||
for _, test := range suite.tests { | ||
resp, err := http.Get(suite.server.URL + test.url) | ||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use Testify, to enhance readability:
resp, err := http.Get(suite.server.URL + test.url)
require.NoError(t, err, "Failed to send GET request")
assert.Equal(t, test.expectedStatusCode, resp.StatusCode, "Unexpected status code")
if test.expectedBody != "" {
response, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err, "Failed to read response body")
assert.Equal(t, test.expectedBody, string(response), "Unexpected response received")
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, these changes were added as well as your above suggestions.
middlewares/stripPrefix_test.go
Outdated
}{ | ||
{ | ||
url: "/norules", | ||
expected: "", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for the expressivity of the test, you can drop all expected: ""
middlewares/stripPrefix_test.go
Outdated
{ | ||
url: "/", | ||
expected: "/", | ||
code: http.StatusOK, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you re-odering? Put code
before expected
middlewares/stripPrefix_test.go
Outdated
}) | ||
} | ||
|
||
type stripPrefixTestCase struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, put this 2 types at the bottom part of the file.
Will rebase as soon as we get a thumbs up. |
0afb0cb
to
deb8f5c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed a few problems with the new approach. See my remarks.
We also seem to have lost test cases verifying that the the first matching prefix will be applied. At the same time, I still think that a few cases are verifying existing behavior redundantly.
I went ahead and put together a proposal on what I think could be a more simple approach while solving the given problems. I pushed it here timoreimann@224f2a1 and would be curious to hear what you think.
Thanks!
middlewares/stripPrefix_test.go
Outdated
} | ||
|
||
type stripPrefixTestCase struct { | ||
url string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be better called path
.
middlewares/stripPrefix_test.go
Outdated
t.Parallel() | ||
defer suite.server.Close() | ||
|
||
for _, test := range suite.tests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The given nested suite/test cases approach comes with a few drawbacks:
- Since the description is attached to the suite, we can't see which test case exactly is failing.
- For the same reason, we cannot execute individual test cases.
- If one test in a suite fails, the entire suite is aborted.
Overall, I also find the nested approach more difficult to understand.
All of this should be solvable by going back to a single, flat hierarchy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added what I thought were the right test to begin with.
I ended up added test to validate per request status; tradeoffs are perf. I don't know if we lose anything here to be frank.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm definitely okay with the (unique set of) tests you have selected. (In fact, I think you included some which I would have likely forgotten.)
It's just that I think structurally, we're not reaping the benefits of sub-tests with the current approach; notably the points described in the Table-driven tests using subtests section of the introductory blog post.
middlewares/stripPrefix_test.go
Outdated
stripServers[stripPath] = httptest.NewServer(&StripPrefix{ | ||
Prefixes: []string{stripPath}, | ||
Handler: handlerPath, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initializing the servers outside of the parallelized context could be problematic. For instance, if I execute your test like this
go test -count 20 -cpu 1,2,4 -run ^TestStripPrefix$ ./middlewares
I immediately run into too many open files
problems on my Mac.
This might not be an issue now but could possibly become one in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Help me understand why you're seeing multiple files opened.
I'm not sure it has anything to do with this at all. Sounds unrelated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Linux/Unix (which Mac is, technically) represent almost everything in terms of files, including sockets. When I repeated the test 20 times and simulated multiples cores above, a higher number of sockets (and thus file handles) would be consumed concurrently by the test server.
It's perfectly okay to hit the limit at some point because my Mac machine obviously isn't optimized for highly concurrent request throughput. What's interesting though is that with the simpler approach creating one test server per more narrow-scoped test execution routine, I only see the limit when running on 32 or 64 cores.
This really isn't supposed to be a hard blocker on the given approach at all, just an indication on which implementation may be easier on the resource consumption IMHO.
@timoreimann your timoreimann@224f2a1 is a very clean way to test ❤️ |
024e74a
to
536de0b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. 👍
Thanks for the contribution. This one should make a bunch of folks happy.
536de0b
to
7c22d5b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
I have rebase the PR on the 1.3 branch.
7c22d5b
to
635cb8d
Compare
When pushing data to downstream proxies; malformed requests were being sent. The corrected behavior is as follows: | Route Stripped | URL | Passed to Backend | | ----------------- | ---------------------- | ------------------ | | / | / | / | | Route Stripped | URL | Passed to Backend | | ----------------- | ---------------------- | ------------------ | | /stat | /stat | / | | /stat | /stat/ | / | | /stat | /status | /status | | /stat | /stat/us | /us | | Route Stripped | URL | Passed to Backend | | ----------------- | ---------------------- | ------------------ | | /stat/ | /stat | /stat | | /stat/ | /stat/ | / | | /stat/ | /status | /status | | /stat/ | /stat/us | /us | Prior, we could strip the prefixing `/`, and we'd also ignore the case where you want to serve something like `/api` as both the index and as a subpath. Additionally, this should resolve a myriad of issues relating to kubernetes ingress `PathPrefixStrip`.
635cb8d
to
f814499
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks very much @seryl !
Awesome contribution, this will help a lot 👏
LGTM
When pushing data to downstream proxies; malformed requests were being
sent.
The corrected behavior is as follows:
Prior, we could strip the prefixing
/
, and we'd also ignore the casewhere you want to serve something like
/api
as both the index and as asubpath.
Additionally, this should resolve a myriad of issues relating to
kubernetes ingress
PathPrefixStrip
.Description
Should fix the following tickets:
/
causes400 bad request
#374Potentially fixes: