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

BUG: param with same prefix to static route not working as expected #61

Closed
daymade opened this issue Apr 26, 2017 · 13 comments
Closed

BUG: param with same prefix to static route not working as expected #61

daymade opened this issue Apr 26, 2017 · 13 comments

Comments

@daymade
Copy link

daymade commented Apr 26, 2017

code version:

last commit index: 23d011e

Current behavior:

when we pass a param to a params route, and the param has same prefix to a static route,
we got two wrong results:
not found and handler right, extracted params wrong

Expected behavior:

  1. route me to right handler
  2. extract params correct

Steps to reproduce:

  1. add one route /:id
  2. add another route /users
  3. add third route /usersnew
  4. in postman we visit an url /usersnew1
  5. handler not found

Related code:

paste this two tests to router_test.go

func TestIssue61_1(t *testing.T) {
	r := NewRouter()

	// Routes
	r.Add("GET", "/:id", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("param_" + Param(r, "id")))
	})
	r.Add("GET", "/users", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("static_users"))
	})
	r.Add("GET", "/usersnew", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("static_usersnew"))
	})

	// Route > /users
	req, _ := http.NewRequest("GET", "/users", nil)
	h := r.Find(req)
	w := httptest.NewRecorder()
	if assert.NotNil(t, h) {
		h(w, req)
		assert.Equal(t, "static_users", w.Body.String())
	}

	// Route > /usersnew
	req, _ = http.NewRequest("GET", "/usersnew", nil)
	h = r.Find(req)
	w = httptest.NewRecorder()
	if assert.NotNil(t, h) {
		h(w, req)
		assert.Equal(t, "static_usersnew", w.Body.String())
	}

	// Route > /:id
	req, _ = http.NewRequest("GET", "/123", nil)
	w = httptest.NewRecorder()
	h = r.Find(req)
	if assert.NotNil(t, h) {
		h(w, req)
		assert.Equal(t, "123", Param(req, "id"))
		assert.Equal(t, "param_123", w.Body.String())
	}

	// Route > /:id
	req, _ = http.NewRequest("GET", "/users123", nil)
	w = httptest.NewRecorder()
	h = r.Find(req)
	if assert.NotNil(t, h) {
		h(w, req)
		assert.Equal(t, "users123", Param(req, "id"))
		assert.Equal(t, "param_users123", w.Body.String())
	}

	// Route > /:id
	// BAD CASE 1 HERE
	req, _ = http.NewRequest("GET", "/usersnew1", nil)
	h = r.Find(req)
	w = httptest.NewRecorder()
	if assert.NotNil(t, h) {
		h(w, req)
		//expected: "usersnew1" received: ""
		assert.Equal(t, "usersnew1", Param(req, "id"))
		//expected: "param_usersnew1" received: "Not Found"
		assert.Equal(t, "param_usersnew1", w.Body.String())
	}
}

func TestIssue61_2(t *testing.T) {
	r := NewRouter()

	// Routes
	r.Add("GET", "/users", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("static_users"))
	})
	r.Add("GET", "/usersnew", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("static_usersnew"))
	})
	// NOTE HERE
	// we add param route after added two static routes
	r.Add("GET", "/:id", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("param_" + Param(r, "id")))
	})

	// Route > /users
	// Works well
	req, _ := http.NewRequest("GET", "/users", nil)
	h := r.Find(req)
	w := httptest.NewRecorder()
	if assert.NotNil(t, h) {
		h(w, req)
		assert.Equal(t, "static_users", w.Body.String())
	}

	// Route > /:id
	// Works well
	req, _ = http.NewRequest("GET", "/123", nil)
	h = r.Find(req)
	w = httptest.NewRecorder()
	if assert.NotNil(t, h) {
		h(w, req)
		assert.Equal(t, "123", Param(req, "id"))
		assert.Equal(t, "param_123", w.Body.String())
	}
	// Works well
	req, _ = http.NewRequest("GET", "/users123", nil)
	h = r.Find(req)
	w = httptest.NewRecorder()
	if assert.NotNil(t, h) {
		h(w, req)
		assert.Equal(t, "users123", Param(req, "id"))
		assert.Equal(t, "param_users123", w.Body.String())
	}

	// Route > /:id
	// BAD CASE HERE
	// NOTE this case and the above one(issue61_1) seem to be identical,
	// But there are one difference, we add param route after added two static routes
	// Which gives us different result: the matched handler is right, but params extracted is wrong
	req, _ = http.NewRequest("GET", "/usersnew1", nil)
	h = r.Find(req)
	w = httptest.NewRecorder()
	if assert.NotNil(t, h) {
		h(w, req)
		//expected: "usersnew1" received: "new1"
		assert.Equal(t, "usersnew1", Param(req, "id"))
		//expected: "param_usersnew1" received: "param_new1"
		assert.Equal(t, "param_usersnew1", w.Body.String())
	}
}
@baylee
Copy link

baylee commented Apr 27, 2017

Encountered the same issue, got Not Found if a parameter in a parameterized route had even the same initial character as a non-parameterized route. Having different methods did not change the result.

Example:

router.Post("/foo/apple", foo.AppleHandler)
router.Get("/foo/:ID", foo.IDHandler)

When I GET /foo/abcde, I get "Not Found". Expectation is that I would be routed to foo.IDHandler.

@husobee
Copy link
Owner

husobee commented May 1, 2017

Will look into this today. Thanks for the report.

@husobee
Copy link
Owner

husobee commented May 2, 2017

@baylee: I am not seeing this problem based on your example, here is how i tried to reproduce:

package main

import (
        "log"
        "net/http"

        "github.com/husobee/vestigo"
)

func main() {
        // setup router and example route
        routes := vestigo.NewRouter()

        routes.Post("/foo/apple", PostExampleHandler)
        routes.Get("/foo/:ID", GetExampleHandler)

        log.Fatal(http.ListenAndServe(":1234", routes))
}

// GetExampleHandler - the "latest" handler function for /example
func GetExampleHandler(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        w.Write([]byte("example latest get foo id"))
}

// PostExampleHandler - the "latest" handler function for /example
func PostExampleHandler(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        w.Write([]byte("example latest post foo apple"))
}

With these results:

$ curl -XPOST http://localhost:1234/foo/apple -D -; echo
HTTP/1.1 200 OK
Date: Tue, 02 May 2017 15:17:07 GMT
Content-Length: 29
Content-Type: text/plain; charset=utf-8

example latest post foo apple


$ curl http://localhost:1234/foo/abcde -D -; echo                           
HTTP/1.1 200 OK
Date: Tue, 02 May 2017 15:15:20 GMT
Content-Length: 25
Content-Type: text/plain; charset=utf-8

example latest get foo id

It seems the problem that @tsongknows is finding is due to the path going deeper in the radix tree without a / seperator. Below is a visualization

/     
      -> :id
      -> users
               -> new

That being the case, currently the logic is, if there is not an exact match, the router will "back up" to the parent node's children, and see if there is parameter-ized or wildcard match on that level.

Since in our failure example, we are trying to use /usersnew1 the router goes down users path and then new path, and can't go any further, therefore backs up to the users path and looks for a parameter-ized or wildcard match, and can't find one. I feel the correct logic should be, go back as far as possible up until the preceding /looking for parameter-ized or wildcard matching.

I feel like I can get some cycles tonight to figure out a solution for this issue. Will keep you posted.

@husobee
Copy link
Owner

husobee commented May 3, 2017

@tsongknows @baylee: potential fix in branch issue-61. I have included the unit tests that are in this issue, and they now pass. Also included unit tests for testing wildcards across trie edge bounds as well. Tests for Issue61_2 were finding an unrelated bug also corrected in the PR, which was on insertion, on node split the child nodes were not updated with the reference to the new parent node. Thank you so much for finding this issue. If you don't mind testing this fix for me, I would appreciate it!

@daymade
Copy link
Author

daymade commented May 3, 2017

hi @husobee , thank you for your quick response!
I just test the new code in #63, all of the tests are passed, but I was wondering why do you commented out TestRouterMultiRoute on line 374 in router_test.go, if it is useless or a temporary hot fix?
see d97bab7#commitcomment-21992935

@husobee
Copy link
Owner

husobee commented May 3, 2017 via email

@daymade
Copy link
Author

daymade commented May 3, 2017

hi @husobee, nice job you have done!

luckless i have some tests still been failed 😅, about prefix returning from r.find

...
func (r *Router) add(method, path string, h http.HandlerFunc, cors *CorsAccessControl) {
...
func (r *Router) find(req *http.Request) (prefix string, h http.HandlerFunc) {

Could you tell me if the prefix returned and the route path we added should be identical?

If they need to be equal , please see following unit test:

// we can put this test in route_test.go 
func TestRouter_Match_StaticAndParamWithSamePrefix(t *testing.T) {
	type args struct {
		req *http.Request
	}

	wantParamTemplate := "/order/:id/address/:no"
	staticTemplate := "/order/new/address"

	r := NewRouter()

	r.Add("POST", wantParamTemplate, func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(wantParamTemplate))
	})
	r.Add("POST", staticTemplate, func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(staticTemplate))
	})

	// OK
	normalRequest, _ := http.NewRequest("POST", "/order/someid/address/123", nil)

	prefix1, h1 := r.find(normalRequest)

	w1 := httptest.NewRecorder()
	if assert.NotNil(t, h1) {
		h1(w1, normalRequest)

		assert.Equal(t, wantParamTemplate, w1.Body.String())

		assert.Equal(t, wantParamTemplate, prefix1)
	}

	// BAD CASE?
	// NOTE: label of segment ns is 'n'
	StaticAndParamWithSameLabelRequest, _ := http.NewRequest("POST", "/order/ns/address/123", nil)

	prefix2, h2 := r.find(StaticAndParamWithSameLabelRequest)

	w2 := httptest.NewRecorder()
	if assert.NotNil(t, h2) {
		h2(w2, normalRequest)

		// Matched right handler
		assert.Equal(t, wantParamTemplate, w2.Body.String())

		// BAD CASE?
		// expected: "/order/:id/address/:no" received: "/order/new/address:/address/:id"
		assert.Equal(t, wantParamTemplate, prefix2)
	}
}

// perhaps node.findChild need to be reviewed

@husobee
Copy link
Owner

husobee commented May 3, 2017

Will look into this tonight. It is pretty strange it is finding the correct handler, and not returning the correct prefix in this instance. Thanks!

@husobee
Copy link
Owner

husobee commented May 4, 2017

@tsongknows I updated the PR with another fix to solve that issue you were seeing, and also removed some dead code paths. Please let me know how that works for you. All tests are passing now.

@daymade
Copy link
Author

daymade commented May 4, 2017

@husobee it works.
before you merge it there are some variable names seems a little uncomfortable

func (n *node) findChild(l string, t ntype) *node

l here is not meaning for label, but for search
would you give some refactoring to it? that will increase the readability, in a sense

@husobee
Copy link
Owner

husobee commented May 4, 2017

Great suggestion. Updated, and just pushed.

@daymade
Copy link
Author

daymade commented May 4, 2017

looks great! I would like to close this issue, thanks for all things you have done @husobee

@husobee
Copy link
Owner

husobee commented May 4, 2017

Cool. Merged. Thanks for the bug report.

@husobee husobee closed this as completed May 4, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants