Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: go-chi/chi
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.0.2
Choose a base ref
...
head repository: go-chi/chi
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v4.0.3
Choose a head ref

Commits on Feb 27, 2019

  1. Re-wrote the middleware compress library.

    This re-write introduces the concept of a Compressor which stores all the settings for how compression/encoding should happen. The old API was changed to use the new Compressor struct which should prevent this change from breaking current consumers.
    
    Additionally, this uses a sync.Pool for encoders that have a Reset(io.Writer) method to reduce memory overhead.
    awbraunstein authored and pkieltyka committed Feb 27, 2019
    Copy the full SHA
    52e06e6 View commit details
  2. Copy the full SHA
    7eb231e View commit details

Commits on Mar 5, 2019

  1. drop unused type (#406)

    vbauerster authored and VojtechVitek committed Mar 5, 2019
    Copy the full SHA
    a86787d View commit details

Commits on Mar 16, 2019

  1. README

    pkieltyka committed Mar 16, 2019
    Copy the full SHA
    d089166 View commit details

Commits on Apr 9, 2019

  1. Fixed a typo

    thimalw authored and pkieltyka committed Apr 9, 2019
    Copy the full SHA
    af3b6a3 View commit details

Commits on Apr 30, 2019

  1. Copy the full SHA
    988e36e View commit details

Commits on May 8, 2019

  1. Copy the full SHA
    08c92af View commit details

Commits on Jul 30, 2019

  1. provided ability to set request id header

    Richard Hayes committed Jul 30, 2019
    Copy the full SHA
    b50b484 View commit details

Commits on Aug 7, 2019

  1. documented Handlers map key

    tkivisik authored and pkieltyka committed Aug 7, 2019
    Copy the full SHA
    120ad4a View commit details
  2. Fixed encoding test

    len(encodings) will always be >= 0, which is probably just a typo.
    Set the header only when there really is an encoding setting present.
    muesli authored and pkieltyka committed Aug 7, 2019
    Copy the full SHA
    5f33708 View commit details
  3. Simplify code

    - We can access the pointer members directly
    - Easier to read concatenation
    muesli authored and pkieltyka committed Aug 7, 2019
    Copy the full SHA
    b83ac99 View commit details
  4. Check returned value in test

    Make sure s2 is empty as expected.
    muesli authored and pkieltyka committed Aug 7, 2019
    Copy the full SHA
    832c750 View commit details
  5. Simplify code

    - Removed unnecessary conversions
    - 0 is the default capacity, no need to specify it
    muesli authored and pkieltyka committed Aug 7, 2019
    Copy the full SHA
    ece538c View commit details
  6. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    bd842df View commit details
  7. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    4309749 View commit details

Commits on Aug 22, 2019

  1. fix typo of => or

    soupdiver authored and pkieltyka committed Aug 22, 2019
    Copy the full SHA
    5520733 View commit details
  2. Create FUNDING.yml

    pkieltyka authored Aug 22, 2019
    Copy the full SHA
    d1b5398 View commit details

Commits on Sep 2, 2019

  1. Copy the full SHA
    21abfca View commit details

Commits on Sep 6, 2019

  1. add go 1.13.x to travis

    pkieltyka committed Sep 6, 2019
    Copy the full SHA
    063783d View commit details

Commits on Oct 1, 2019

  1. Add CodeFund Sponsorship to README

    [CodeFund](https://codefund.io) provides ethical sponsorships to open source maintainers. This PR will place the "Sponsored by" image at the top of the README. The sponsoring companies are not paying per click nor impression. They are paying the maintainer(s) on a per-month basis to be the primary sponsor of this project.
    Eric Berry authored and pkieltyka committed Oct 1, 2019
    Copy the full SHA
    2171e2a View commit details

Commits on Oct 3, 2019

  1. Stop timer explicitly to reduce CPI usage and GC overhead

    David Poros authored and pkieltyka committed Oct 3, 2019
    Copy the full SHA
    feade56 View commit details
  2. Copy the full SHA
    906b567 View commit details

Commits on Oct 31, 2019

  1. Copy the full SHA
    221acf2 View commit details

Commits on Nov 30, 2019

  1. Update README.md

    TYPO: cancelations => cancellations
    fedir authored and pkieltyka committed Nov 30, 2019
    Copy the full SHA
    df7d757 View commit details

Commits on Dec 4, 2019

  1. README

    pkieltyka committed Dec 4, 2019
    Copy the full SHA
    73c7bc0 View commit details

Commits on Dec 10, 2019

  1. Copy the full SHA
    7fb3452 View commit details

Commits on Jan 9, 2020

  1. Make fileserver example a little more elaborate.

    While it's trivial to remove the basePath from the code, it wasn't quite obvious to me which paths I had to strip where to make the fileserver work if not mounted to /.
    dschmidt authored and pkieltyka committed Jan 9, 2020
    Copy the full SHA
    e363f43 View commit details
  2. Merge pull request #439 from justcompile/request-id-header

    middleware: RequestID - Define Header
    pkieltyka authored Jan 9, 2020
    Copy the full SHA
    6aa3c0e View commit details
  3. Merge pull request #449 from kowalczykp/err-abort-handler

    middleware: suppress http.ErrAbortHandler in recoverer
    pkieltyka authored Jan 9, 2020
    Copy the full SHA
    708d187 View commit details
  4. Copy the full SHA
    3658d98 View commit details
  5. Copy the full SHA
    2db6155 View commit details
12 changes: 12 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: #
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ go:
- 1.10.x
- 1.11.x
- 1.12.x
- 1.13.x

script:
- go get -d -t ./...
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## v4.0.3 (2020-01-09)

- core: fix regexp routing to include default value when param is not matched
- middleware: rewrite of middleware.Compress
- middleware: suppress http.ErrAbortHandler in middleware.Recoverer
- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3


## v4.0.2 (2019-02-26)

- Minor fixes
- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2


## v4.0.1 (2019-01-21)

- Fixes issue with compress middleware: #382 #385
- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1


## v4.0.0 (2019-01-10)

- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8
25 changes: 11 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ included some useful/optional subpackages: [middleware](/middleware), [render](h
* **Fast** - yes, see [benchmarks](#benchmarks)
* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http`
* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and subrouter mounting
* **Context control** - built on new `context` package, providing value chaining, cancelations and timeouts
* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts
* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91))
* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown
* **No external dependencies** - plain ol' Go stdlib + net/http
@@ -179,7 +179,7 @@ type Router interface {
http.Handler
Routes

// Use appends one of more middlewares onto the Router stack.
// Use appends one or more middlewares onto the Router stack.
Use(middlewares ...func(http.Handler) http.Handler)

// With adds inline middlewares for an endpoint handler.
@@ -412,18 +412,15 @@ We'll be more than happy to see [your contributions](./CONTRIBUTING.md)!
## Beyond REST

chi is just a http router that lets you decompose request handling into many smaller layers.
Many companies including Pressly.com (of course) use chi to write REST services for their public
APIs. But, REST is just a convention for managing state via HTTP, and there's a lot of other pieces
required to write a complete client-server system or network of microservices.

Looking ahead beyond REST, I also recommend some newer works in the field coming from
[gRPC](https://github.com/grpc/grpc-go), [NATS](https://nats.io), [go-kit](https://github.com/go-kit/kit)
and even [graphql](https://github.com/graphql-go/graphql). They're all pretty cool with their
own unique approaches and benefits. Specifically, I'd look at gRPC since it makes client-server
communication feel like a single program on a single computer, no need to hand-write a client library
and the request/response payloads are typed contracts. NATS is pretty amazing too as a super
fast and lightweight pub-sub transport that can speak protobufs, with nice service discovery -
an excellent combination with gRPC.
Many companies use chi to write REST services for their public APIs. But, REST is just a convention
for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server
system or network of microservices.

Looking beyond REST, I also recommend some newer works in the field:
* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen
* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs
* [graphql](https://github.com/99designs/gqlgen) - Declarative query language
* [NATS](https://nats.io) - lightweight pub-sub


## License
20 changes: 12 additions & 8 deletions _examples/fileserver/main.go
Original file line number Diff line number Diff line change
@@ -12,25 +12,29 @@ import (
func main() {
r := chi.NewRouter()

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})
basePath := "/sub"

r.Route(basePath, func(root chi.Router) {
root.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})

workDir, _ := os.Getwd()
filesDir := filepath.Join(workDir, "files")
FileServer(r, "/files", http.Dir(filesDir))
workDir, _ := os.Getwd()
filesDir := filepath.Join(workDir, "files")
FileServer(root, basePath, "/files", http.Dir(filesDir))
})

http.ListenAndServe(":3333", r)
}

// FileServer conveniently sets up a http.FileServer handler to serve
// static files from a http.FileSystem.
func FileServer(r chi.Router, path string, root http.FileSystem) {
func FileServer(r chi.Router, basePath string, path string, root http.FileSystem) {
if strings.ContainsAny(path, "{}*") {
panic("FileServer does not permit URL parameters.")
}

fs := http.StripPrefix(path, http.FileServer(root))
fs := http.StripPrefix(basePath+path, http.FileServer(root))

if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
2 changes: 0 additions & 2 deletions _examples/rest/main.go
Original file line number Diff line number Diff line change
@@ -378,8 +378,6 @@ func (rd *ArticleResponse) Render(w http.ResponseWriter, r *http.Request) error
return nil
}

type ArticleListResponse []*ArticleResponse

func NewArticleListResponse(articles []*Article) []render.Renderer {
list := []render.Renderer{}
for _, article := range articles {
2 changes: 1 addition & 1 deletion chi.go
Original file line number Diff line number Diff line change
@@ -68,7 +68,7 @@ type Router interface {
http.Handler
Routes

// Use appends one of more middlewares onto the Router stack.
// Use appends one or more middlewares onto the Router stack.
Use(middlewares ...func(http.Handler) http.Handler)

// With adds inline middlewares for an endpoint handler.
7 changes: 4 additions & 3 deletions context.go
Original file line number Diff line number Diff line change
@@ -99,7 +99,8 @@ func (x *Context) RoutePattern() string {
// RouteContext returns chi's routing Context object from a
// http.Request Context.
func RouteContext(ctx context.Context) *Context {
return ctx.Value(RouteCtxKey).(*Context)
val, _ := ctx.Value(RouteCtxKey).(*Context)
return val
}

// URLParam returns the url parameter from a http.Request object.
@@ -125,8 +126,8 @@ type RouteParams struct {

// Add will append a URL parameter to the end of the route param
func (s *RouteParams) Add(key, value string) {
(*s).Keys = append((*s).Keys, key)
(*s).Values = append((*s).Values, value)
s.Keys = append(s.Keys, key)
s.Values = append(s.Values, value)
}

// ServerBaseContext wraps an http.Handler to set the request context to the
310 changes: 218 additions & 92 deletions middleware/compress.go

Large diffs are not rendered by default.

226 changes: 226 additions & 0 deletions middleware/compress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package middleware

import (
"compress/flate"
"compress/gzip"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/go-chi/chi"
)

func testRequestWithAcceptedEncodings(t *testing.T, ts *httptest.Server, method, path string, encodings ...string) (*http.Response, string) {
req, err := http.NewRequest(method, ts.URL+path, nil)
if err != nil {
t.Fatal(err)
return nil, ""
}
if len(encodings) > 0 {
encodingsString := strings.Join(encodings, ",")
req.Header.Set("Accept-Encoding", encodingsString)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
return nil, ""
}

respBody := decodeResponseBody(t, resp)
defer resp.Body.Close()

return resp, respBody
}

func decodeResponseBody(t *testing.T, resp *http.Response) string {
var reader io.ReadCloser
switch resp.Header.Get("Content-Encoding") {
case "gzip":
var err error
reader, err = gzip.NewReader(resp.Body)
if err != nil {
t.Fatal(err)
}
case "deflate":
reader = flate.NewReader(resp.Body)
default:
reader = resp.Body
}
respBody, err := ioutil.ReadAll(reader)
if err != nil {
t.Fatal(err)
return ""
}
reader.Close()

return string(respBody)
}

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

r.Use(Compress(5, "text/html"))

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("textstring"))
})

ts := httptest.NewServer(r)
defer ts.Close()

tests := []struct {
name string
acceptedEncodings []string
expectedEncoding string
extraCode func()
}{
{
name: "no expected encodings",
acceptedEncodings: nil,
expectedEncoding: "",
},
{
name: "gzip is only encoding",
acceptedEncodings: []string{"gzip"},
expectedEncoding: "gzip",
},
{
name: "gzip is preferred over deflate",
acceptedEncodings: []string{"gzip", "deflate"},
expectedEncoding: "gzip",
},
{
name: "deflate is used",
acceptedEncodings: []string{"deflate"},
expectedEncoding: "deflate",
},
{
name: "deflate is preferred over gzip",
acceptedEncodings: []string{"gzip, deflate"},
expectedEncoding: "deflate",
extraCode: func() {
SetEncoder("deflate", encoderDeflate)
},
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
if tc.extraCode != nil {
tc.extraCode()
}
resp, respString := testRequestWithAcceptedEncodings(t, ts, "GET", "/", tc.acceptedEncodings...)
if respString != "textstring" {
t.Errorf("response text doesn't match; expected:%q, got:%q", "textstring", respString)
}
if got := resp.Header.Get("Content-Encoding"); got != tc.expectedEncoding {
t.Errorf("expected encoding %q but got %q", tc.expectedEncoding, got)
}

})

}
}

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

compressor := NewCompressor(5, "text/html", "text/css")
if len(compressor.encoders) != 0 || len(compressor.pooledEncoders) != 2 {
t.Errorf("gzip and deflate should be pooled")
}

compressor.SetEncoder("nop", func(w io.Writer, _ int) io.Writer {
return w
})

if len(compressor.encoders) != 1 {
t.Errorf("nop encoder should be stored in the encoders map")
}

r.Use(compressor.Handler())

r.Get("/gethtml", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("textstring"))
})

r.Get("/getcss", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("textstring"))
})

r.Get("/getplain", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("textstring"))
})

ts := httptest.NewServer(r)
defer ts.Close()

tests := []struct {
name string
path string
acceptedEncodings []string
expectedEncoding string
}{
{
name: "no expected encodings due to no accepted encodings",
path: "/gethtml",
acceptedEncodings: nil,
expectedEncoding: "",
},
{
name: "no expected encodings due to content type",
path: "/getplain",
acceptedEncodings: nil,
expectedEncoding: "",
},
{
name: "gzip is only encoding",
path: "/gethtml",
acceptedEncodings: []string{"gzip"},
expectedEncoding: "gzip",
},
{
name: "gzip is preferred over deflate",
path: "/getcss",
acceptedEncodings: []string{"gzip", "deflate"},
expectedEncoding: "gzip",
},
{
name: "deflate is used",
path: "/getcss",
acceptedEncodings: []string{"deflate"},
expectedEncoding: "deflate",
},
{

name: "nop is preferred",
path: "/getcss",
acceptedEncodings: []string{"nop, gzip, deflate"},
expectedEncoding: "nop",
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
resp, respString := testRequestWithAcceptedEncodings(t, ts, "GET", tc.path, tc.acceptedEncodings...)
if respString != "textstring" {
t.Errorf("response text doesn't match; expected:%q, got:%q", "textstring", respString)
}
if got := resp.Header.Get("Content-Encoding"); got != tc.expectedEncoding {
t.Errorf("expected encoding %q but got %q", tc.expectedEncoding, got)
}

})

}
}
3 changes: 3 additions & 0 deletions middleware/content_charset_test.go
Original file line number Diff line number Diff line change
@@ -105,6 +105,9 @@ func TestSplit(t *testing.T) {
if s1 != "type1" {
t.Errorf("Want \"type1\" got \"%s\"", s1)
}
if s2 != "" {
t.Errorf("Want empty string got \"%s\"", s2)
}
}

func TestContentEncoding(t *testing.T) {
3 changes: 3 additions & 0 deletions middleware/middleware_test.go
Original file line number Diff line number Diff line change
@@ -136,18 +136,21 @@ func testRequestNoRedirect(t *testing.T, ts *httptest.Server, method, path strin
}

func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("expecting no error")
}
}

func assertError(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Fatalf("expecting error")
}
}

func assertEqual(t *testing.T, a, b interface{}) {
t.Helper()
if !reflect.DeepEqual(a, b) {
t.Fatalf("expecting values to be equal but got: '%v' and '%v'", a, b)
}
2 changes: 1 addition & 1 deletion middleware/recoverer.go
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ import (
func Recoverer(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rvr := recover(); rvr != nil {
if rvr := recover(); rvr != nil && rvr != http.ErrAbortHandler {

logEntry := GetLogEntry(r)
if logEntry != nil {
6 changes: 5 additions & 1 deletion middleware/request_id.go
Original file line number Diff line number Diff line change
@@ -20,6 +20,10 @@ type ctxKeyRequestID int
// RequestIDKey is the key that holds the unique request ID in a request context.
const RequestIDKey ctxKeyRequestID = 0

// RequestIDHeader is the name of the HTTP Header which contains the request id.
// Exported so that it can be changed by developers
var RequestIDHeader = "X-Request-Id"

var prefix string
var reqid uint64

@@ -63,7 +67,7 @@ func init() {
func RequestID(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID := r.Header.Get("X-Request-Id")
requestID := r.Header.Get(RequestIDHeader)
if requestID == "" {
myid := atomic.AddUint64(&reqid, 1)
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
71 changes: 71 additions & 0 deletions middleware/request_id_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package middleware

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/go-chi/chi"
)

func maintainDefaultRequestId() func() {
original := RequestIDHeader

return func() {
RequestIDHeader = original
}
}

func TestRequestID(t *testing.T) {
tests := map[string]struct {
requestIDHeader string
request func() *http.Request
expectedResponse string
}{
"Retrieves Request Id from default header": {
"X-Request-Id",
func() *http.Request {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Add("X-Request-Id", "req-123456")

return req
},
"RequestID: req-123456",
},
"Retrieves Request Id from custom header": {
"X-Trace-Id",
func() *http.Request {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Add("X-Trace-Id", "trace:abc123")

return req
},
"RequestID: trace:abc123",
},
}

defer maintainDefaultRequestId()()

for _, test := range tests {
w := httptest.NewRecorder()

r := chi.NewRouter()

RequestIDHeader = test.requestIDHeader

r.Use(RequestID)

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
requestID := GetReqID(r.Context())
response := fmt.Sprintf("RequestID: %s", requestID)

w.Write([]byte(response))
})
r.ServeHTTP(w, test.request())

if w.Body.String() != test.expectedResponse {
t.Fatalf("RequestID was not the expected value")
}
}
}
85 changes: 44 additions & 41 deletions middleware/throttle.go
Original file line number Diff line number Diff line change
@@ -16,7 +16,9 @@ var (
)

// Throttle is a middleware that limits number of currently processed requests
// at a time.
// at a time across all users. Note: Throttle is not a rate-limiter per user,
// instead it just puts a ceiling on the number of currentl in-flight requests
// being processed from the point from where the Throttle middleware is mounted.
func Throttle(limit int) func(http.Handler) http.Handler {
return ThrottleBacklog(limit, 0, defaultBacklogTimeout)
}
@@ -47,55 +49,56 @@ func ThrottleBacklog(limit int, backlogLimit int, backlogTimeout time.Duration)
t.backlogTokens <- token{}
}

fn := func(h http.Handler) http.Handler {
t.h = h
return &t
}
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

select {

case <-ctx.Done():
http.Error(w, errContextCanceled, http.StatusServiceUnavailable)
return

case btok := <-t.backlogTokens:
timer := time.NewTimer(t.backlogTimeout)

return fn
defer func() {
t.backlogTokens <- btok
}()

select {
case <-timer.C:
http.Error(w, errTimedOut, http.StatusServiceUnavailable)
return
case <-ctx.Done():
timer.Stop()
http.Error(w, errContextCanceled, http.StatusServiceUnavailable)
return
case tok := <-t.tokens:
defer func() {
timer.Stop()
t.tokens <- tok
}()
next.ServeHTTP(w, r)
}
return

default:
http.Error(w, errCapacityExceeded, http.StatusServiceUnavailable)
return
}
}

return http.HandlerFunc(fn)
}
}

// token represents a request that is being processed.
type token struct{}

// throttler limits number of currently processed requests at a time.
type throttler struct {
h http.Handler
tokens chan token
backlogTokens chan token
backlogTimeout time.Duration
}

// ServeHTTP is the primary throttler request handler
func (t *throttler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-ctx.Done():
http.Error(w, errContextCanceled, http.StatusServiceUnavailable)
return
case btok := <-t.backlogTokens:
timer := time.NewTimer(t.backlogTimeout)

defer func() {
t.backlogTokens <- btok
}()

select {
case <-timer.C:
http.Error(w, errTimedOut, http.StatusServiceUnavailable)
return
case <-ctx.Done():
http.Error(w, errContextCanceled, http.StatusServiceUnavailable)
return
case tok := <-t.tokens:
defer func() {
t.tokens <- tok
}()
t.h.ServeHTTP(w, r)
}
return
default:
http.Error(w, errCapacityExceeded, http.StatusServiceUnavailable)
return
}
}
4 changes: 2 additions & 2 deletions middleware/wrap_writer.go
Original file line number Diff line number Diff line change
@@ -70,12 +70,12 @@ func (b *basicWriter) WriteHeader(code int) {
if !b.wroteHeader {
b.code = code
b.wroteHeader = true
b.ResponseWriter.WriteHeader(code)
}
b.ResponseWriter.WriteHeader(code)
}

func (b *basicWriter) Write(buf []byte) (int, error) {
b.WriteHeader(http.StatusOK)
b.maybeWriteHeader()
n, err := b.ResponseWriter.Write(buf)
if b.tee != nil {
_, err2 := b.tee.Write(buf[:n])
38 changes: 35 additions & 3 deletions mux_test.go
Original file line number Diff line number Diff line change
@@ -1088,7 +1088,7 @@ func TestMuxSubroutes(t *testing.T) {
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

body = string(w.Body.Bytes())
body = w.Body.String()
expected = "account2"
if body != expected {
t.Fatalf("expected:%s got:%s", expected, body)
@@ -1127,7 +1127,7 @@ func TestSingleHandler(t *testing.T) {
w := httptest.NewRecorder()
h.ServeHTTP(w, r)

body := string(w.Body.Bytes())
body := w.Body.String()
expected := "hi joe"
if body != expected {
t.Fatalf("expected:%s got:%s", expected, body)
@@ -1401,6 +1401,38 @@ func TestMuxWildcardRouteCheckTwo(t *testing.T) {
r.Get("/*/wildcard/{must}/be/at/end", handler)
}

func TestMuxRegexp(t *testing.T) {
r := NewRouter()
r.Route("/{param:[0-9]+}/test", func(r Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fmt.Sprintf("Hi: %s", URLParam(r, "param"))))
})
})

ts := httptest.NewServer(r)
defer ts.Close()

if _, body := testRequest(t, ts, "GET", "//test", nil); body != "Hi: " {
t.Fatalf(body)
}
}

func TestMuxRegexp2(t *testing.T) {
r := NewRouter()
r.Get("/foo-{suffix:[a-z]{2,3}}.json", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(URLParam(r, "suffix")))
})
ts := httptest.NewServer(r)
defer ts.Close()

if _, body := testRequest(t, ts, "GET", "/foo-.json", nil); body != "" {
t.Fatalf(body)
}
if _, body := testRequest(t, ts, "GET", "/foo-abc.json", nil); body != "abc" {
t.Fatalf(body)
}
}

func TestMuxContextIsThreadSafe(t *testing.T) {
router := NewRouter()
router.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
@@ -1564,7 +1596,7 @@ func testHandler(t *testing.T, h http.Handler, method, path string, body io.Read
r, _ := http.NewRequest(method, path, body)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
return w.Result(), string(w.Body.Bytes())
return w.Result(), w.Body.String()
}

type testFileSystem struct {
20 changes: 14 additions & 6 deletions tree.go
Original file line number Diff line number Diff line change
@@ -331,7 +331,7 @@ func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node {
func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) {
// Set the handler for the method type on the node
if n.endpoints == nil {
n.endpoints = make(endpoints, 0)
n.endpoints = make(endpoints)
}

paramKeys := patParamKeys(pattern)
@@ -417,6 +417,8 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
continue
}

found := false

// serially loop through each node grouped by the tail delimiter
for idx := 0; idx < len(nds); idx++ {
xn = nds[idx]
@@ -433,7 +435,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
}

if ntyp == ntRegexp && xn.rex != nil {
if xn.rex.Match([]byte(xsearch[:p])) == false {
if !xn.rex.Match([]byte(xsearch[:p])) {
continue
}
} else if strings.IndexByte(xsearch[:p], '/') != -1 {
@@ -443,9 +445,14 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {

rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p])
xsearch = xsearch[p:]
found = true
break
}

if !found {
rctx.routeParams.Values = append(rctx.routeParams.Values, "")
}

default:
// catch-all nodes
rctx.routeParams.Values = append(rctx.routeParams.Values, search)
@@ -460,7 +467,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
// did we find it yet?
if len(xsearch) == 0 {
if xn.isLeaf() {
h, _ := xn.endpoints[method]
h := xn.endpoints[method]
if h != nil && h.handler != nil {
rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...)
return xn
@@ -582,7 +589,7 @@ func (n *node) routes() []Route {
}

// Group methodHandlers by unique patterns
pats := make(map[string]endpoints, 0)
pats := make(map[string]endpoints)

for mt, h := range eps {
if h.pattern == "" {
@@ -597,7 +604,7 @@ func (n *node) routes() []Route {
}

for p, mh := range pats {
hs := make(map[string]http.Handler, 0)
hs := make(map[string]http.Handler)
if mh[mALL] != nil && mh[mALL].handler != nil {
hs["*"] = mh[mALL].handler
}
@@ -698,7 +705,7 @@ func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) {
rexpat = "^" + rexpat
}
if rexpat[len(rexpat)-1] != '$' {
rexpat = rexpat + "$"
rexpat += "$"
}
}

@@ -795,6 +802,7 @@ func (ns nodes) findEdge(label byte) *node {
}

// Route describes the details of a routing handler.
// Handlers map key is an HTTP method
type Route struct {
Pattern string
Handlers map[string]http.Handler