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

Add Path Replacement Rule #1374

Merged
merged 1 commit into from Apr 27, 2017
Merged

Add Path Replacement Rule #1374

merged 1 commit into from Apr 27, 2017

Conversation

ssttevee
Copy link
Contributor

@ssttevee ssttevee commented Apr 2, 2017

Replaces the old path with the new one and adds the old path to the X-Replaced-Path header.

My use case is to map to a FAAS web triggers (i.e. google cloud functions, aws lambda).

@ssttevee ssttevee force-pushed the path-replace-rule branch 2 times, most recently from e4f0e02 to 12e21cd Compare April 2, 2017 01:30
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 left a few comments.

We should definitely also add some tests for PathReplace.ServeHTTP and pathReplace.

docs/basics.md Outdated
@@ -82,6 +82,7 @@ Frontends can be defined using the following rules:
- `HostRegexp: traefik.io, {subdomain:[a-z]+}.traefik.io`: Adds a matcher for the URL hosts. It accepts templates with zero or more URL variables enclosed by `{}`. Variables can define an optional regexp pattern to be matched.
- `Method: GET, POST, PUT`: Method adds a matcher for HTTP methods. It accepts a sequence of one or more methods to be matched.
- `Path: /products/, /articles/{category}/{id:[0-9]+}`: Path adds a matcher for the URL paths. It accepts templates with zero or more URL variables enclosed by `{}`.
- `PathReplace: /serverless-path`: Replaces the path and adds the old path to the `X-Replaced-Path` header. Useful for mapping to AWS Lambda or Google Cloud Functions.
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 not sure if it was a conscious decision, but we seem to have a pattern that Path* match a path and optionally modify it (e.g., Path, PathPrefix, PathPrefixStrip), while *Path do modifications only (AddPrefix). Regardless of how deliberate this naming choice was, I like it. :-)

Your new rule seems to fall into the second category. If that was your intention too, then I'd suggest to call it ReplacePath for naming consistency reasons.


func (s *PathReplace) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.Header.Add("X-Replaced-Path", r.URL.Path)
r.URL.Path = s.Path
Copy link
Contributor

Choose a reason for hiding this comment

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

A few questions:

  1. Is it intentional that you replace the entire path?
  2. Shouldn't we check if the path we intend to replace actually exists in r.URL.Path and only replace / add the header if it does?
  3. What's the desired behavior if s.Path is shorter than r.URL.Path, i.e., we cannot replace it completely?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the idea is to replace the entire path so that all paths will be forwarded to a single path on the backend.

For example, if I'm using Google Cloud Functions, I want both http://example.com/ and http://example.com/some/long/url.html to be replaced with /some-function, then I can simulate having multiple paths on https://some-project.cloudfunctions.net/some-function.

Regarding your third question, I'm not sure why we would ever be unable to replace the path completely.

