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

Unusual path parameter values make mux to choose incorrect handler #308

Closed
sergey-seleznev opened this issue Oct 25, 2017 · 16 comments
Closed
Assignees

Comments

@sergey-seleznev
Copy link

Hi!

I would like to define a number of handlers, that have:

  • same URL with a file path as the rightmost parameter,
  • different HTTP request methods.

Here's a simplified example:

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/files/{path:.*}", getHandler).Methods("GET")
	router.HandleFunc("/files/{path:.*}", postHandler).Methods("POST")
	log.Fatal(http.ListenAndServe(":8080", router))
}

func getHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("[GET] " + mux.Vars(r)["path"]))
}

func postHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("[POST] " + mux.Vars(r)["path"]))
}

It seems that mux tends to ignore methods and select improper handler in some specific cases:

  • GET http://localhost:8080/files/file.txt => [GET] file.txt (OK)
  • POST http://localhost:8080/files/file.txt => [POST] file.txt (OK)
  • GET http://localhost:8080/files/path/to/file.txt => [GET] path/to/file.txt (OK)
  • POST http://localhost:8080/files/path/to/file.txt => [POST] path/to/file.txt (OK)
  • GET http://localhost:8080/files/path/to//file.txt => [GET] path/to/file.txt (OK)
  • POST http://localhost:8080/files/path/to//file.txt => [GET] path/to/file.txt (unexpected)
  • GET http://localhost:8080/files/path/to/./file.txt => [GET] path/to/file.txt (OK)
  • POST http://localhost:8080/files/path/to/./file.txt => [GET] path/to/file.txt (unexpected)
  • GET http://localhost:8080/files/path/to/../file.txt => [GET] path/file.txt (OK)
  • POST http://localhost:8080/files/path/to/../file.txt => [GET] path/file.txt (unexpected)

Handler declaration order doesn't change the tests results mentioned.

@elithrar
Copy link
Contributor

This may be related to #300

@kisielk
Copy link
Contributor

kisielk commented Nov 28, 2017

Can you check if the latest version fixes the problem for you?

@sergey-seleznev
Copy link
Author

Seems not to fix that. I having cleared out the previous package traces from src and pkg, and updated to commit 65ec7248c53f499f6b480655e019e0b9d7a6ce11 of the package. Still incorrect methods are called in unexpected scenarios listed above. It's quite easy to verify: all the sample code I run is in the 1st message. Does it execute correctly on your side?

@elithrar
Copy link
Contributor

Normal paths from the tests in the OP:

for i in `seq 20`; do curl http://localhost:8080/files/path/to/file.txt; done
[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt[GET] path/to/file.txt% 
 for i in `seq 20`; do curl -XPOST http://localhost:8080/files/path/to/file.txt; done
[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt% 

Paths noted as "unexpected" in the OP:

for i in `seq 20`; do curl -XPOST "http://localhost:8080/files/path/to/./file.txt"; done
[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt% 
# re-directs at first due to the double slash.
for i in `seq 20`; do curl -L -XPOST "http://localhost:8080/files/path/to//file.txt"; done
[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt[POST] path/to/file.txt% 

I don't see the bug on the latest HEAD (65ec724).

@sergey-seleznev
Copy link
Author

Thank you for the details!
I've tried with CURL now as you did - it worked fine for me as well.
The incorrect result seems to be specific to Postman:
postman-vs-curl

@kisielk
Copy link
Contributor

kisielk commented Nov 30, 2017

Can you provide a log of the full request sent by Postman? I imagine it must print it somewhere

@sergey-seleznev
Copy link
Author

I have this one, but it is more like request-response details:
image

@elithrar
Copy link
Contributor

elithrar commented Nov 30, 2017 via email

@sergey-seleznev
Copy link
Author

sergey-seleznev commented Dec 1, 2017

@elithrar it outputs the same that Postman receives.

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/files/{path:.*}", getHandler).Methods("GET")
	router.HandleFunc("/files/{path:.*}", postHandler).Methods("POST")
	log.Fatal(http.ListenAndServe(":8080", router))
}

func getHandler(w http.ResponseWriter, r *http.Request) {
	log := "[GET] " + mux.Vars(r)["path"]
	fmt.Println(log)
	w.Write([]byte(log))
}

func postHandler(w http.ResponseWriter, r *http.Request) {
	log := "[POST] " + mux.Vars(r)["path"]
	fmt.Println(log)
	w.Write([]byte(log))
}

I would blame Postman, but it's actually getting expected response calling a sample ASP.NET Core WebAPI server containing just a couple lines of custom code:

[HttpGet("/files/{*path}")]
public String GetHandler(String path) => "[GET] " + path;

[HttpPost("/files/{*path}")]
public String PostHandler(String path) => "[POST] " + path;

So I guess it's some request specifics of Postman (that could come from other client as well) that gets mux to mix up the handlers.

@kisielk
Copy link
Contributor

kisielk commented Dec 1, 2017

Here's a reproducer in Go, using the server code from the post above:

package main

import (
	"io/ioutil"
	"log"
	"net/http"
)

func main() {
	urls := []string{
		"http://localhost:8080/files/path/to/file.txt",
		"http://localhost:8080/files/path/to/./file.txt",
	}
	for _, url := range urls {
		req, err := http.NewRequest("POST", url, nil)
		if err != nil {
			log.Fatal(err)
		}
		client := &http.Client{}
		resp, err := client.Do(req)
		if err != nil {
			log.Fatal(err)
		} else {
			out, _ := ioutil.ReadAll(resp.Body)
			log.Println(string(out))
		}
	}
}

The first one results in [POST] but the second one in [GET]

@kisielk
Copy link
Contributor

kisielk commented Dec 1, 2017

With some further testing and debugging I narrowed the problem down to here: https://github.com/gorilla/mux/blob/master/mux.go#L122

When the client receives the redirect, it retries the request with a GET instead of a POST. I guess this must be what's happening with postman too.

@kisielk
Copy link
Contributor

kisielk commented Dec 1, 2017

It behaves correctly when using http.StatusPermanentRedirect instead of http.StatusMovedPermanently. So maybe the code should switch to that?

@elithrar
Copy link
Contributor

elithrar commented Dec 1, 2017

@kisielk Correct, a 308 allows a retry w/ the same method, and a 301/302 must change to a GET on the subsequent (re-directed) request.

308 isn't as widely supported on all clients, so this is a little tricky. This isn't so much "broken" as implicit. I also loathe "more dials" to configure things.

Perhaps default to 308 (newer) and allow a hook to modify that if need be? RedirectHandler?

@kisielk
Copy link
Contributor

kisielk commented Dec 1, 2017

I'm inclined to leave it as it is. At least 301 is understood by all clients, and nothing bad will happen. If you have a client that's making POST requests to a URL that results in a redirect, and they turn into GETs then you could either:

a) Fix your request URL
b) Configure your client to try the redirected URL with POSTs instead of switching to GETs (it seems curl already does this).

If we switch it to 308, the only advantage is that you don't need to do a) or b), but the potential consequence is that some clients won't understand the status code at all, and it could break some existing uses.

@elithrar
Copy link
Contributor

elithrar commented Dec 2, 2017 via email

@elithrar elithrar added documentation and removed bug labels Dec 2, 2017
@elithrar elithrar self-assigned this Dec 2, 2017
elithrar added a commit that referenced this issue Dec 2, 2017
* StrictSlash enabled routes return a 301 to the client
* As per the HTTP standards, non-idempotent methods, such as POST or PUT, will be followed with a GET by the client
* Users should use middleware if they wish to change this behaviour to return a HTTP 308.
@elithrar
Copy link
Contributor

elithrar commented Dec 2, 2017

See PR #321

Closing this out, but let me know if there are outstanding concerns.

@elithrar elithrar closed this as completed Dec 2, 2017
kisielk pushed a commit that referenced this issue Dec 2, 2017
* [docs] Note StrictSlash re-direct behaviour #308

* StrictSlash enabled routes return a 301 to the client
* As per the HTTP standards, non-idempotent methods, such as POST or PUT, will be followed with a GET by the client
* Users should use middleware if they wish to change this behaviour to return a HTTP 308.

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

No branches or pull requests

3 participants