Giter VIP home page Giter VIP logo

httptreemux's Introduction

httptreemux Build Status GoDoc

High-speed, flexible, tree-based HTTP router for Go.

This is inspired by Julien Schmidt's httprouter, in that it uses a patricia tree, but the implementation is rather different. Specifically, the routing rules are relaxed so that a single path segment may be a wildcard in one route and a static token in another. This gives a nice combination of high performance with a lot of convenience in designing the routing patterns. In benchmarks, httptreemux is close to, but slightly slower than, httprouter.

Release notes may be found using the Github releases tab. Version numbers are compatible with the Semantic Versioning 2.0.0 convention, and a new release is made after every change to the code.

Installing with Go Modules

When using Go Modules, import this repository with import "github.com/dimfeld/httptreemux/v5" to ensure that you get the right version.

Why?

There are a lot of good routers out there. But looking at the ones that were really lightweight, I couldn't quite get something that fit with the route patterns I wanted. The code itself is simple enough, so I spent an evening writing this.

Handler

The handler is a simple function with the prototype func(w http.ResponseWriter, r *http.Request, params map[string]string). The params argument contains the parameters parsed from wildcards and catch-alls in the URL, as described below. This type is aliased as httptreemux.HandlerFunc.

Using http.HandlerFunc

Due to the inclusion of the context package as of Go 1.7, httptreemux now supports handlers of type http.HandlerFunc. There are two ways to enable this support.

Adapting an Existing Router

The UsingContext method will wrap the router or group in a new group at the same path, but adapted for use with context and http.HandlerFunc.

router := httptreemux.New()

group := router.NewGroup("/api")
group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request, params map[string]string) {
    id := params["id"]
    fmt.Fprintf(w, "GET /api/v1/%s", id)
})

// UsingContext returns a version of the router or group with context support.
ctxGroup := group.UsingContext() // sibling to 'group' node in tree
ctxGroup.GET("/v2/:id", func(w http.ResponseWriter, r *http.Request) {
    ctxData := httptreemux.ContextData(r.Context())
    params := ctxData.Params()
    id := params["id"]

    // Useful for middleware to see which route was hit without dealing with wildcards
    routePath := ctxData.Route()

    // Prints GET /api/v2/:id id=...
    fmt.Fprintf(w, "GET %s id=%s", routePath, id)
})

http.ListenAndServe(":8080", router)

New Router with Context Support

The NewContextMux function returns a router preconfigured for use with context and http.HandlerFunc.

router := httptreemux.NewContextMux()

router.GET("/:page", func(w http.ResponseWriter, r *http.Request) {
    params := httptreemux.ContextParams(r.Context())
    fmt.Fprintf(w, "GET /%s", params["page"])
})

group := router.NewGroup("/api")
group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request) {
    ctxData := httptreemux.ContextData(r.Context())
    params := ctxData.Params()
    id := params["id"]

    // Useful for middleware to see which route was hit without dealing with wildcards
    routePath := ctxData.Route()

    // Prints GET /api/v1/:id id=...
    fmt.Fprintf(w, "GET %s id=%s", routePath, id)
})

http.ListenAndServe(":8080", router)

Routing Rules

The syntax here is also modeled after httprouter. Each variable in a path may match on one segment only, except for an optional catch-all variable at the end of the URL.

Some examples of valid URL patterns are:

  • /post/all
  • /post/:postid
  • /post/:postid/page/:page
  • /post/:postid/:page
  • /images/*path
  • /favicon.ico
  • /:year/:month/
  • /:year/:month/:post
  • /:page

Note that all of the above URL patterns may exist concurrently in the router.

Path elements starting with : indicate a wildcard in the path. A wildcard will only match on a single path segment. That is, the pattern /post/:postid will match on /post/1 or /post/1/, but not /post/1/2.

A path element starting with * is a catch-all, whose value will be a string containing all text in the URL matched by the wildcards. For example, with a pattern of /images/*path and a requested URL images/abc/def, path would contain abc/def. A catch-all path will not match an empty string, so in this example a separate route would need to be installed if you also want to match /images/.

Using : and * in routing patterns

The characters : and * can be used at the beginning of a path segment by escaping them with a backslash. A double backslash at the beginning of a segment is interpreted as a single backslash. These escapes are only checked at the very beginning of a path segment; they are not necessary or processed elsewhere in a token.

router.GET("/foo/\\*starToken", handler) // matches /foo/*starToken
router.GET("/foo/star*inTheMiddle", handler) // matches /foo/star*inTheMiddle
router.GET("/foo/starBackslash\\*", handler) // matches /foo/starBackslash\*
router.GET("/foo/\\\\*backslashWithStar") // matches /foo/\*backslashWithStar

Routing Groups

Lets you create a new group of routes with a given path prefix. Makes it easier to create clusters of paths like:

  • /api/v1/foo
  • /api/v1/bar

To use this you do:

router = httptreemux.New()
api := router.NewGroup("/api/v1")
api.GET("/foo", fooHandler) // becomes /api/v1/foo
api.GET("/bar", barHandler) // becomes /api/v1/bar

Routing Priority

The priority rules in the router are simple.

  1. Static path segments take the highest priority. If a segment and its subtree are able to match the URL, that match is returned.
  2. Wildcards take second priority. For a particular wildcard to match, that wildcard and its subtree must match the URL.
  3. Finally, a catch-all rule will match when the earlier path segments have matched, and none of the static or wildcard conditions have matched. Catch-all rules must be at the end of a pattern.

So with the following patterns adapted from simpleblog, we'll see certain matches:

router = httptreemux.New()
router.GET("/:page", pageHandler)
router.GET("/:year/:month/:post", postHandler)
router.GET("/:year/:month", archiveHandler)
router.GET("/images/*path", staticHandler)
router.GET("/favicon.ico", staticHandler)

Example scenarios

  • /abc will match /:page
  • /2014/05 will match /:year/:month
  • /2014/05/really-great-blog-post will match /:year/:month/:post
  • /images/CoolImage.gif will match /images/*path
  • /images/2014/05/MayImage.jpg will also match /images/*path, with all the text after /images stored in the variable path.
  • /favicon.ico will match /favicon.ico

Special Method Behavior

If TreeMux.HeadCanUseGet is set to true, the router will call the GET handler for a pattern when a HEAD request is processed, if no HEAD handler has been added for that pattern. This behavior is enabled by default.

Go's http.ServeContent and related functions already handle the HEAD method correctly by sending only the header, so in most cases your handlers will not need any special cases for it.

By default TreeMux.OptionsHandler is a null handler that doesn't affect your routing. If you set the handler, it will be called on OPTIONS requests to a path already registered by another method. If you set a path specific handler by using router.OPTIONS, it will override the global Options Handler for that path.

Trailing Slashes

The router has special handling for paths with trailing slashes. If a pattern is added to the router with a trailing slash, any matches on that pattern without a trailing slash will be redirected to the version with the slash. If a pattern does not have a trailing slash, matches on that pattern with a trailing slash will be redirected to the version without.

The trailing slash flag is only stored once for a pattern. That is, if a pattern is added for a method with a trailing slash, all other methods for that pattern will also be considered to have a trailing slash, regardless of whether or not it is specified for those methods too. However this behavior can be turned off by setting TreeMux.RedirectTrailingSlash to false. By default it is set to true.

One exception to this rule is catch-all patterns. By default, trailing slash redirection is disabled on catch-all patterns, since the structure of the entire URL and the desired patterns can not be predicted. If trailing slash removal is desired on catch-all patterns, set TreeMux.RemoveCatchAllTrailingSlash to true.

router = httptreemux.New()
router.GET("/about", pageHandler)
router.GET("/posts/", postIndexHandler)
router.POST("/posts", postFormHandler)

GET /about will match normally.
GET /about/ will redirect to /about.
GET /posts will redirect to /posts/.
GET /posts/ will match normally.
POST /posts will redirect to /posts/, because the GET method used a trailing slash.

Custom Redirects

RedirectBehavior sets the behavior when the router redirects the request to the canonical version of the requested URL using RedirectTrailingSlash or RedirectClean. The default behavior is to return a 301 status, redirecting the browser to the version of the URL that matches the given pattern.

These are the values accepted for RedirectBehavior. You may also add these values to the RedirectMethodBehavior map to define custom per-method redirect behavior.

  • Redirect301 - HTTP 301 Moved Permanently; this is the default.
  • Redirect307 - HTTP/1.1 Temporary Redirect
  • Redirect308 - RFC7538 Permanent Redirect
  • UseHandler - Don't redirect to the canonical path. Just call the handler instead.

Case Insensitive Routing

You can optionally allow case-insensitive routing by setting the CaseInsensitive property on the router to true. This allows you to make all routes case-insensitive. For example:

router := httptreemux.New()
router.CaseInsensitive
router.GET("/My-RoUtE", pageHandler)

In this example, performing a GET request to /my-route will match the route and execute the pageHandler functionality. It's important to note that when using case-insensitive routing, the CaseInsensitive property must be set before routes are defined or there may be unexpected side effects.

Rationale/Usage

On a POST request, most browsers that receive a 301 will submit a GET request to the redirected URL, meaning that any data will likely be lost. If you want to handle and avoid this behavior, you may use Redirect307, which causes most browsers to resubmit the request using the original method and request body.

Since 307 is supposed to be a temporary redirect, the new 308 status code has been proposed, which is treated the same, except it indicates correctly that the redirection is permanent. The big caveat here is that the RFC is relatively recent, and older or non-compliant browsers will not handle it. Therefore its use is not recommended unless you really know what you're doing.

Finally, the UseHandler value will simply call the handler function for the pattern, without redirecting to the canonical version of the URL.

RequestURI vs. URL.Path

Escaped Slashes

Go automatically processes escaped characters in a URL, converting + to a space and %XX to the corresponding character. This can present issues when the URL contains a %2f, which is unescaped to '/'. This isn't an issue for most applications, but it will prevent the router from correctly matching paths and wildcards.

For example, the pattern /post/:post would not match on /post/abc%2fdef, which is unescaped to /post/abc/def. The desired behavior is that it matches, and the post wildcard is set to abc/def.

Therefore, this router defaults to using the raw URL, stored in the Request.RequestURI variable. Matching wildcards and catch-alls are then unescaped, to give the desired behavior.

TL;DR: If a requested URL contains a %2f, this router will still do the right thing. Some Go HTTP routers may not due to Go issue 3659.

Escaped Characters

As mentioned above, characters in the URL are not unescaped when using RequestURI to determine the matched route. If this is a problem for you and you are unable to switch to URL.Path for the above reasons, you may set router.EscapeAddedRoutes to true. This option will run each added route through the URL.EscapedPath function, and add an additional route if the escaped version differs.

http Package Utility Functions

Although using RequestURI avoids the issue described above, certain utility functions such as http.StripPrefix modify URL.Path, and expect that the underlying router is using that field to make its decision. If you are using some of these functions, set the router's PathSource member to URLPath. This will give up the proper handling of escaped slashes described above, while allowing the router to work properly with these utility functions.

Concurrency

The router contains an RWMutex that arbitrates access to the tree. This allows routes to be safely added from multiple goroutines at once.

No concurrency controls are needed when only reading from the tree, so the default behavior is to not use the RWMutex when serving a request. This avoids a theoretical slowdown under high-usage scenarios from competing atomic integer operations inside the RWMutex. If your application adds routes to the router after it has begun serving requests, you should avoid potential race conditions by setting router.SafeAddRoutesWhileRunning to true to use the RWMutex when serving requests.

Error Handlers

NotFoundHandler

TreeMux.NotFoundHandler can be set to provide custom 404-error handling. The default implementation is Go's http.NotFound function.

MethodNotAllowedHandler

If a pattern matches, but the pattern does not have an associated handler for the requested method, the router calls the MethodNotAllowedHandler. The default version of this handler just writes the status code http.StatusMethodNotAllowed and sets the response header's Allowed field appropriately.

Panic Handling

TreeMux.PanicHandler can be set to provide custom panic handling. The SimplePanicHandler just writes the status code http.StatusInternalServerError. The function ShowErrorsPanicHandler, adapted from gocraft/web, will print panic errors to the browser in an easily-readable format.

Unexpected Differences from Other Routers

This router is intentionally light on features in the name of simplicity and performance. When coming from another router that does heavier processing behind the scenes, you may encounter some unexpected behavior. This list is by no means exhaustive, but covers some nonobvious cases that users have encountered.

gorilla/pat query string modifications

When matching on parameters in a route, the gorilla/pat router will modify Request.URL.RawQuery to make it appear like the parameters were in the query string. httptreemux does not do this. See Issue #26 for more details and a code snippet that can perform this transformation for you, should you want it.

httprouter and catch-all parameters

When using httprouter, a route with a catch-all parameter (e.g. /images/*path) will match on URLs like /images/ where the catch-all parameter is empty. This router does not match on empty catch-all parameters, but the behavior can be duplicated by adding a route without the catch-all (e.g. /images/).

Middleware

This package provides no middleware. But there are a lot of great options out there and it's pretty easy to write your own. The router provides the Use and UseHandler functions to ease the creation of middleware chains. (Real documentation of these functions coming soon.)

Acknowledgements

httptreemux's People

Contributors

arolek avatar dbudworth avatar derekperkins avatar devillecodes avatar dimfeld avatar dmitris avatar idawes avatar lukahartwig avatar minjatj avatar nadeemsyed avatar patrickrand avatar robsonpeixoto avatar samwhited avatar stuartclan avatar vmihailenco avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

httptreemux's Issues

Serving static file directory

Hi,

I am having issues attempting to serve a directory of static files in conjunction with other handlers.

I have a series of handlers such as:

    ws.Handle("GET", "/apidocs/", SwaggerHandler)
    ws.Handle("GET", "/:namespace", GetAllUniqueHandler)
    ws.Handle("GET", "/:namespace/:name", GetAllVersionsHandler)
    ws.Handle("GET", "/:namespace/:name/:version", GetVersionHandler)
    ....

where the SwaggerHandler is defined as follows:

func SwaggerHandler(ctx *Context) (err error) {
    http.StripPrefix("/apidocs/",
        http.FileServer(
            http.Dir("/<absolute>/<path>/<to>/<my-files>/")))
    return
}

I've read through your code-base and docs, and tried setting ws.TreeMux.PathSource = httptreemux.URLPath as recommended, but I still have no luck. All of my non-static file handlers work perfectly, but my SwaggerHandler just returns an empty page.

Do you have any insight on what I may be doing wrong, or how I may resolve this issue?

Thank you.

How to serve static files

In defaultMux, I can serve static files like this.

http.Handle("/files", http.FileServer(Http.Dir(uploadPath)))

In group.go, I haven't find something similar.

TreeMux.Handler(path string, root http.FileSystem)  // not work

Host Header Match

Hello,

Thank you for this amazing library. I would like to ask if it would be possible to have something like this:

The router would have a route like this:

Route1:
   uri: /hello
   method: GET
   host: foo.bar

This request wouldn't match

$ curl -X GET -H example.com http://localhost/hello

/hello
Host: example.com

404 Not Found

This request would match

$ curl -X GET -H foo.bar http://localhost/hello

/hello
Host: foo.bar

200 OK

I can try to help implement it if it doesn't exist yet, but I would probably need some help on where to touch to do that.

Thank you again!

Example, or documentation, for integration of middleware(s)

I'm considering to move to httptreemux, from go-gin. However, I'm missing middleware support. The basic use case is handling of authorization (extract information from request, validate, and set context parameters).

The README says, that it's easy:

This package provides no middleware. But there are a lot of great options out there and it's pretty easy to write your own.

However, I have no clue, how to add any middleware to httptreemux. Can you please add any example, how to do that for some group of routes?

Case Insensitive Routing

We would like the ability to make case insensitive route calls. In other words, if we have set up a route to /my/route we would like a call to /MY/ROUTE to hit that route. I could not find any documentation explaining how to do this in the current project. Is this already possible? If so, how? If not, would it be possible to add it as a feature? I'm happy to help develop it, but not sure exactly how it ought to be implemented.

Discrepancy in package interface

Hello, I'm trying to leverage the ability to use the request context to retrieve the path parameters and I'm finding that the way to do it doesn't exactly work the way I would want it to. Sorry it's a bit of a long winded explanation but I can't think of a better way to go about it...

  • One may create a tree mux with New(). That object is a Group so exposes methods to register httptreemux handlers. It's also a http.Handler.
  • One may create a group using NewGroup. A group makes it possible to register httptreemux handlers.
  • One may create a context group with UsingContext which makes it possible to register http handlers.

One cannot however create a tree mux which would both allow registering http handlers and expose a ServeHTTP method. At the end of the day that's the interface I wish to have:

	Mux interface {
		http.Handler
		Handle(method, pattern string, handler http.HandlerFunc)
	}

This is what I need to do in order to achieve this:

type mux struct {
	r *httptreemux.TreeMux
	g *httptreemux.ContextGroup
}

func NewMux() Mux {
	r := httptreemux.New()
	r.EscapeAddedRoutes = true
	return &mux{r: r, g: r.UsingContext()}
}

func (m *mux) Handle(method, pattern string, handler http.HandlerFunc) {
	m.g.Handle(method, pattern, handler)
}

func (m *mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	m.r.ServeHTTP(w, r)
}

Really I wish I could just do httptreemux.NewWithContext() or something like that which would return an object that exposes both Handle (accepting http.HandlerFunc) and ServeHTTP.

Hope that makes sense....

URL is escaped twice on trailing slash redirect

The following test uncovers an issue with the current implementation of httptreemux:

func TestRedirectEscapedPath(t *testing.T) {
	router := NewContextMux()

	testHandler := func(w http.ResponseWriter, r *http.Request) {}

	router.GET("/:escaped/", testHandler)

	w := httptest.NewRecorder()
	u, err := url.Parse("/Test P@th")
	if err != nil {
		t.Error(err)
		return
	}
	r := httptest.NewRequest(http.MethodGet, u.String(), nil)

	router.ServeHTTP(w, r)

	path, err := w.Result().Location()
	if err != nil {
		t.Error(err)
		return
	}
	expected := "/Test%20P@th/"
	if path.String() != expected {
		t.Errorf("Given path wasn't escaped correctly.\n" +
			"Expected: %q\nBut got: %q", expected, path.String())
	}
}

This is caused since in lookup, path is used for redirects but it still contains escaped characters. Then in redirect newUrl.String() is called which results in double encoded characters.

URL escaped paths don't match

Consider the following route:

router.GET("/foo/\\*starToken", handler) // matches /foo/*starToken

And the following client code:

u := URL{Host: "localhost:8080", Scheme: "http", Path: "/foo/*starToken"}
req, _ := http.NewRequest("GET", u.String(), nil)
resp, _ := http.DefaultClient.Do(req)
if resp.StatusCode == 404 {
    fmt.Println("oh noes")
}

Then "oh noes" gets printed. That's because u.String() returns:

"http://localhost:8080/foo/%2AstarToken"

It seems httptreemux is not handling the escape properly. Now it could also be argued that the stdlib is being a bit too generous in its escaping, http://www.rfc-editor.org/rfc/rfc1738.txt states that "*" is OK in URLs but it is what it is...

Usage with negroni and route groups

Hey, thanks for the awesome router!

I struggling with creating two different groups of routes with group-specific middleware. Currently I'm using negroni as a middleware helper and what I want to achieve is for example to have:

/users
/events
...

and the rest of my REST api to be protected by an API-Key HTTP Header middleware and:

/web/doThis
/web/doThat

to be protected by a second API-Key HTTP Header middleware.

Currently I came up with the following code (based on the proposal in the negroni documentation https://github.com/codegangsta/negroni#route-specific-middleware) but it doesn't work because httptreemux is not accepting negroni.New() as a handler.

mux := httptreemux.New()
webmux := httptreemux.New()

n := negroni.Classic()
n.Use(gzip.Gzip(gzip.DefaultCompression))
n.Use(negroni.HandlerFunc(handler.ApiKey))

api.NewApi(mux, webmux).Init() // where routes get defined

// based on negroni documentation
mux.Handle("GET", "/web", negroni.New(      // not even sure if GET makes sense for all
    negroni.HandlerFunc(handler.ApiKeyWeb),
    negroni.Wrap(webmux),
))

n.UseHandler(mux)
n.Run(":8080")

Can you think of a way to make this work out? You can tell that I'm pretty new to middlewares gg

Any help, or tip or direction is very appreciated :)

regards

rework ParamsContextKey

To prevent external collisions of the params context value, the following changes should be made:

  1. add a new unexported type
  2. define an unexported const value of the above type
  3. use that const value as the params context key

This pattern is explained on the Go blog: https://blog.golang.org/context#TOC_3.2.

Benchmark link 404s

I'm unsure if the benchmark link in the README points to a private repo or if the link is broken, but either way could it be updated to point to benchmarks referenced in the README? Thanks!

Trailing slashes - default behavior seems odd for index

With httprouter, I can use the code below as a catch-all. I understand it's not really necessary, but it's for the sake of having a system starting point.

routerMain.GET("/*route", middleWareMain.ThenFunc(fallbackHandler))

So that I can build up routing that is prioritized before, and does not conflict with, the catch-all, treemux must be used. However, I must now add another route for index requests.

treemuxMain.GET("/", middleWareMain.ThenFunc(fallbackHandler))
treemuxMain.GET("/*route", middleWareMain.ThenFunc(fallbackHandler))

There is a trailing slash added to the index and it looks odd/ugly to me (I can't add an "empty" route without receiving an 'index out of range' error). Am I misunderstanding something?

Thanks!

Laravel style Routing

Hi Daniel,

Can you give me a hint on what me be changed so that the route parameters are enclosed in {} and question marks can be used to make it optional just like in Laravel.

http://laravel.com/docs/4.2/routing#route-parameters

Route::get('user/{id}', function($id)
{
return 'User '.$id;
});

Optional Route Parameters

Route::get('user/{name?}', function($name = null)
{
return $name;
});

Optional Route Parameters With Defaults

Route::get('user/{name?}', function($name = 'John')
{
return $name;
});

Regular Expression Route Constraints

Route::get('user/{name}', function($name)
{
//
})
->where('name', '[A-Za-z]+');

Route::get('user/{id}', function($id)
{
//
})
->where('id', '[0-9]+');

RedirectTrailingSlash strips away query string

I'm not sure if this is a bug or intended behavior, but it seems that when treemux does a redirect based on an extra (or missing) trailing slash, the url that it passes to http.Redirect does not include the original query string parameters.

For example if I have a path called /api/v1/objects/ then /api/v1/objects?limit=10 gets redirected to /api/v1/objects/ and the query string is lost.

How to best serve sub folders and files?

Hi,
I'd like to serve everything that is present in folder called pages. Here's a sample structure:

|____pages
| |____webgl
| | |____Compressed
| | | |____webgl-export.jsgz
| | |____index.html
| | |____Release
| | | |____fileloader.js
| | |____TemplateData
| | | |____default-cover.jpg
| | | |____favicon.ico

Here's what I'm doing with httptreemux:

func pagesHandler(w http.ResponseWriter, r *http.Request, params map[string]string) {
    http.ServeFile(w, r, filepath.Join("pages", params["filepath"]))
    return
}

func InitializePages(router *httptreemux.TreeMux) {
    // For serving standalone projects or pages saved in in content/pages
    router.GET("/pages/*filepath", pagesHandler)
}

This works fine when I open e. g. http://server.url/pages/webgl/Release/fileloader.js.

However, http.ServeFile is supposed to serve the index.html when opening http://server.url/pages/webgl/.
It does that, but the path will be rewritten to the one without a trailing slash (http://server.url/pages/webgl).
This breaks links to files and sub folders in the index.html, e.g.:

<script src="Release/fileloader.js"></script>

which will now link to http://server.url/pages/Release/fileloader.js instead of http://server.url/pages/webgl/Release/fileloader.js.

Thanks
Kai

r.RequestURI vs r.URL.Path

Is there a reason that you are using r.RequestURI instead of r.URL.Path. The problem with r.RequestURI ist that it does not work with http.StripPrefix, so the following code always returns a 404 because it does not notice that the "admin" prefix has been stripped off the path (http.StripPrefix does only modify r.URL.Path):

mux := httptreemux.New()
mux.GET("/", getIndex)
mux.GET("/users", getUsers)
mux.GET("/users/:id", getUser)
http.Handle("/admin/", http.StripPrefix("/admin", mux)

When will inStaticToken be used

I tried to read your source code and found that I could not understand the inStaticToken scenario. Could you give me a hint?

For Chinese:
我试图阅读您的源代码,发现我无法理解inStaticToken场景。你能给我一个提示吗?

Update Support for Go Modules

Hi, I have been using this package for possibily 5 years. With the new module system, it would be nice to update the module support.

There is a sticky point in that the current tag is v5.0.2. If you agree to this I would recommend tagging the project to v6.0.0 and have people use https://github.com/dimfeld/httptreemux/v6 on the import paths to be compliant.

What do you think? I can do this.

Method Not Allowed returned when it should not be.

Consider the following code:

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/dimfeld/httptreemux"
)

func main() {
    router := httptreemux.New()
    router.OPTIONS("/*cors", handler)
    router.GET("/foo/:id", handler)
    log.Fatal(http.ListenAndServe(":8080", router))
}

func handler(w http.ResponseWriter, r *http.Request, params map[string]string) {
    w.Write([]byte(fmt.Sprintf("%s OK", r.Method)))
}

Running the corresponding application and making requests to it produces:

$ http localhost:8080/foo/2
HTTP/1.1 200 OK
Content-Length: 6
Content-Type: text/plain; charset=utf-8
Date: Mon, 14 Mar 2016 03:55:48 GMT

GET OK

Correct

$ http options localhost:8080/foo
HTTP/1.1 200 OK
Content-Length: 10
Content-Type: text/plain; charset=utf-8
Date: Mon, 14 Mar 2016 03:55:57 GMT

OPTIONS OK 

Correct

$ http options localhost:8080/foo/2
HTTP/1.1 405 Method Not Allowed
Allow: GET
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Date: Mon, 14 Mar 2016 03:55:54 GMT

Incorrect

Remove routes

Since we can now add routes at runtime (using the mutex), I think it would be appropriate to provide a function to remove routes as well.

Please excuse me if there is such a function already. I couldn't find anything.

Support Go Modules not properly integrated

I think integration of go modules is good but i think it should not include version/release in path

module github.com/dimfeld/httptreemux/v5

Issues I have with that is all my code import path was based on github.com/dimfeld/httptreemux and i can used to just call httptreemux.NAME OF FUNC now i have to change all my import path for httptreemux by adding alias like

import httptreemux "github.com/dimfeld/httptreemux/v5"

also in case of creating new version/release you have to change path ?

please take a looks this example: https://github.com/lib/pq

Expose LookupResult params field

We would like to be able to instrument httptreemux for observability with Datadog, similar to what was done for julienschmidt/httprouter in DataDog/dd-trace-go/contrib/julienschmidt/httprouter/httprouter.go.

In short, in order to build the Datadog resource name from the request path we need access to the LookupResult params field. For this reason we would like to export this struct field, i.e. rename LookupResult.params to LookupResult.Params.

This is what the router could could look like to add Datadog trace support to httptreemux:

// Package httptreemux provides functions to trace the dimfeld/httptreemux/v5 package (https://github.com/dimfeld/httptreemux).
package httptreemux // import "gopkg.in/DataDog/dd-trace-go.v1/contrib/dimfeld/httptreemux/v5"

import (
	"math"
	"net/http"
	"strings"

	httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
	"gopkg.in/DataDog/dd-trace-go.v1/internal/log"

	"github.com/dimfeld/httptreemux/v5"
)

// Router is a traced version of httptreemux.TreeMux.
type Router struct {
	*httptreemux.TreeMux
	config *routerConfig
}

// New returns a new router augmented with tracing.
func New(opts ...RouterOption) *Router {
	cfg := new(routerConfig)
	defaults(cfg)
	for _, fn := range opts {
		fn(cfg)
	}
	if !math.IsNaN(cfg.analyticsRate) {
		cfg.spanOpts = append(cfg.spanOpts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate))
	}
	cfg.spanOpts = append(cfg.spanOpts, tracer.Measured())
	log.Debug("contrib/dimfeld/httptreemux/v5: Configuring Router: %#v", cfg)
	return &Router{httptreemux.New(), cfg}
}

// ServeHTTP implements http.Handler.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// get the resource associated to this request
	route := req.URL.Path
	lr, _ := r.Lookup(w, req)
	for k, v := range lr.Params {
		// replace parameter values in URL path with their names
		route = strings.Replace(route, v, ":"+k, 1)
	}
	resource := req.Method + " " + route
	// pass r.TreeMux to avoid a circular reference panic on calling r.ServeHTTP
	httptrace.TraceAndServe(r.TreeMux, w, req, &httptrace.ServeConfig{
		Service:  r.config.serviceName,
		Resource: resource,
		SpanOpts: r.config.spanOpts,
	})
}

The plan is to submit a PR against DataDog/dd-trace-go once the necessary access to the LookupResult params is granted.

Posting a html form doesn't work with trailing slash redirect.

Hi,
thanks for making this router. I'm trying it out right now.
However, I encountered this weird behavior:

I have a html form that does a POST to /admin/register/ (notice the trailing slash).

When I use httptreemux like this:

router.GET("/admin/register", getRegistrationHandler)
router.POST("/admin/register", postRegistrationHandler)

the postRegistrationHandler isn't called.

But when I use httptreemux like this:

router.GET("/admin/register", getRegistrationHandler)
router.POST("/admin/register/", postRegistrationHandler)

it works just fine.

This seems to be a problem with redirection, right? Maybe because I defined the GET method without a trailing slash?

Thanks

index out of range panic on bad URLs

👋 hello!

we often see panics coming from our router when we get hit by people vuln scanning our app. we use lookupFunc to serve our frontend if no backend routes match. I think we're just missing a range check before evaluating.

an example URL that panics: GET /images/../cgi/cgi_i_filter.js

Here's the rough shape of our setup:

// LookupFunc is associated with a mux router. It permits querying the router to see if it
// can respond to a request.
type LookupFunc func(w http.ResponseWriter, r *http.Request) (httptreemux.LookupResult, bool)

func SinglePageApp(urlPrefix, dirPath string, includeSourcemaps bool) func(h http.Handler, lookupFunc LookupFunc) http.Handler {
fs := static.LocalFile(dirPath, true)
fileserver := http.FileServer(fs)
if urlPrefix != "" {
fileserver = http.StripPrefix(urlPrefix, fileserver)
}

return func(h http.Handler, lookupFunc LookupFunc) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// If we have an official route for this request, we should skip our handler. We
		// only run when we can't find a match.
		if _, found := lookupFunc(w, r); found {
			h.ServeHTTP(w, r)
			return
		}

		if !fs.Exists(urlPrefix, r.URL.Path) {
			r.URL.Path = "/"
		}
                     // serving the SPA goes here

thanks! 🙏

Allow params in concurrent paths to be different

Hi, thanks for your work on httptreemux.

I just noticed that the paths /users/:pk/:related and /users/:id/updatePassword cannot exist concurrently. The first param must be identical (:pk or :id but not both).

Would it be possible to easily solve the problem ? I think it should not change the behavior but if it is too complicated...

Thanks !

404 when wildcard value starts with "p"

Hey,
I'm experiencing a very peculiar issue.
Let's say my server is at http://127.0.0.1:8084 and I'm using this setup:

mux := httptreemux.New()
mux.GET("/:slug/", postHandler)

postHandler get's called every time (well, every time I've tried so far) EXCEPT when I'm opening:

http://127.0.0.1:8084/patch/
http://127.0.0.1:8084/post-test/
http://127.0.0.1:8084/prmmmmm/

and (so far) every other url with a slug that starts with "p". I haven't tried all of the other letters yet.

A few things to note:

  • This doesn't happen with mux.GET("/tag/:slug/", tagHandler) and a slug that starts with "p". So it might only happen when the wildcard is the first part of the path.
  • When I open e. g. http://127.0.0.1:8084/patch it doesn't get redirected to http://127.0.0.1:8084/patch/, so the bug might be before the redirection happens?

Thanks

/EDIT: I found the same issue with "a". all other lower case letters are fine. I haven't tried any special characters except for öäü.

OAuth package goth does not work

Apparently query parameters which should be available via req.URL.Query().Get("some-key") get removed by httptreemux. Not that this is necessarily a bug, but if this is by design, one should be aware of that.

Add handler templates for params

Please add the value of the handler template in params.

In case router.GET("/:year/:month/:post", postHandler) it would be nice to have smth like params["template"]=="/:year/:month/:post"

I've the same function for multiple different URL. I need to identify which URL template has triggered the function.

use for non-http

Is there a way to use this for non-http scenarios. I only need the path routing implementation. i.e. string -> parse -> call a function based on arguments in "route" path

Can't set global OPTIONS handler

I tried creating a global OPTIONS handler (simplified below) that would match any path, but everything still returned a 405. I think the issue is that all the OPTIONS requests matched paths but no methods, and after the path matched, it didn't fall back to my wildcard. I don't want to manually add OPTIONS for every single path, and the mux panics if I wrap the Handle function because there are multiple registrations for the same path. Any suggestions?

mux.Handle("OPTIONS", "/*path", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
    w.Header().Set("Access-Control-Allow-Credentials", "true")
    w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
    w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, PATCH, DELETE")
    w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, Accept-Encoding")
    w.Header().Set("Access-Control-Max-Age", "600")
    w.WriteHeader(204)
})

godoc does not contain useful information

When using godoc from the command line or via godoc.org, useful information is not included, including routing rules. I think it would be a good idea to move/copyinformation that's in README.md to a godoc package comment.

httptreemux is incompatible with httptest.ResponseRecorder testing style

Running the following snippet gives a panic:

package main

import (
    "net/http"
    "net/http/httptest"

    "github.com/dimfeld/httptreemux"
)

func main() {
    req, _ := http.NewRequest("GET", "http://localhost/", nil)

    rtr := httptreemux.New()
    rtr.GET("/", func(_ http.ResponseWriter, _ *http.Request, _ map[string]string) {})

    respRec := httptest.NewRecorder()
    rtr.ServeHTTP(respRec, req)
}
panic: runtime error: index out of range

goroutine 1 [running]:
github.com/dimfeld/httptreemux.(*TreeMux).ServeHTTP(0xc2080680c0, 0x567f68, 0xc20801e0c0, 0xc208050b60)
    /.../github.com/dimfeld/httptreemux/router.go:170 +0x6ff
main.main()
    /.../test.go:21 +0x3ee
exit status 2

The reason is that httptreemux uses request.RequestURI which is not set for "client" requests.

Allow `*` in paths

In my API I need to define paths with * in them. I haven't seen a way to escape the wildcard character - maybe I missed it?

Empty catch-all routes don't cache empty values

I am trying to use *path catch-all syntax to match both http://localhost:8080/example/some/path and http://localhost:8080/example/. I expect that catch-all syntax will handle latter with empty "path" param, but it doesn't. I got 404.

package main

import (
	"github.com/dimfeld/httptreemux"
	"net/http"
)

func main() {
	router := httptreemux.NewContextMux()
	h := func(w http.ResponseWriter, r *http.Request) {
		params := httptreemux.ContextParams(r.Context())
		w.Write([]byte(params["path"]))
	}
	router.GET("/example/*path", h)
	http.ListenAndServe(":8080", router)
}

I can workaround it with adding one more handler:

	router.GET("/example/*path", h)
	router.GET("/example/", h)

But I didn't have to do it with https://github.com/julienschmidt/httprouter

If this is not an issue by design, I would at least mention this in documentation :)

Feature request: Add .WithContext() option

In addition to the new group.UsingContext(), I would like to see a group.WithContext(), that can be passed an existing context.

This is useful when passing in a general server context.Context object.

Example:

func main() {
   ctx := context.WithValue(context.Background(), "somekey", "somevalue")
   router := httptreemux.New()
   ctxGroup := router.WithContext(ctx)
   ctxGroup.GET(....)

RedirectTrailingSlash causing error 405 method not allowed

Hi,
If I understood the documentation correctly, the following code should allow both POST request to the following paths localhost:8080/api/v1/account and localhost:8080/api/v1/account/:

apiRoot := router.NewGroup("/api/v1")
account := apiRoot.NewGroup("/account")
account.GET("/:id", api.AccountHandler.
account.POST("", api.AccountHandler.POST)

However, it appears as only localhost:8080/api/v1/account is working and localhost:8080/api/v1/account/ throws an error 405.

Any ideas on what is causing this or if it's an expected behavior?

Thanks!

proposal: use http.HandlerFunc instead of the 2 custom handler funcs

In go 1.7, the request has an extra context field. It can be used for storing the parameter map and the panic data, instead of using custom handler functions with an extra parameter.

I've played around with the idea in this fork:
https://github.com/urandom/httptreemux

To me it seems a good usage of the context is to store the whole parameter map as a single key. That's because usually when people have url parameters, they want all of them, as opposed just one or two items. The retrieval is also faster, since the context is essentially a linked list. The panic handler is quite similar.

I've added two functions in in the package, RequestParams and RequestError, though I'm not sure if the names are perfect.

If you approve of this proposal, perhaps it would be best if you make these changes in a version 5 branch, so you can still have version 4 if you want to break the api in some other way for the go1.6 and lower.

Dependency on dimfeld/httppath

When tinkering with your router, I found that it depends on github.com/dimfeld/httppathwhich is not declared. Do you want a PR for including this dependency directly in the router(because it is only one func) or just one for adding this to the readme ?

Thanks.

Setting Content-Type header to application/json is not being set in the response

Here's an example:

package main

import (
        mux "github.com/dimfeld/httptreemux"
        "encoding/json"
        "net/http"
)

func main() {
        router := mux.New()
        router.GET("/", func(w http.ResponseWriter, r *http.Request, _ map[string]string) {
                response, _ := json.Marshal([]string{"foo", "bar"})
                w.WriteHeader(http.StatusOK)
                w.Header().Set("Content-Type", "application/json")
                w.Write(response)
        })
        http.ListenAndServe(":8000", router)
}

It would be expected that the Content-Type header would be set to application/json in the response to a request to localhost:8000/. However, it's not, it's simply set to text/plain.

Is this expected behavior and how can I override httpremux?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.