server.go Outdated
@@ -66,6 +66,7 @@ type serverRoute struct {
route *mux.Route
stripPrefixes []string
addPrefix string
pathReplace string
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be called replacePath if you follow my naming reasoning above.

rules.go Outdated
@@ -74,6 +74,13 @@ func (r *Rules) pathStrip(paths ...string) *mux.Route {
return r.route.route
}

func (r *Rules) pathReplace(paths ...string) *mux.Route {
for _, path := range paths {
r.route.addPrefix = path
Copy link
Contributor

Choose a reason for hiding this comment

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

You mean pathReplace / replacePath here.

@ssttevee
Copy link
Contributor Author

ssttevee commented Apr 5, 2017

I agree with your suggestion to rename it, however I think you're missing the point of my changes. My reply to your comment further explains the idea.

@timoreimann
Copy link
Contributor

Thanks, I got a clearer picture now. Makes total sense to me.

If you could do the rename and add a test or two, I'd be glad. :-)

@ssttevee
Copy link
Contributor Author

I have made the changes, but I feel like one might want to customize the header that it is set to too.

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.

Hey Thanks @ssttevee !
One small comment, but other than that, LGTM :)

}

// SetHandler sets handler
func (s *ReplacePath) SetHandler(Handler http.Handler) {
Copy link
Member

Choose a reason for hiding this comment

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

Is it needed (apart from tests) ?

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 was wondering the same thing for the AddPrefix handler. I thought it might be part of an interface so I copied it anyways.

I'll remove it since you don't think it's needed either.

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.

Some more comments.

I'd also prefer snail case instead of camel case in file names. I know we use the later elsewhere, but it's really not Go idiomatic.

}

for _, path := range paths {
req, err := http.NewRequest("ANY", "http://localhost"+path, nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use subtests and pass path to t.Run. Right now, if a test fails, there's no way to tell which one did.

Copy link
Contributor

Choose a reason for hiding this comment

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

Unless ANY is some kind of special place holder accepted by Go/HTTP, I'd rather like to use GET. This will be safe to work in the future too.

package middlewares_test

import (
"github.com/containous/traefik/middlewares"
Copy link
Contributor

Choose a reason for hiding this comment

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

Please move Github imports below stdlib ones, separated by a blank line.

rules.go Outdated
@@ -74,6 +74,13 @@ func (r *Rules) pathStrip(paths ...string) *mux.Route {
return r.route.route
}

func (r *Rules) replacePath(paths ...string) *mux.Route {
for _, path := range paths {
r.route.addPrefix = path
Copy link
Contributor

Choose a reason for hiding this comment

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

server.go Outdated
@@ -791,6 +792,14 @@ func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http
}
}

// path replace
if len(serverRoute.replacePath) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

This definitely needs a test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

there is no existing test file for server.go, should I make one to only test this part?

Copy link
Contributor

Choose a reason for hiding this comment

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

Please do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

actually, does this function even need to be a method of Server? it doesn't access any of it's fields.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah good catch, we could convert it from a method to a function right away. Would simplify testing.

@ldez
Copy link
Member

ldez commented Apr 24, 2017

@ssttevee do you the time to update this PR before the upcoming feature freeze for the 1.3 release?

@ssttevee
Copy link
Contributor Author

I couldn't think of a good way to test the wireFrontendBackend method, but everything else should be done.

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.

A few smallish comments left; I think we're almost done. :-)

t.Run(path, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost"+path, nil)
if err != nil {
t.Error(err)
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 be a t.Fatal.


handler.ServeHTTP(nil, req)
if newPath != replacementPath {
t.Errorf("new path should be \"%s\"", replacementPath)
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest to use single quotes (') to avoid escaping.

}

if oldPath != path {
t.Errorf("old path should be \"%s\"", path)
Copy link
Contributor

Choose a reason for hiding this comment

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

Single quotes preferred here too.

newPath = r.URL.Path
oldPath = r.Header.Get("X-Replaced-Path")
}),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Please move the block from line 13 to 20 into the test loop body in order to make sure the tests work okay even if we run them in parallel.

func (s *ReplacePath) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.Header.Add(ReplacedPathHeader, r.URL.Path)
r.URL.Path = s.Path
r.RequestURI = r.URL.RequestURI()
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 not too deep into this part of HTTP, but is it legitimate to change this?

This is what net/http says:

// RequestURI is the unmodified Request-URI of the
// Request-Line (RFC 2616, Section 5.1) as sent by the client
// to a server. Usually the URL field should be used instead.
// It is an error to set this field in an HTTP client request.
RequestURI string

WDYT?

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 did find it a bit strange too, but iirc, it does not directly affect a client request.

I'm probably wrong, but at one point there might have been some code that read r.RequestURI` to use for the path to use for the backend request. That would also explain why the same expression elsewhere in the code base.

Copy link
Contributor Author

@ssttevee ssttevee Apr 25, 2017

Choose a reason for hiding this comment

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

In the github.com/vulcand/oxy, when creating a new forwarder, it is used for r.Opaque which in http(s) requests has no effect.

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@ssttevee
Copy link
Contributor Author

After investigating, I've concluded that setting r.RequestURI is unnecessary (maybe even erroneous). My reasoning is in this comment.

It will probably be best to remove all instances of this from the rest of code base to avoid future contributors copying it.

@ldez ldez added this to the 1.3 milestone Apr 25, 2017
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.

@ssttevee thanks for bearing with me, and in particular for looking into the RequestURI matter.

PR LGTM. 🎉

@timoreimann
Copy link
Contributor

@ssttevee one last request: could you squash your commits for git hygiene reasons?

@ssttevee
Copy link
Contributor Author

it's done!

@aantono
Copy link
Contributor

aantono commented May 9, 2017

I believe I found a possible bug, or at least a not intuitive behavior when ReplacePath rule is preceded by PathPrefixStrip
Basically because PathPrefixStrip is a Matcher, but also happens to be a Modifier as well, when combined with ReplacePath, like PathPrefixStrip:/management;ReplacePath:/health, due to the order in which the Modifiers are applied in server.go, the above rule-set does not behave as expected.

While one would expect that we match the request to /management url, strip the /management prefix, followed by a replace with /health. This is NOT, however, what is happening. The matcher does the job correctly and matches the request on /management and sends it down the correct Handler chain, but that's when the issue creeps in.

Because ReplacePath Modifier gets executed first in the Handler chain, it replaces /management with /health, and when StripPrefix Modifier gets called, it is trying to match the Path with /management prefix, and obviously failing to do so, thus returning 404-Not Found.

If the Rule is written as PathPrefixStrip:/management;ReplacePath:/management/health, then everything works, but the actual invocation URL Path ends up being NOT /management/health but /health, thus making this configuration be misleading.

Should I open a new Issue to track this improvement?

@timoreimann
Copy link
Contributor

@aantono thanks for the report.

Please do file a new bug report and link it here. Appreciated!

@aantono
Copy link
Contributor

aantono commented May 9, 2017

Done... I'll try to see if I can make PR with a fix tomorrow.
My current thinking is to try to move the ReplacePath block in server.go to be the first in the chain, above the AddPrefix - https://github.com/containous/traefik/blob/master/server/server.go#L778
Thoughts?

@timoreimann
Copy link
Contributor

@aantono sounds reasonable on first sight.

We should try to add a few tests to make sure the known combinations work as expected.

@IceXPR
Copy link

IceXPR commented May 15, 2017

I still have the problem with the route replacements in the containous/traefik image.

@IceXPR
Copy link

IceXPR commented May 15, 2017

@aantono What version of Traefik did you use for the workaround you mentioned (PathPrefixStrip:/management;ReplacePath:/management/health)?

@aantono
Copy link
Contributor

aantono commented May 15, 2017

That was the 1.3-RC1 (the one I used was downloaded from the releases page)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/rules kind/enhancement a new or improved feature.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants