Skip to content
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

Merged
merged 1 commit into from
May 24, 2017

Conversation

seryl
Copy link
Contributor

@seryl seryl commented May 20, 2017

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.

Description

Should fix the following tickets:

Potentially fixes:

@seryl seryl force-pushed the fix-stripprefixpatch-behavior branch from 295c36e to ba24dc2 Compare May 20, 2017 04:52
@seryl
Copy link
Contributor Author

seryl commented May 20, 2017

Example of malformed requests:

10.244.0.164 - - [19/May/2017:23:58:14 +0000] "GET app.301d8f1c250fa1437ad7.js HTTP/1.1" 400 173 "-" "-" "-"
10.244.0.164 - - [19/May/2017:23:58:15 +0000] "GET app.301d8f1c250fa1437ad7.js HTTP/1.1" 400 173 "-" "-" "-"
10.244.0.164 - - [19/May/2017:23:58:17 +0000] "GET app.301d8f1c250fa1437ad7.js HTTP/1.1" 400 173 "-" "-" "-"
10.244.0.164 - - [19/May/2017:23:58:58 +0000] "GET _/public/img/pack.png HTTP/1.1" 400 173 "-" "-" "-"
10.244.0.164 - - [19/May/2017:23:59:07 +0000] "GET favicon.ico HTTP/1.1" 400 173 "-" "-" "-"
10.244.0.164 - - [20/May/2017:00:00:35 +0000] "GET app.301d8f1c250fa1437ad7.js HTTP/1.1" 400 173 "-" "-" "-"
10.244.0.164 - - [20/May/2017:00:00:35 +0000] "GET app.301d8f1c250fa1437ad7.js HTTP/1.1" 400 173 "-" "-" "-"
10.244.0.164 - - [20/May/2017:00:00:38 +0000] "GET favicon.ico HTTP/1.1" 400 173 "-" "-" "-"
10.244.0.164 - - [20/May/2017:00:00:41 +0000] "GET favicon.ico HTTP/1.1" 400 173 "-" "-" "-"
10.244.0.164 - - [20/May/2017:00:00:44 +0000] "GET favicon.ico HTTP/1.1" 400 173 "-" "-" "-

Note: They are all missing prefixed paths.

Copy link
Contributor

@timoreimann timoreimann left a 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.

r.Header[forwardedPrefixHeader] = []string{prefix}
r.RequestURI = r.URL.RequestURI()
s.Handler.ServeHTTP(w, r)
orgPrefix := strings.TrimSpace(prefix)
Copy link
Contributor

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?

r.Header[forwardedPrefixHeader] = []string{prefix}
r.RequestURI = r.URL.RequestURI()
s.Handler.ServeHTTP(w, r)
orgPrefix := strings.TrimSpace(prefix)
Copy link
Contributor

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.

prefix = orgPrefix
if !strings.HasSuffix(prefix, "/") {
prefix = orgPrefix + "/"
}
Copy link
Contributor

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, "/") + "/"

if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) {
if !strings.HasPrefix(p, "/") {
r.URL.Path = "/" + p
}
Copy link
Contributor

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, "/")

)

func TestStripPrefix(t *testing.T) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blank line here.

for _, test := range tests {
resp, err := http.Get(server.URL + test.url)
if err != nil {
t.Fatal(err)
Copy link
Contributor

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)

}
response, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
Copy link
Contributor

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.

}

if test.expectedResponse != string(response) {
t.Errorf("Expected '%s' : '%s'\n", test.expectedResponse, response)
Copy link
Contributor

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.

}

if test.expectedResponse != string(response) {
t.Errorf("Expected '%s' : '%s'\n", test.expectedResponse, response)
Copy link
Contributor

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)

Copy link
Contributor Author

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.


{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"},
Copy link
Contributor

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 /statand 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.

@timoreimann
Copy link
Contributor

@emilevauge @ldez what do you think, should we have this fix be part of 1.3?

@seryl
Copy link
Contributor Author

seryl commented May 21, 2017

@timoreimann Updated with your requested changes.

Let me know if there's anything else we need (rebase, etc).

@seryl seryl force-pushed the fix-stripprefixpatch-behavior branch from e956c3f to fcc4066 Compare May 21, 2017 05:46
Copy link
Contributor

@ldez ldez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job 👍

}

suites := []struct {
server *httptest.Server
Copy link
Contributor

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
}


for _, test := range suite.tests {
resp, err := http.Get(suite.server.URL + test.url)
if err != nil {
Copy link
Contributor

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")
}

Copy link
Contributor Author

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.

}{
{
url: "/norules",
expected: "",
Copy link
Contributor

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: ""

{
url: "/",
expected: "/",
code: http.StatusOK,
Copy link
Contributor

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

})
}

type stripPrefixTestCase struct {
Copy link
Contributor

@ldez ldez May 21, 2017

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.

@seryl
Copy link
Contributor Author

seryl commented May 21, 2017

Will rebase as soon as we get a thumbs up.

@seryl seryl force-pushed the fix-stripprefixpatch-behavior branch from 0afb0cb to deb8f5c Compare May 21, 2017 20:40
Copy link
Contributor

@timoreimann timoreimann left a 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!

}

type stripPrefixTestCase struct {
url string
Copy link
Contributor

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.

t.Parallel()
defer suite.server.Close()

for _, test := range suite.tests {
Copy link
Contributor

@timoreimann timoreimann May 22, 2017

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

stripServers[stripPath] = httptest.NewServer(&StripPrefix{
Prefixes: []string{stripPath},
Handler: handlerPath,
})
Copy link
Contributor

@timoreimann timoreimann May 22, 2017

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.

Copy link
Contributor Author

@seryl seryl May 22, 2017

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.

Copy link
Contributor

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.

@ldez
Copy link
Contributor

ldez commented May 22, 2017

@timoreimann your timoreimann@224f2a1 is a very clean way to test ❤️

Copy link
Contributor

@timoreimann timoreimann left a 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.

@ldez ldez added this to the 1.3 milestone May 23, 2017
@ldez ldez force-pushed the fix-stripprefixpatch-behavior branch from 536de0b to 7c22d5b Compare May 23, 2017 19:43
@ldez ldez changed the base branch from master to v1.3 May 23, 2017 19:43
Copy link
Contributor

@ldez ldez left a 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.

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`.
@ldez ldez force-pushed the fix-stripprefixpatch-behavior branch from 635cb8d to f814499 Compare May 23, 2017 22:56
Copy link
Member

@emilevauge emilevauge left a 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants