Skip to content

Commit

Permalink
Merge pull request #605 from uber/lu.router
Browse files Browse the repository at this point in the history
Swap out julienschmidt/httprouter
  • Loading branch information
skiptomylu authored Jul 16, 2019
2 parents 1008f97 + e7171ab commit 2ebd5a4
Show file tree
Hide file tree
Showing 9 changed files with 938 additions and 26 deletions.
2 changes: 0 additions & 2 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ import:
version: ^1.3.0
- package: github.com/uber-go/tally
version: ^3.3.8
# update to master since the last semver release is 2 years old
- package: github.com/julienschmidt/httprouter
version: master
- package: github.com/kardianos/osext
version: master
- package: go.uber.org/thriftrw
Expand Down
21 changes: 6 additions & 15 deletions runtime/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ import (
"fmt"
"net/http"

"github.com/julienschmidt/httprouter"
"github.com/opentracing/opentracing-go"
"github.com/pborman/uuid"
"github.com/pkg/errors"
"github.com/uber-go/tally"
zrouter "github.com/uber/zanzibar/runtime/router"
"go.uber.org/zap"
"net/url"
)
Expand Down Expand Up @@ -61,9 +61,9 @@ type HTTPRouter interface {

// ParamsFromContext extracts the URL parameters that are embedded in the context by the Zanzibar HTTP router implementation.
func ParamsFromContext(ctx context.Context) url.Values {
julienParams := httprouter.ParamsFromContext(ctx)
params := zrouter.ParamsFromContext(ctx)
urlValues := make(url.Values)
for _, paramValue := range julienParams {
for _, paramValue := range params {
urlValues.Add(paramValue.Key, paramValue.Value)
}
return urlValues
Expand Down Expand Up @@ -131,7 +131,7 @@ func (endpoint *RouterEndpoint) HandleRequest(
// httpRouter data structure to handle and register endpoints
type httpRouter struct {
gateway *Gateway
httpRouter *httprouter.Router
httpRouter *zrouter.Router
notFoundEndpoint *RouterEndpoint
methodNotAllowedEndpoint *RouterEndpoint
panicCount tally.Counter
Expand Down Expand Up @@ -167,8 +167,7 @@ func NewHTTPRouter(gateway *Gateway) HTTPRouter {
requestUUIDHeaderKey: gateway.requestUUIDHeaderKey,
}

router.httpRouter = &httprouter.Router{
RedirectTrailingSlash: true,
router.httpRouter = &zrouter.Router{
HandleMethodNotAllowed: true,
NotFound: http.HandlerFunc(router.handleNotFound),
MethodNotAllowed: http.HandlerFunc(router.handleMethodNotAllowed),
Expand All @@ -179,13 +178,6 @@ func NewHTTPRouter(gateway *Gateway) HTTPRouter {

// Register register a handler function.
func (router *httpRouter) Handle(method, prefix string, handler http.Handler) (err error) {
defer func() {
recoveredValue := recover()
if recoveredValue != nil {
err = fmt.Errorf("caught error when registering %s %s: %+v", method, prefix, recoveredValue)
}
}()

h := func(w http.ResponseWriter, r *http.Request) {
reqUUID := r.Header.Get(router.requestUUIDHeaderKey)
if reqUUID == "" {
Expand All @@ -197,8 +189,7 @@ func (router *httpRouter) Handle(method, prefix string, handler http.Handler) (e
handler.ServeHTTP(w, r)
}

router.httpRouter.Handler(method, prefix, http.HandlerFunc(h))
return err
return router.httpRouter.Handle(method, prefix, http.HandlerFunc(h))
}

// ServeHTTP implements the http.Handle as a convenience to allow HTTPRouter to be invoked by the standard library HTTP server.
Expand Down
155 changes: 155 additions & 0 deletions runtime/router/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package router

import (
"context"
"net/http"
"sort"
"strings"
)

// Router dispatches http requests to a registered http.Handler.
// It implements a similar interface to the one in github.com/julienschmidt/httprouter,
// the main differences are:
// 1. this router does not treat "/a/:b" and "/a/b/c" as conflicts (https://github.com/julienschmidt/httprouter/issues/175)
// 2. this router does not treat "/a/:b" and "/a/:c" as different routes and therefore does not allow them to be registered at the same time (https://github.com/julienschmidt/httprouter/issues/6)
// 3. this router does not treat "/a" and "/a/" as different routes
// Also the `*` pattern is greedy, if a handler is register for `/a/*`, then no handler
// can be further registered for any path that starts with `/a/`
type Router struct {
tries map[string]*Trie

// If enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool

// Configurable http.Handler which is called when a request
// cannot be routed and HandleMethodNotAllowed is true.
// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
// The "Allow" header with allowed request methods is set before the handler
// is called.
MethodNotAllowed http.Handler

// Configurable http.Handler which is called when no matching route is
// found. If it is not set, http.NotFound is used.
NotFound http.Handler

// Function to handle panics recovered from http handlers.
// It should be used to generate a error page and return the http error code
// 500 (Internal Server Error).
// The handler can be used to keep your server from crashing because of
// unrecovered panics.
PanicHandler func(http.ResponseWriter, *http.Request, interface{})

// TODO: (clu) maybe support OPTIONS
}

type paramsKey string

// urlParamsKey is the request context key under which URL params are stored.
const urlParamsKey = paramsKey("urlParamsKey")

// ParamsFromContext pulls the URL parameters from a request context,
// or returns nil if none are present.
func ParamsFromContext(ctx context.Context) []Param {
p, _ := ctx.Value(urlParamsKey).([]Param)
return p
}

// Handle registers a http.Handler for given method and path.
func (r *Router) Handle(method, path string, handler http.Handler) error {
if r.tries == nil {
r.tries = make(map[string]*Trie)
}

trie, ok := r.tries[method]
if !ok {
trie = NewTrie()
r.tries[method] = trie
}
return trie.Set(path, handler)
}

// ServeHTTP dispatches the request to a register handler to handle.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if r.PanicHandler != nil {
defer func(w http.ResponseWriter, req *http.Request) {
if recovered := recover(); recovered != nil {
r.PanicHandler(w, req, recovered)
}
}(w, req)
}

reqPath := req.URL.Path
if trie, ok := r.tries[req.Method]; ok {
if handler, params, err := trie.Get(reqPath); err == nil {
ctx := context.WithValue(req.Context(), urlParamsKey, params)
req = req.WithContext(ctx)
handler.ServeHTTP(w, req)
return
}
}

if r.HandleMethodNotAllowed {
if allowed := r.allowed(reqPath, req.Method); allowed != "" {
w.Header().Set("Allow", allowed)
if r.MethodNotAllowed != nil {
r.MethodNotAllowed.ServeHTTP(w, req)
} else {
http.Error(w,
http.StatusText(http.StatusMethodNotAllowed),
http.StatusMethodNotAllowed,
)
}
return
}
}

if r.NotFound != nil {
r.NotFound.ServeHTTP(w, req)
} else {
http.NotFound(w, req)
}
}

func (r *Router) allowed(path, reqMethod string) string {
var allow []string

for method, trie := range r.tries {
if method == reqMethod || method == http.MethodOptions {
continue
}

if _, _, err := trie.Get(path); err == nil {
allow = append(allow, method)
}
}
sort.Slice(allow, func(i, j int) bool {
return allow[i] < allow[j]
})

return strings.Join(allow, ", ")
}
Loading

0 comments on commit 2ebd5a4

Please sign in to comment.