Echo Inspired Stand Alone URL Router

Vestigo - A Standalone Golang URL Router

Join the chat at https://gitter.im/husobee/vestigo Build Status Coverage Status GoDoc

Abstract

Many fast Golang URL routers are often embedded inside frameworks. Vestigo is a stand alone url router which has respectable performance that passes URL parameters to handlers by embedding them into the request's Form.

There is such an abundance of parts and pieces that can be fit together for go web services, it seems like a shame to have a very fast URL router require the use of one framework, and one context model. This library aims to give the world a fast, and featureful URL router that can stand on it's own, without being forced into a particular web framework.

Design

  1. Radix Tree Based
  2. Attach URL Parameters into Request (PAT style) instead of context
  3. HTTP Compliance (TRACE, OPTIONS, HEAD)
  4. CORS Enabled (per resource access-controls)

TODOs for V1

  • Router functioning with a resource concept attached to leaf nodes
  • Use resources to drive responses to particular Methods (not found v not allowed)
  • Implement Resource and Globally scoped CORS preflights
  • Fix bug in router where handler.allowedMethods is getting populated where it shouldn't be
  • Validate with Tests RFC 2616 Compliance (OPTIONS, etc)

TODOs for V2

  • Validators for URL params
  • Implement RFC 6570 URI Parameters

Performance

Initial implementation on a fork of standard http performance testing library shows the following:

BenchmarkVestigo_GithubAll         20000             75763 ns/op            9280 B/op        339 allocs/op

I should mention that the above performance is about 2x slower than the fastest URL router I have tested (Echo/Gin), and is slightly worse than HTTPRouter, but I am happy with this performance considering this implementation is the fastest implementation that can handle standard http.HandlerFunc handlers, without forcing end users to use a particular context, or use a non-standard handler function, locking them into an implementation.

Examples

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/husobee/vestigo"
)

func main() {
	router := vestigo.NewRouter()
	// you can enable trace by setting this to true
	vestigo.AllowTrace = true

	// Setting up router global  CORS policy
	// These policy guidelines are overriddable at a per resource level shown below
	router.SetGlobalCors(&vestigo.CorsAccessControl{
		AllowOrigin:      []string{"*", "test.com"},
		AllowCredentials: true,
		ExposeHeaders:    []string{"X-Header", "X-Y-Header"},
		MaxAge:           3600 * time.Second,
		AllowHeaders:     []string{"X-Header", "X-Y-Header"},
	})

	// setting two methods on the same resource
	router.Get("/welcome", GetWelcomeHandler)
	router.Post("/welcome", PostWelcomeHandler)

	// URL parameter "name"
	router.Post("/welcome/:name", PostWelcomeHandler)

	// Catch-All methods to allow easy migration from http.ServeMux
	router.HandleFunc("/general", GeneralHandler)

	// Below Applies Local CORS capabilities per Resource (both methods covered)
	// by default this will merge the "GlobalCors" settings with the resource
	// cors settings.  Without specifying the AllowMethods, the router will
	// accept any Request-Methods that have valid handlers associated
	router.SetCors("/welcome", &vestigo.CorsAccessControl{
		AllowMethods: []string{"GET"},                    // only allow cors for this resource on GET calls
		AllowHeaders: []string{"X-Header", "X-Z-Header"}, // Allow this one header for this resource
	})

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

func PostWelcomeHandler(w http.ResponseWriter, r *http.Request) {
	name := vestigo.Param(r, "name") // url params live in the request
	w.WriteHeader(200)
	w.Write([]byte("welcome " + name + "!"))
}

func GetWelcomeHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(200)
	w.Write([]byte("welcome!"))
}

func GeneralHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(200)
	w.Write([]byte("Gotta catch em all!"))
}

Middleware

Router helper methods (Get, Post, ...) support optional middleware (vestigo provides only middleware type, it is up to the user to create one).

router.Get("/welcome", GetWelcomeHandler, someMiddleware)

someMiddleware := func(f http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// before
		f(w, r)
		// after
	}
}

To break the chain (for example in case of authentication middleware, we don't want to continue execution), just do not call passed handler function. Example:

auth := func(f http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if authSuccessful() {
			f(w, r)
		}
	}
}

App Performance with net/http/pprof

It is often very helpful to view profiling information from your web application. Below is an example of hooking up net/http/pprof with vestigo serving the routes:

// Load the routes.
func Load(router *vestigo.Router) {
	router.Get("/debug/pprof/", Index)
	router.Get("/debug/pprof/:pprof", Profile)
}

// Index shows the profile index.
func Index(w http.ResponseWriter, r *http.Request) {
	pprof.Index(w, r)
}

// Profile shows the individual profiles.
func Profile(w http.ResponseWriter, r *http.Request) {
	switch vestigo.Param(r, "pprof") {
	case "cmdline":
		pprof.Cmdline(w, r)
	case "profile":
		pprof.Profile(w, r)
	case "symbol":
		pprof.Symbol(w, r)
	case "trace":
		pprof.Trace(w, r)
	default:
		Index(w, r)
	}
}

Note on wildcards: if you want to get the actual path matched by the wildcard you can perform vestigo.Param(*http.Request, "_name") to get the matched path, example below:

router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
	fmt.Println(vestigo.Param(r, "_name"))
})

Licensing

Contributing

If you wish to contribute, please fork this repository, submit an issue, or pull request with your suggestions. Please use gofmt and golint before trying to contribute.

Comments
  • BUG: param with same prefix to static route not working as expected

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

    code version:

    last commit index: 23d011ed4e5ec0f73678c48df5ca8b7ace92f8d0

    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())
    	}
    }
    
  • r.FormValue does not return parameter when it is called in middleware first

    r.FormValue does not return parameter when it is called in middleware first

    I have a RestHandler that allows me to change the HTTP Method via a form field so I can use PATCH and DELETE for actual PATCH and DELETE operations.

    When I try to use the middleware code with your router, the http.Request.FormValue function does not return the correct value anymore.

    Here is the code:

    • https://play.golang.org/p/HYR-Ws6UBt

    Here is the output:

    • 2016/07/06 03:08:10 Working ID: 4
    • 2016/07/06 03:08:10 NotWorking ID:
  • Strange Wildcard behavior

    Strange Wildcard behavior

    Hi,

    while playing around with this cool router, I noticed something quite strange. Given the following source:

    package main
    
    import (
    	"fmt"
    	"net/http"
    
    	"github.com/husobee/vestigo"
    )
    
    func main() {
    	router := vestigo.NewRouter()
    
    	router.Get("/greet/:name", GreetUser)
    	router.HandleFunc("/*", IndexHandler)
    
    	http.ListenAndServe(":8080", router)
    }
    
    func GreetUser(w http.ResponseWriter, r *http.Request) {
    	fmt.Fprintf(w, "Welcome %s\n", vestigo.Param(r, "name"))
    }
    
    func IndexHandler(w http.ResponseWriter, r *http.Request) {
    	fmt.Fprintf(w, "You opened %s\n", vestigo.Param(r, "_name"))
    }
    

    The result looks like this:

    /greet/	-> 404 (Expected)
    /greet/demo	-> Welcome demo (Expected)
    /demo	-> You opened demo (Expected)
    /demo/	-> You opened demo/ (Expected)
    /gre	-> Welcome gre (Unexpected)
    

    Is it just me who finds this strange?

  • Route matching

    Route matching

    Hi,

    I have been testing your router lately and I have run into unexpected behaviour. I'm not completely sure whether it is a bug in radix tree implementation or design choice, but I'll give you an example below.

    package main
    
    import (
        "log"
        "net/http"
    
        "github.com/husobee/vestigo"
    )
    
    func main () {
        r := vestigo.NewRouter()
    
        r.Get("/new", GetWelcomeHandler)
        r.Get("/:name", GetWelcomeHandler)    
    
        log.Fatal(http.ListenAndServe(":1234", r))
    }
    
    func PostWelcomeHandler(w http.ResponseWriter, r *http.Request) {
        name := vestigo.Param(r, "name") // url params live in the request
        w.WriteHeader(200)
        w.Write([]byte("welcome " + name +"!"))
    }
    func GetWelcomeHandler(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        w.Write([]byte("welcome!"))
    }
    

    Now I'm going to make some GET requests:

    01:53:54 ~/Github/vestigo-example $ http http://localhost:1234/new
    HTTP/1.1 200 OK
    Content-Length: 8
    Content-Type: text/plain; charset=utf-8
    Date: Sat, 12 Dec 2015 19:41:14 GMT
    
    welcome!
    
    01:53:54 ~/Github/vestigo-example $ 
    
    01:53:54 ~/Github/vestigo-example $ http http://localhost:1234/foobar
    HTTP/1.1 200 OK
    Content-Length: 8
    Content-Type: text/plain; charset=utf-8
    Date: Sat, 12 Dec 2015 19:42:19 GMT
    
    welcome!
    
    01:53:54 ~/Github/vestigo-example $ 
    

    This is expected, but here's an edge case:

    01:53:54 ~/Github/vestigo-example $ http http://localhost:1234/new-foobar
    HTTP/1.1 404 Not Found
    Content-Length: 9
    Content-Type: text/plain; charset=utf-8
    Date: Sat, 12 Dec 2015 19:43:15 GMT
    
    Not Found
    
    01:53:54 ~/Github/vestigo-example $ 
    

    I'd expect the server to return welcome instead of 404. What happens here is that if you request anything which starts new you end up getting 404. Swapping route order in main.go does not affect anything.

    I have little knowledge of radix tree implementations, so I thought to ask you first whether this is a design choice or is it something which could be fixed?

  • Hangs at 100% CPU

    Hangs at 100% CPU

    When adding the /:lang/foo route any request that should end in a 404 (e.g. /hello/world) hangs instead without returning anything and maxing the CPU for the process.

    package main
    
    import (
    	"fmt"
    	"net/http"
    
    	"github.com/husobee/vestigo"
    )
    
    func main() {
    	r := vestigo.NewRouter()
    	http.Handle("/", r)
    
    	r.Get("/foo", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "foo") })
    	r.Get("/:lang/foo", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "foo") })
    
    	http.ListenAndServe(":8080", nil)
    }
    
  • Fix for ParamNames

    Fix for ParamNames

    I noticed that the function ParamNames gets all param names, but doesn't remove the : at the beginning of a parameter. If one tries to use the names returned by ParamNames, there will be additional : I added a simple TrimPrefix to solve this.

  • routing bug

    routing bug

    Hi,

    While testing the package I found an bug :

    router := vestigo.NewRouter()
    router.Get("/a/a", func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("found : a "))
        })
    router.Get("/a/b", func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("found : b "))
        }))
    router.Get("/a/:bb",  func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("cannot find bb ")) 
        }))
    
    log.Fatal(http.ListenAndServe("localhost:1234", router))
    

    if i navigate to /a/a and a/b it works okay. navigating to a/bb is not possible though .

    Hope it can be fixed. looks pretty neat. :+1:

  • Middleware - HandlerFunc instead of Handler?oO

    Middleware - HandlerFunc instead of Handler?oO

    Hi

    I might be doing something awfully wrong, but I usually wrote my middleware using something like:

    func MiddleWare(next http.Handler) http.Handler {
      return http.Handlerfunc(func(w http.ResponseWriter, r *http.Request) {
        ...
        next.ServeHTTP(w, r)
        ...
      })
    }
    

    Somehow Vestigos middleware type doesn't seem to like that. Why does it only accept 'http.HandlerFunc' instead of both 'http.HandlerFunc' and 'http.Handler' as usual?

    I just get:

    command-line-arguments ./routes.go:21:7: cannot use TestMiddle (type func(http.Handler) http.Handler) as type vestigo.Middleware in assignment

    Well, again,...maybe I made a mistake somewhere along the path.

    Best regards

  • Routing

    Routing "hangs" without wildcard

    Well ... I broke it again ... :sweat_smile:

    I got curious for a sec and tried this:

    package main
    
    import (
    	"fmt"
    	"net/http"
    
    	"github.com/husobee/vestigo"
    )
    
    func main() {
    	router := vestigo.NewRouter()
    
    	router.Get("/", ServeIndex)
    
    	router.Get("/users/:name", UserShow)
    
    	router.Get("/admin/", AdminIndex)
    
    	router.Get("/books/", BookIndex)
    	router.Get("/books/:book", BookShow)
    
    	http.ListenAndServe(":8080", router)
    }
    
    func ServeIndex(w http.ResponseWriter, r *http.Request) {
    	fmt.Fprintln(w, "root")
    }
    
    func BookIndex(w http.ResponseWriter, r *http.Request) {
    	fmt.Fprintln(w, "bookindex")
    }
    
    func BookShow(w http.ResponseWriter, r *http.Request) {
    	fmt.Fprintf(w, "book %s\n", vestigo.Param(r, "book"))
    }
    
    func UserShow(w http.ResponseWriter, r *http.Request) {
    	fmt.Fprintf(w, "user %s\n", vestigo.Param(r, "name"))
    }
    
    func AdminIndex(w http.ResponseWriter, r *http.Request) {
    	fmt.Fprintln(w, "admin")
    }
    

    It turns out, it doesn't work so well. /users/ "hangs" instead of 404.

    |Request|Result|Expected?| |---|---|---| |/|root|Yes| |/somethings|Not Found|Yes| |/books|Not Found|Yes| |/books/|bookindex|Yes| |/books/sonea|book sonea|Yes| |/users|Not Found|Yes| |/users/|[HANGS]|No (Expected: Not Found)| |/users/chris|user chris|Yes| |/admin|Not Found|Yes| |/admin/|admin|Yes| |/admin/chris|Not Found|Yes|

    Sorry to bother you yet again. But I realy like this router. And since I can't realy help with the source, I'd at least want to help by testing it as much as I can. :innocent: And just by the way: This is not a deal breaker. If one knows of this, it's easily catchable.

    At least I try to deliver a more test-friendly result in the response now ...

  • routing bug - route ignored

    routing bug - route ignored

    package main
    
    import (
        "fmt"
        "net/http"
    
        "github.com/husobee/vestigo"
    )
    
    func main() {
        r := vestigo.NewRouter()
    
        r.Get("/test", testHandler)
        r.Get("/test/:id", testGetHandler)
        r.Get("/test/list/:abc", testListHandler)
    
        http.ListenAndServe(":9999", r)
    }
    
    func testHandler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "testHandler")
    }
    
    func testGetHandler(w http.ResponseWriter, r *http.Request) {
        id := vestigo.Param(r, "id")
        fmt.Fprintln(w, "id:", id)
    }
    
    func testListHandler(w http.ResponseWriter, r *http.Request) {
        abc := vestigo.Param(r, "abc")
        fmt.Fprintln(w, "abc:", abc)
    }
    

    Given the above program, the routes "/test/123" and "/test/list/123" are both handled by testGetHandler. Shouldn't "/test/list/123" be handled by testListHandler (or at least 404ed).

    Removing the line: r.Get("/test/list/:abc", testListHandler) causes the route "/test/list/123" to 404.

    go version go1.6.2 windows/amd64 vestigo version 7aafc8cfe1bfaf7bfc9a083a112cc1b56b7dc219

  • Vestigo fails to resolve route and crashes

    Vestigo fails to resolve route and crashes

    First, thanks for a really good router package!

    I've run into an issue where vestigo sometimes fails to resolve routes if a shorter route with same prefix is also registered. The issue seems to depend on the order of registering the routes.

    Example:

    package main
    
    import (
    	"github.com/husobee/vestigo"
    	"net/http"
    	"fmt"
    )
    
    func main() {
    	router := vestigo.NewRouter()
    
    	router.Get("/path/:param1/:param2", func(w http.ResponseWriter, r *http.Request) {
    		p1 := vestigo.Param(r, "param1")
    		p2 := vestigo.Param(r, "param2")
    		w.Write([]byte(fmt.Sprintf("/path/p1/p2, p1='%s', p2='%s'", p1, p2)))
    	})
    
    	router.Get("/path/:param1", func(w http.ResponseWriter, r *http.Request) {
    		p1 := vestigo.Param(r, "param1")
    		w.Write([]byte(fmt.Sprintf("/path/p1, p1='%s'", p1)))
    	})
    
    	http.ListenAndServe(":1234", router)
    }
    

    Build and start the server, then access it:

    $ curl ":1234/path/p1"
    /path/p1, p1='p1'
    
    $ curl ":1234/path/p1/p2"
    curl: (52) Empty reply from server
    

    Program output:

    2018/02/26 11:28:07 http: panic serving [fe80::...%Ethernet Connection]:50681: runtime error: index out of range
    goroutine 6 [running]:
    net/http.(*conn).serve.func1(0xc042036400)
            C:/System/Go/src/net/http/server.go:1491 +0x131
    panic(0x618240, 0xc042004060)
            C:/System/Go/src/runtime/panic.go:458 +0x251
    github.com/husobee/vestigo.(*Router).find(0xc042004eb0, 0xc0420b02d0, 0x65b630, 0x65b684, 0x6)
            C:/Work/go/src/github.com/husobee/vestigo/router.go:227 +0x11ae
    github.com/husobee/vestigo.(*Router).Find(0xc042004eb0, 0xc0420b02d0, 0x65b630)
            C:/Work/go/src/github.com/husobee/vestigo/router.go:171 +0x3c
    github.com/husobee/vestigo.(*Router).ServeHTTP(0xc042004eb0, 0x7502c0, 0xc042039c70, 0xc0420b02d0)
            C:/Work/go/src/github.com/husobee/vestigo/router.go:66 +0x3c
    net/http.serverHandler.ServeHTTP(0xc042036200, 0x7502c0, 0xc042039c70, 0xc0420b02d0)
            C:/System/Go/src/net/http/server.go:2202 +0x84
    net/http.(*conn).serve(0xc042036400, 0x750680, 0xc042008a00)
            C:/System/Go/src/net/http/server.go:1579 +0x4be
    created by net/http.(*Server).Serve
            C:/System/Go/src/net/http/server.go:2293 +0x454
    

    If I change the order of adding the routes, it works. This is however very error prone in my main application since it registers a lot of routes, and accidentally breaking the server becomes easy.

  • Support Go Modules

    Support Go Modules

    Hello,

    Please consider supporting Go Modules, the new packaging standard that will be adopted fully in Go 1.12. Experimental support is in Go 1.11 and the new module paths are supported in Go 1.9.7+ and Go 1.10.3+ in a read-only manner for backwards compatibility with all supported versions of Go.

    Because this library has only one external dependency (and only during testing) which already supports modules, is still v1, and is already using semver compatible tags, the go.mod file is fairly simple. The only other thing that would need to be done is to create a release after this patch is merged (eg. v1.1.1) to allow consumers of this library to pin to a version with the go.mod file.

    This is fully backwards compatible because versions of Go older than 1.9.7 will ignore the mod file and import this package as they always have.

    I have also taken the liberty of bumping Travis to test against supported versions of Go that are still receiving security updates, if this is incorrect and you support unsupported versions of Go, I'll happily change it back, I just thought it would be easier to test against versions that support modules.

    To manage the mod file yourself in the future, use Go 1.11+ and run the command go mod tidy to include all imports in the mod file. To update dependencies, go get can be used as usual (go get -u to update all packages, or go get <library> to pull updates for an individual dep). Note that go get will never up the major version since it tries not to break your build, so you do have to manually bump that if a library makes a breaking change.

    EDIT: The test failures appear to be unrelated. Tests are passing, but the coveralls secret isn't being set.

  • Middleware Chains - Nil pointer dereference

    Middleware Chains - Nil pointer dereference

    Hi

    I was playing around building different middleware chains on the fly. I was using a slice: middleware vestigo.Middleware[] for that purpose. This sometimes resulted in a crash with nil pointer dereference / invalid memory access. I traced it back to a function in 'router.go' in the vestigo package:

    func buildChain(f http.HandlerFunc, m ...Middleware) http.HandlerFunc {
        // if our chain is done, use the original handlerfunc
        if len(m) == 0 {
            return f
        }
        // otherwise nest the handlerfuncs
        return m[0](buildChain(f, m[1:cap(m)]...))
    }
    

    I replaced the line return m[0](buildChain(f, m[1:cap(m)]...)) with return m[0](buildChain(f, m[1:len(m)]...))

    on my local sources, now everything is fine. I was wondering why you were using cap here at any rate? Wouldn't using 'len' be safer?

  • Issues with wildcard routes and trailing path separator

    Issues with wildcard routes and trailing path separator

    I'm having issues with wildcard routes and trailing path separators.

    Example routes: /common/this/*substr /common/that/:param/*substr

    Testing on paths ending with a trailing slash will:

    1. Succeed when not expected
    2. Include parts of the path before the wildcard in the result.

    Full example:

    package main
        
    import (
        "github.com/husobee/vestigo"
        "net/http"
        "fmt"
        "net/http/httptest"
    )
    
    func testrouter(router *vestigo.Router, method string, url string, expected string) {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest(method, url, nil)
        handler := router.Find(req)
        handler(w, req)
        reply := w.Body.String()
        fmt.Println("\n* Testing:", url)
        fmt.Println("- reply:", reply)
        fmt.Println("- expected:", expected)
        if reply == expected { fmt.Println("OK") } else { fmt.Println("FAILED") }
    }
    
    func main() {
        router := vestigo.NewRouter()
    
        router.Get("/unique/*substr", func(w http.ResponseWriter, r *http.Request) {
            substr := vestigo.Param(r, "_name")
            fmt.Fprintf(w, `"%s"`, substr)
        })
    
        router.Get("/common/this/*substr", func(w http.ResponseWriter, r *http.Request) {
            substr := vestigo.Param(r, "_name")
            fmt.Fprintf(w, `"%s"`, substr)
        })
    
        router.Get("/common/that/:param/*substr", func(w http.ResponseWriter, r *http.Request) {
            param := vestigo.Param(r, "param")
            substr := vestigo.Param(r, "_name")
            fmt.Fprintf(w, `"%s", "%s"`, param, substr)
        })
    
        testrouter(router, "GET", "/unique/",                 `""`)
        testrouter(router, "GET", "/unique/abc",              `"abc"`)
        testrouter(router, "GET", "/common/this/",            `""`)
        testrouter(router, "GET", "/common/this/abc",         `"abc"`)
        testrouter(router, "GET", "/common/this/abc/",        `"abc/"`)
        testrouter(router, "GET", "/common/this/abc/def",     `"abc/def"`)
        testrouter(router, "GET", "/common/that/",            `Not Found`)
        testrouter(router, "GET", "/common/that/abc/",        `"abc", ""`)
        testrouter(router, "GET", "/common/that/abc/def",     `"abc", "def"`)
        testrouter(router, "GET", "/common/that/abc/def/",    `"abc", "def/"`)
        testrouter(router, "GET", "/common/that/abc/def/ghi", `"abc", "def/ghi"`)
    }
    

    Output:

    * Testing: /unique/
    - reply: "unique/"
    - expected: ""
    FAILED
    
    * Testing: /unique/abc
    - reply: "abc"
    - expected: "abc"
    OK
    
    * Testing: /common/this/
    - reply: "is/"
    - expected: ""
    FAILED
    
    * Testing: /common/this/abc
    - reply: "abc"
    - expected: "abc"
    OK
    
    * Testing: /common/this/abc/
    - reply: "abc/"
    - expected: "abc/"
    OK
    
    * Testing: /common/this/abc/def
    - reply: "abc/def"
    - expected: "abc/def"
    OK
    
    * Testing: /common/that/
    - reply: "at", "/"
    - expected: Not Found
    FAILED
    
    * Testing: /common/that/abc/
    - reply: "abc", "/"
    - expected: "abc", ""
    FAILED
    
    * Testing: /common/that/abc/def
    - reply: "abc", "def"
    - expected: "abc", "def"
    OK
    
    * Testing: /common/that/abc/def/
    - reply: "abc", "def/"
    - expected: "abc", "def/"
    OK
    
    * Testing: /common/that/abc/def/ghi
    - reply: "abc", "def/ghi"
    - expected: "abc", "def/ghi"
    OK
    
  • The router find function can't return matchAny raw router name

    The router find function can't return matchAny raw router name

    When the METHOD is GET, route is /root/home/*filepath

    The request of url '/root/home/log' should return /root/home/*filepath, but now just return /root/home

    
    func (r *Router) find(req *http.Request) (prefix string, h http.HandlerFunc) {
    	// get tree base node from the router
    	cn := r.root
    
    	h = notFoundHandler
    
    	if !validMethod(req.Method) {
    		// if the method is completely invalid
    		h = methodNotAllowedHandler(cn.resource.allowedMethods)
    		return
    	}
    
    	var (
    		search          = req.URL.Path
    		c               *node // Child node
    		n               int   // Param counter
    		collectedPnames = []string{}
    	)
    
    	// Search order static > param > match-any
    	for {
    
    		if search == "" {
    			if cn.resource != nil {
    				// Found route, check if method is applicable
    				theHandler, allowedMethods := cn.resource.GetMethodHandler(req.Method)
    				if theHandler == nil {
    					if uint16(req.Method[0])<<8|uint16(req.Method[1]) == 0x4f50 {
    						h = optionsHandler(r.globalCors, cn.resource.Cors, allowedMethods)
    						return
    					}
    					if allowedMethods != "" {
    						// route is valid, but method is not allowed, 405
    						h = methodNotAllowedHandler(allowedMethods)
    					}
    					return
    				}
    				h = corsFlightWrapper(r.globalCors, cn.resource.Cors, allowedMethods, theHandler)
    				for i, v := range collectedPnames {
    					if len(cn.pnames[req.Method]) > i {
    						AddParam(req, cn.pnames[req.Method][i], v)
    					}
    				}
    
    				brokenPrefix := strings.Split(prefix, "/")
    				prefix = ""
    				k := 0
    				for _, v := range brokenPrefix {
    					if v != "" {
    						prefix += "/"
    						if v == ":" {
    							if pnames, ok := cn.pnames[req.Method]; ok {
    								prefix += v + pnames[k]
    							}
    							k++
    						} else {
    							prefix += v
    						}
    					}
    				}
    			}
    			return
    		}
    
    		pl := 0 // Prefix length
    		l := 0  // LCP length
    
    		if cn.label != ':' {
    			sl := len(search)
    			pl = len(cn.prefix)
    			prefix += cn.prefix
    
    			// LCP
    			max := pl
    			if sl < max {
    				max = sl
    			}
    			for ; l < max && search[l] == cn.prefix[l]; l++ {
    			}
    		}
    
    		if l == pl {
    			// Continue search
    			search = search[l:]
    
    			if search == "" && cn != nil && cn.parent != nil && cn.resource.allowedMethods == "" {
    				parent := cn.parent
    				search = cn.prefix
    				for parent != nil {
    					if sib := parent.findChildWithLabel('*'); sib != nil {
    						search = parent.prefix + search
    						cn = parent
    						goto MatchAny
    					}
    					parent = parent.parent
    				}
    			}
    
    		}
    
    		if search == "" {
    			// TODO: Needs improvement
    			if cn.findChildWithType(mtype) == nil {
    				continue
    			}
    			// Empty value
    			goto MatchAny
    		}
    
    		// Static node
    		c = cn.findChild(search, stype)
    		if c != nil {
    			cn = c
    			continue
    		}
    		// Param node
    	Param:
    
    		c = cn.findChildWithType(ptype)
    		if c != nil {
    			cn = c
    
    			i, l := 0, len(search)
    			for ; i < l && search[i] != '/'; i++ {
    			}
    
    			collectedPnames = append(collectedPnames, search[0:i])
    			prefix += ":"
    			n++
    			search = search[i:]
    
    			if len(cn.children) == 0 && len(search) != 0 {
    				return
    			}
    
    			continue
    		}
    
    		// Match-any node
    	MatchAny:
    		//		c = cn.getChild()
    		c = cn.findChildWithType(mtype)
    		if c != nil {
    			cn = c
    			collectedPnames = append(collectedPnames, search)
    			search = "" // End search
    			continue
    		}
    		// last ditch effort to match on wildcard (issue #8)
    		var tmpsearch = search
    		for {
    			if cn != nil && cn.parent != nil && cn.prefix != ":" {
    				tmpsearch = cn.prefix + tmpsearch
    				cn = cn.parent
    				if cn.prefix == "/" {
    					var sib *node = cn.findChildWithLabel(':')
    					if sib != nil {
    						search = tmpsearch
    						goto Param
    					}
    					if sib := cn.findChildWithLabel('*'); sib != nil {
    						search = tmpsearch
    						goto MatchAny
    					}
    				}
    			} else {
    				break
    			}
    		}
    
    		// Not found
    		return
    	}
    }
    
    
  • Remove Globals

    Remove Globals

    Is there a reason why these must be called on the vestigo package instead of on the router? If someone wanted to use more than one router, they couldn't set this methods on the individual instances of the router. It also complicates testing because you can't guarantee state.

    vestigo.AllowTrace = true vestigo.CustomNotFoundHandlerFunc(fn) vestigo.CustomMethodNotAllowedHandlerFunc(fn) vestigo.Param(r, name)

Related tags
Simple Golang HTTP router
Simple Golang HTTP router

Bellt Simple Golang HTTP router Bellt Package implements a request router with the aim of managing controller actions based on fixed and parameterized

Sep 27, 2022
Bxog is a simple and fast HTTP router for Go (HTTP request multiplexer).

Bxog is a simple and fast HTTP router for Go (HTTP request multiplexer). Usage An example of using the multiplexer: package main import ( "io" "net

Dec 26, 2022
A high performance fasthttp request router that scales well
A high performance fasthttp request router that scales well

FastHttpRouter FastHttpRouter is forked from httprouter which is a lightweight high performance HTTP request router (also called multiplexer or just m

Dec 1, 2022
FastRouter is a fast, flexible HTTP router written in Go.

FastRouter FastRouter is a fast, flexible HTTP router written in Go. FastRouter contains some customizable options, such as TrailingSlashesPolicy, Pan

Sep 27, 2022
Go Router + Middleware. Your Contexts.

gocraft/web gocraft/web is a Go mux and middleware package. We deal with casting and reflection so YOUR code can be statically typed. And we're fast.

Dec 28, 2022
Go Server/API micro framework, HTTP request router, multiplexer, mux
Go Server/API micro framework, HTTP request router, multiplexer, mux

?? gorouter Go Server/API micro framework, HTTP request router, multiplexer, mux. ?? ABOUT Contributors: Rafał Lorenz Want to contribute ? Feel free t

Dec 16, 2022
A high performance HTTP request router that scales well

HttpRouter HttpRouter is a lightweight high performance HTTP request router (also called multiplexer or just mux for short) for Go. In contrast to the

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

httptreemux 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

Dec 28, 2022
:rotating_light: Is a lightweight, fast and extensible zero allocation HTTP router for Go used to create customizable frameworks.
:rotating_light: Is a lightweight, fast and extensible zero allocation HTTP router for Go used to create customizable frameworks.

LARS LARS is a fast radix-tree based, zero allocation, HTTP router for Go. view examples. If looking for a more pure Go solution, be sure to check out

Dec 27, 2022
An extremely fast Go (golang) HTTP router that supports regular expression route matching. Comes with full support for building RESTful APIs.

ozzo-routing You may consider using go-rest-api to jumpstart your new RESTful applications with ozzo-routing. Description ozzo-routing is a Go package

Dec 31, 2022
Pure is a fast radix-tree based HTTP router
Pure is a fast radix-tree based HTTP router

package pure Pure is a fast radix-tree based HTTP router that sticks to the native implementations of Go's "net/http" package; in essence, keeping the

Dec 1, 2022
Go HTTP router

violetear Go HTTP router http://violetear.org Design Goals Keep it simple and small, avoiding extra complexity at all cost. KISS Support for static an

Dec 10, 2022
xujiajun/gorouter is a simple and fast HTTP router for Go. It is easy to build RESTful APIs and your web framework.

gorouter xujiajun/gorouter is a simple and fast HTTP router for Go. It is easy to build RESTful APIs and your web framework. Motivation I wanted a sim

Dec 8, 2022
lightweight, idiomatic and composable router for building Go HTTP services

chi is a lightweight, idiomatic and composable router for building Go HTTP services. It's especially good at helping you write large REST API services

Jan 8, 2023
Router socks. One port socks for all the others.
Router socks. One port socks for all the others.

Router socks The next step after compromising a machine is to enumerate the network behind. Many tools exist to expose a socks port on the attacker's

Dec 13, 2022
:tongue: CleverGo is a lightweight, feature rich and high performance HTTP router for Go.

CleverGo CleverGo is a lightweight, feature rich and trie based high performance HTTP request router. go get -u clevergo.tech/clevergo English 简体中文 Fe

Nov 17, 2022
Fast and flexible HTTP router
Fast and flexible HTTP router

treemux - fast and flexible HTTP router Basic example Debug logging CORS example Error handling Rate limiting using Redis Gzip compression OpenTelemet

Dec 27, 2022
Fast, simple, and lightweight HTTP router for Golang

Sariaf Fast, simple and lightweight HTTP router for golang Install go get -u github.com/majidsajadi/sariaf Features Lightweight compatible with net/ht

Aug 19, 2022
Simple router build on `net/http` supports custom middleWare.

XMUS-ROUTER Fast lightweight router build on net/http supports delegate and in url params. usage : Create new router using NewRouter() which need rout

Dec 27, 2021