13 Commits

Author SHA1 Message Date
Matt Silverlock
e67b3c02c7 Remove TravisCI badge (#503) 2019-07-20 13:14:35 -07:00
Franklin Harding
7a1bf406d6 [docs] Add documentation for using mux to serve a SPA (#493)
* Add documentation for using mux to serve a SPA

* r -> router to prevent shadowing

* Expand SPA acronym

* BrowserRouter link

* Add more comments to explain how the spaHandler.ServeHTTP method works
2019-07-20 07:53:35 -07:00
Christian Muehlhaeuser
eab9c4f3d2 Simplify code (#501)
Use a single append call instead of a ranged for loop.
2019-07-20 07:49:38 -07:00
Christian Muehlhaeuser
50fbc3e7fb Avoid unnecessary conversion (#502)
No need to convert here.
2019-07-20 07:48:32 -07:00
Matt Silverlock
d83b6ffe49 Update config.yml (#495)
* Update config.yml

* Update config.yml
2019-07-01 13:26:33 -07:00
Matt Silverlock
00bdffe0f3 Update stale.yml (#494) 2019-06-29 21:17:52 -07:00
Franklin Harding
0534769016 Improve CORS Method Middleware (#477)
* More sensical CORSMethodMiddleware

* Only sets Access-Control-Allow-Methods on valid preflight requests
* Does not return after setting the Access-Control-Allow-Methods header
* Does not append OPTIONS header to Access-Control-Allow-Methods
regardless of whether there is an OPTIONS method matcher
* Adds tests for the listed behavior

* Add example for CORSMethodMiddleware

* Do not check for preflight and add documentation to the README

* Use http.MethodOptions instead of "OPTIONS"

* Add link to CORSMethodMiddleware section to readme

* Add test for unmatching route methods

* Rename CORS Method Middleware to Handling CORS Requests in README

* Link CORSMethodMiddleware in README to godoc

* Break CORSMethodMiddleware doc into bullets for readability

* Add comment about specifying OPTIONS to example in README for CORSMethodMiddleware

* Document cURL command used for testing CORS Method Middleware

* Update comment in example to "Handle the request"

* Add explicit comment about OPTIONS matchers to CORSMethodMiddleware doc

* Update circleci config to only check gofmt diff on latest go version

* Break up gofmt and go vet checks into separate steps.

* Use canonical circleci config
2019-06-29 13:52:29 -07:00
Matt Silverlock
d70f7b4baa Delete ISSUE_TEMPLATE.md (#492) 2019-06-29 11:01:59 -07:00
Franklin Harding
48f941fa99 Use subtests for middleware tests (#478)
* Use subtests for middleware tests
* Don't use subtests for MiddlewareAdd
2019-06-29 10:24:12 -07:00
Matt Silverlock
64954673e9 Delete .travis.yml (#490) 2019-06-28 16:07:30 -07:00
Franklin Harding
4248f5cd87 Fix nil panic in authentication middleware example (#489) 2019-06-28 08:33:07 -07:00
Matt Silverlock
212aa90d7c [WIP] Create CircleCI config (#484)
* [ci] Create CircleCI config
* Fix typos in container versions
* Add CircleCI badge
2019-06-24 09:05:39 -07:00
M@
ed099d4238 host:port matching does not require a :port to be specified.
In lieu of checking the template pattern on every Match request, a bool is added to the routeRegexp, and set
if the routeRegexp is a host AND there is no ":" in the template. I dislike extending the type, but I'd dislike
doing a string match on every single Match, even more.
2019-05-16 17:20:44 -07:00
13 changed files with 656 additions and 279 deletions

87
.circleci/config.yml Normal file
View File

@@ -0,0 +1,87 @@
version: 2.0
jobs:
# Base test configuration for Go library tests Each distinct version should
# inherit this base, and override (at least) the container image used.
"test": &test
docker:
- image: circleci/golang:latest
working_directory: /go/src/github.com/gorilla/mux
steps: &steps
# Our build steps: we checkout the repo, fetch our deps, lint, and finally
# run "go test" on the package.
- checkout
# Logs the version in our build logs, for posterity
- run: go version
- run:
name: "Fetch dependencies"
command: >
go get -t -v ./...
# Only run gofmt, vet & lint against the latest Go version
- run:
name: "Run golint"
command: >
if [ "${LATEST}" = true ] && [ -z "${SKIP_GOLINT}" ]; then
go get -u golang.org/x/lint/golint
golint ./...
fi
- run:
name: "Run gofmt"
command: >
if [[ "${LATEST}" = true ]]; then
diff -u <(echo -n) <(gofmt -d -e .)
fi
- run:
name: "Run go vet"
command: >
if [[ "${LATEST}" = true ]]; then
go vet -v ./...
fi
- run: go test -v -race ./...
"latest":
<<: *test
environment:
LATEST: true
"1.12":
<<: *test
docker:
- image: circleci/golang:1.12
"1.11":
<<: *test
docker:
- image: circleci/golang:1.11
"1.10":
<<: *test
docker:
- image: circleci/golang:1.10
"1.9":
<<: *test
docker:
- image: circleci/golang:1.9
"1.8":
<<: *test
docker:
- image: circleci/golang:1.8
"1.7":
<<: *test
docker:
- image: circleci/golang:1.7
workflows:
version: 2
build:
jobs:
- "latest"
- "1.12"
- "1.11"
- "1.10"
- "1.9"
- "1.8"
- "1.7"

10
.github/stale.yml vendored
View File

@@ -1,10 +1,10 @@
daysUntilStale: 60
daysUntilClose: 7
daysUntilStale: 75
daysUntilClose: 14
# Issues with these labels will never be considered stale
exemptLabels:
- v2
- needs-review
- work-required
- proposal
- needs review
- build system
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it hasn't seen

View File

@@ -1,24 +0,0 @@
language: go
matrix:
include:
- go: 1.7.x
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x
- go: 1.11.x
- go: 1.x
env: LATEST=true
- go: tip
allow_failures:
- go: tip
install:
- # Skip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- if [[ "$LATEST" = true ]]; then go vet .; fi
- go test -v -race ./...

View File

@@ -1,11 +0,0 @@
**What version of Go are you running?** (Paste the output of `go version`)
**What version of gorilla/mux are you at?** (Paste the output of `git rev-parse HEAD` inside `$GOPATH/src/github.com/gorilla/mux`)
**Describe your problem** (and what you have tried so far)
**Paste a minimal, runnable, reproduction of your issue below** (use backticks to format it)

158
README.md
View File

@@ -1,7 +1,7 @@
# gorilla/mux
[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux)
[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux)
[![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux)
[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge)
![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png)
@@ -25,10 +25,12 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv
* [Examples](#examples)
* [Matching Routes](#matching-routes)
* [Static Files](#static-files)
* [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.)
* [Registered URLs](#registered-urls)
* [Walking Routes](#walking-routes)
* [Graceful Shutdown](#graceful-shutdown)
* [Middleware](#middleware)
* [Handling CORS Requests](#handling-cors-requests)
* [Testing Handlers](#testing-handlers)
* [Full Example](#full-example)
@@ -210,6 +212,93 @@ func main() {
}
```
### Serving Single Page Applications
Most of the time it makes sense to serve your SPA on a separate web server from your API,
but sometimes it's desirable to serve them both from one place. It's possible to write a simple
handler for serving your SPA (for use with React Router's [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) for example), and leverage
mux's powerful routing for your API endpoints.
```go
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"path/filepath"
"time"
"github.com/gorilla/mux"
)
// spaHandler implements the http.Handler interface, so we can use it
// to respond to HTTP requests. The path to the static directory and
// path to the index file within that static directory are used to
// serve the SPA in the given static directory.
type spaHandler struct {
staticPath string
indexPath string
}
// ServeHTTP inspects the URL path to locate a file within the static dir
// on the SPA handler. If a file is found, it will be served. If not, the
// file located at the index path on the SPA handler will be served. This
// is suitable behavior for serving an SPA (single page application).
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// get the absolute path to prevent directory traversal
path, err := filepath.Abs(r.URL.Path)
if err != nil {
// if we failed to get the absolute path respond with a 400 bad request
// and stop
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// prepend the path with the path to the static directory
path = filepath.Join(h.staticPath, path)
// check whether a file exists at the given path
_, err = os.Stat(path)
if os.IsNotExist(err) {
// file does not exist, serve index.html
http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))
return
} else if err != nil {
// if we got an error (that wasn't that the file doesn't exist) stating the
// file, return a 500 internal server error and stop
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// otherwise, use http.FileServer to serve the static dir
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
// an example API handler
json.NewEncoder(w).Encode(map[string]bool{"ok": true})
})
spa := spaHandler{staticPath: "build", indexPath: "index.html"}
router.PathPrefix("/").Handler(spa)
srv := &http.Server{
Handler: router,
Addr: "127.0.0.1:8000",
// Good practice: enforce timeouts for servers you create!
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
```
### Registered URLs
Now let's see how to build registered URLs.
@@ -491,6 +580,73 @@ r.Use(amw.Middleware)
Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it.
### Handling CORS Requests
[CORSMethodMiddleware](https://godoc.org/github.com/gorilla/mux#CORSMethodMiddleware) intends to make it easier to strictly set the `Access-Control-Allow-Methods` response header.
* You will still need to use your own CORS handler to set the other CORS headers such as `Access-Control-Allow-Origin`
* The middleware will set the `Access-Control-Allow-Methods` header to all the method matchers (e.g. `r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)` -> `Access-Control-Allow-Methods: GET,PUT,OPTIONS`) on a route
* If you do not specify any methods, then:
> _Important_: there must be an `OPTIONS` method matcher for the middleware to set the headers.
Here is an example of using `CORSMethodMiddleware` along with a custom `OPTIONS` handler to set all the required CORS headers:
```go
package main
import (
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
// IMPORTANT: you must specify an OPTIONS method matcher for the middleware to set CORS headers
r.HandleFunc("/foo", fooHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodOptions)
r.Use(mux.CORSMethodMiddleware(r))
http.ListenAndServe(":8080", r)
}
func fooHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
if r.Method == http.MethodOptions {
return
}
w.Write([]byte("foo"))
}
```
And an request to `/foo` using something like:
```bash
curl localhost:8080/foo -v
```
Would look like:
```bash
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /foo HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.59.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Access-Control-Allow-Methods: GET,PUT,PATCH,OPTIONS
< Access-Control-Allow-Origin: *
< Date: Fri, 28 Jun 2019 20:13:30 GMT
< Content-Length: 3
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
foo
```
### Testing Handlers
Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_.

2
doc.go
View File

@@ -295,7 +295,7 @@ A more complex authentication middleware, which maps session token to users, cou
r := mux.NewRouter()
r.HandleFunc("/", handler)
amw := authenticationMiddleware{}
amw := authenticationMiddleware{tokenUsers: make(map[string]string)}
amw.Populate()
r.Use(amw.Middleware)

View File

@@ -0,0 +1,37 @@
package mux_test
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/gorilla/mux"
)
func ExampleCORSMethodMiddleware() {
r := mux.NewRouter()
r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
// Handle the request
}).Methods(http.MethodGet, http.MethodPut, http.MethodPatch)
r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://example.com")
w.Header().Set("Access-Control-Max-Age", "86400")
}).Methods(http.MethodOptions)
r.Use(mux.CORSMethodMiddleware(r))
rw := httptest.NewRecorder()
req, _ := http.NewRequest("OPTIONS", "/foo", nil) // needs to be OPTIONS
req.Header.Set("Access-Control-Request-Method", "POST") // needs to be non-empty
req.Header.Set("Access-Control-Request-Headers", "Authorization") // needs to be non-empty
req.Header.Set("Origin", "http://example.com") // needs to be non-empty
r.ServeHTTP(rw, req)
fmt.Println(rw.Header().Get("Access-Control-Allow-Methods"))
fmt.Println(rw.Header().Get("Access-Control-Allow-Origin"))
// Output:
// GET,PUT,PATCH,OPTIONS
// http://example.com
}

2
go.mod
View File

@@ -1 +1,3 @@
module github.com/gorilla/mux
go 1.12

View File

@@ -32,37 +32,19 @@ func (r *Router) useInterface(mw middleware) {
r.middlewares = append(r.middlewares, mw)
}
// CORSMethodMiddleware sets the Access-Control-Allow-Methods response header
// on a request, by matching routes based only on paths. It also handles
// OPTIONS requests, by settings Access-Control-Allow-Methods, and then
// returning without calling the next http handler.
// CORSMethodMiddleware automatically sets the Access-Control-Allow-Methods response header
// on requests for routes that have an OPTIONS method matcher to all the method matchers on
// the route. Routes that do not explicitly handle OPTIONS requests will not be processed
// by the middleware. See examples for usage.
func CORSMethodMiddleware(r *Router) MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var allMethods []string
err := r.Walk(func(route *Route, _ *Router, _ []*Route) error {
for _, m := range route.matchers {
if _, ok := m.(*routeRegexp); ok {
if m.Match(req, &RouteMatch{}) {
methods, err := route.GetMethods()
if err != nil {
return err
}
allMethods = append(allMethods, methods...)
}
break
}
}
return nil
})
allMethods, err := getAllMethodsForRoute(r, req)
if err == nil {
w.Header().Set("Access-Control-Allow-Methods", strings.Join(append(allMethods, "OPTIONS"), ","))
if req.Method == "OPTIONS" {
return
for _, v := range allMethods {
if v == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", strings.Join(allMethods, ","))
}
}
}
@@ -70,3 +52,28 @@ func CORSMethodMiddleware(r *Router) MiddlewareFunc {
})
}
}
// getAllMethodsForRoute returns all the methods from method matchers matching a given
// request.
func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) {
var allMethods []string
err := r.Walk(func(route *Route, _ *Router, _ []*Route) error {
for _, m := range route.matchers {
if _, ok := m.(*routeRegexp); ok {
if m.Match(req, &RouteMatch{}) {
methods, err := route.GetMethods()
if err != nil {
return err
}
allMethods = append(allMethods, methods...)
}
break
}
}
return nil
})
return allMethods, err
}

View File

@@ -3,7 +3,6 @@ package mux
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
)
@@ -28,12 +27,12 @@ func TestMiddlewareAdd(t *testing.T) {
router.useInterface(mw)
if len(router.middlewares) != 1 || router.middlewares[0] != mw {
t.Fatal("Middleware was not added correctly")
t.Fatal("Middleware interface was not added correctly")
}
router.Use(mw.Middleware)
if len(router.middlewares) != 2 {
t.Fatal("MiddlewareFunc method was not added correctly")
t.Fatal("Middleware method was not added correctly")
}
banalMw := func(handler http.Handler) http.Handler {
@@ -41,7 +40,7 @@ func TestMiddlewareAdd(t *testing.T) {
}
router.Use(banalMw)
if len(router.middlewares) != 3 {
t.Fatal("MiddlewareFunc method was not added correctly")
t.Fatal("Middleware function was not added correctly")
}
}
@@ -55,34 +54,37 @@ func TestMiddleware(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/")
// Test regular middleware call
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
t.Run("regular middleware call", func(t *testing.T) {
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
})
// Middleware should not be called for 404
req = newRequest("GET", "/not/found")
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
t.Run("not called for 404", func(t *testing.T) {
req = newRequest("GET", "/not/found")
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
})
// Middleware should not be called if there is a method mismatch
req = newRequest("POST", "/")
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
// Add the middleware again as function
router.Use(mw.Middleware)
req = newRequest("GET", "/")
router.ServeHTTP(rw, req)
if mw.timesCalled != 3 {
t.Fatalf("Expected %d calls, but got only %d", 3, mw.timesCalled)
}
t.Run("not called for method mismatch", func(t *testing.T) {
req = newRequest("POST", "/")
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
})
t.Run("regular call using function middleware", func(t *testing.T) {
router.Use(mw.Middleware)
req = newRequest("GET", "/")
router.ServeHTTP(rw, req)
if mw.timesCalled != 3 {
t.Fatalf("Expected %d calls, but got only %d", 3, mw.timesCalled)
}
})
}
func TestMiddlewareSubrouter(t *testing.T) {
@@ -98,42 +100,56 @@ func TestMiddlewareSubrouter(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/")
router.ServeHTTP(rw, req)
if mw.timesCalled != 0 {
t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled)
}
t.Run("not called for route outside subrouter", func(t *testing.T) {
router.ServeHTTP(rw, req)
if mw.timesCalled != 0 {
t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled)
}
})
req = newRequest("GET", "/sub/")
router.ServeHTTP(rw, req)
if mw.timesCalled != 0 {
t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled)
}
t.Run("not called for subrouter root 404", func(t *testing.T) {
req = newRequest("GET", "/sub/")
router.ServeHTTP(rw, req)
if mw.timesCalled != 0 {
t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled)
}
})
req = newRequest("GET", "/sub/x")
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
t.Run("called once for route inside subrouter", func(t *testing.T) {
req = newRequest("GET", "/sub/x")
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
})
req = newRequest("GET", "/sub/not/found")
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
t.Run("not called for 404 inside subrouter", func(t *testing.T) {
req = newRequest("GET", "/sub/not/found")
router.ServeHTTP(rw, req)
if mw.timesCalled != 1 {
t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled)
}
})
router.useInterface(mw)
t.Run("middleware added to router", func(t *testing.T) {
router.useInterface(mw)
req = newRequest("GET", "/")
router.ServeHTTP(rw, req)
if mw.timesCalled != 2 {
t.Fatalf("Expected %d calls, but got only %d", 2, mw.timesCalled)
}
t.Run("called once for route outside subrouter", func(t *testing.T) {
req = newRequest("GET", "/")
router.ServeHTTP(rw, req)
if mw.timesCalled != 2 {
t.Fatalf("Expected %d calls, but got only %d", 2, mw.timesCalled)
}
})
req = newRequest("GET", "/sub/x")
router.ServeHTTP(rw, req)
if mw.timesCalled != 4 {
t.Fatalf("Expected %d calls, but got only %d", 4, mw.timesCalled)
}
t.Run("called twice for route inside subrouter", func(t *testing.T) {
req = newRequest("GET", "/sub/x")
router.ServeHTTP(rw, req)
if mw.timesCalled != 4 {
t.Fatalf("Expected %d calls, but got only %d", 4, mw.timesCalled)
}
})
})
}
func TestMiddlewareExecution(t *testing.T) {
@@ -145,30 +161,33 @@ func TestMiddlewareExecution(t *testing.T) {
w.Write(handlerStr)
})
rw := NewRecorder()
req := newRequest("GET", "/")
t.Run("responds normally without middleware", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/")
// Test handler-only call
router.ServeHTTP(rw, req)
router.ServeHTTP(rw, req)
if !bytes.Equal(rw.Body.Bytes(), handlerStr) {
t.Fatal("Handler response is not what it should be")
}
// Test middleware call
rw = NewRecorder()
router.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(mwStr)
h.ServeHTTP(w, r)
})
if !bytes.Equal(rw.Body.Bytes(), handlerStr) {
t.Fatal("Handler response is not what it should be")
}
})
router.ServeHTTP(rw, req)
if !bytes.Equal(rw.Body.Bytes(), append(mwStr, handlerStr...)) {
t.Fatal("Middleware + handler response is not what it should be")
}
t.Run("responds with handler and middleware response", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/")
router.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(mwStr)
h.ServeHTTP(w, r)
})
})
router.ServeHTTP(rw, req)
if !bytes.Equal(rw.Body.Bytes(), append(mwStr, handlerStr...)) {
t.Fatal("Middleware + handler response is not what it should be")
}
})
}
func TestMiddlewareNotFound(t *testing.T) {
@@ -187,26 +206,29 @@ func TestMiddlewareNotFound(t *testing.T) {
})
// Test not found call with default handler
rw := NewRecorder()
req := newRequest("GET", "/notfound")
t.Run("not called", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/notfound")
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a 404")
}
// Test not found call with custom handler
rw = NewRecorder()
req = newRequest("GET", "/notfound")
router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Custom 404 handler"))
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a 404")
}
})
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a custom 404")
}
t.Run("not called with custom not found handler", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/notfound")
router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Custom 404 handler"))
})
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a custom 404")
}
})
}
func TestMiddlewareMethodMismatch(t *testing.T) {
@@ -225,27 +247,29 @@ func TestMiddlewareMethodMismatch(t *testing.T) {
})
})
// Test method mismatch
rw := NewRecorder()
req := newRequest("POST", "/")
t.Run("not called", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("POST", "/")
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a method mismatch")
}
// Test not found call
rw = NewRecorder()
req = newRequest("POST", "/")
router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Method not allowed"))
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a method mismatch")
}
})
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a method mismatch")
}
t.Run("not called with custom method not allowed handler", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("POST", "/")
router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Method not allowed"))
})
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a method mismatch")
}
})
}
func TestMiddlewareNotFoundSubrouter(t *testing.T) {
@@ -269,27 +293,29 @@ func TestMiddlewareNotFoundSubrouter(t *testing.T) {
})
})
// Test not found call for default handler
rw := NewRecorder()
req := newRequest("GET", "/sub/notfound")
t.Run("not called", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/sub/notfound")
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a 404")
}
// Test not found call with custom handler
rw = NewRecorder()
req = newRequest("GET", "/sub/notfound")
subrouter.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Custom 404 handler"))
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a 404")
}
})
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a custom 404")
}
t.Run("not called with custom not found handler", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/sub/notfound")
subrouter.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Custom 404 handler"))
})
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a custom 404")
}
})
}
func TestMiddlewareMethodMismatchSubrouter(t *testing.T) {
@@ -313,66 +339,142 @@ func TestMiddlewareMethodMismatchSubrouter(t *testing.T) {
})
})
// Test method mismatch without custom handler
rw := NewRecorder()
req := newRequest("POST", "/sub/")
t.Run("not called", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("POST", "/sub/")
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a method mismatch")
}
// Test method mismatch with custom handler
rw = NewRecorder()
req = newRequest("POST", "/sub/")
router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Method not allowed"))
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a method mismatch")
}
})
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a method mismatch")
}
t.Run("not called with custom method not allowed handler", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("POST", "/sub/")
router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("Method not allowed"))
})
router.ServeHTTP(rw, req)
if bytes.Contains(rw.Body.Bytes(), mwStr) {
t.Fatal("Middleware was called for a method mismatch")
}
})
}
func TestCORSMethodMiddleware(t *testing.T) {
router := NewRouter()
cases := []struct {
path string
response string
method string
testURL string
expectedAllowedMethods string
testCases := []struct {
name string
registerRoutes func(r *Router)
requestHeader http.Header
requestMethod string
requestPath string
expectedAccessControlAllowMethodsHeader string
expectedResponse string
}{
{"/g/{o}", "a", "POST", "/g/asdf", "POST,PUT,GET,OPTIONS"},
{"/g/{o}", "b", "PUT", "/g/bla", "POST,PUT,GET,OPTIONS"},
{"/g/{o}", "c", "GET", "/g/orilla", "POST,PUT,GET,OPTIONS"},
{"/g", "d", "POST", "/g", "POST,OPTIONS"},
{
name: "does not set without OPTIONS matcher",
registerRoutes: func(r *Router) {
r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch)
},
requestMethod: "GET",
requestPath: "/foo",
expectedAccessControlAllowMethodsHeader: "",
expectedResponse: "a",
},
{
name: "sets on non OPTIONS",
registerRoutes: func(r *Router) {
r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch)
r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions)
},
requestMethod: "GET",
requestPath: "/foo",
expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS",
expectedResponse: "a",
},
{
name: "sets without preflight headers",
registerRoutes: func(r *Router) {
r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch)
r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions)
},
requestMethod: "OPTIONS",
requestPath: "/foo",
expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS",
expectedResponse: "b",
},
{
name: "does not set on error",
registerRoutes: func(r *Router) {
r.HandleFunc("/foo", stringHandler("a"))
},
requestMethod: "OPTIONS",
requestPath: "/foo",
expectedAccessControlAllowMethodsHeader: "",
expectedResponse: "a",
},
{
name: "sets header on valid preflight",
registerRoutes: func(r *Router) {
r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch)
r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions)
},
requestMethod: "OPTIONS",
requestPath: "/foo",
requestHeader: http.Header{
"Access-Control-Request-Method": []string{"GET"},
"Access-Control-Request-Headers": []string{"Authorization"},
"Origin": []string{"http://example.com"},
},
expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS",
expectedResponse: "b",
},
{
name: "does not set methods from unmatching routes",
registerRoutes: func(r *Router) {
r.HandleFunc("/foo", stringHandler("c")).Methods(http.MethodDelete)
r.HandleFunc("/foo/bar", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch)
r.HandleFunc("/foo/bar", stringHandler("b")).Methods(http.MethodOptions)
},
requestMethod: "OPTIONS",
requestPath: "/foo/bar",
requestHeader: http.Header{
"Access-Control-Request-Method": []string{"GET"},
"Access-Control-Request-Headers": []string{"Authorization"},
"Origin": []string{"http://example.com"},
},
expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS",
expectedResponse: "b",
},
}
for _, tt := range cases {
router.HandleFunc(tt.path, stringHandler(tt.response)).Methods(tt.method)
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
router := NewRouter()
router.Use(CORSMethodMiddleware(router))
tt.registerRoutes(router)
for _, tt := range cases {
rr := httptest.NewRecorder()
req := newRequest(tt.method, tt.testURL)
router.Use(CORSMethodMiddleware(router))
router.ServeHTTP(rr, req)
rw := NewRecorder()
req := newRequest(tt.requestMethod, tt.requestPath)
req.Header = tt.requestHeader
if rr.Body.String() != tt.response {
t.Errorf("Expected body '%s', found '%s'", tt.response, rr.Body.String())
}
router.ServeHTTP(rw, req)
allowedMethods := rr.Header().Get("Access-Control-Allow-Methods")
actualMethodsHeader := rw.Header().Get("Access-Control-Allow-Methods")
if actualMethodsHeader != tt.expectedAccessControlAllowMethodsHeader {
t.Fatalf("Expected Access-Control-Allow-Methods to equal %s but got %s", tt.expectedAccessControlAllowMethodsHeader, actualMethodsHeader)
}
if allowedMethods != tt.expectedAllowedMethods {
t.Errorf("Expected Access-Control-Allow-Methods '%s', found '%s'", tt.expectedAllowedMethods, allowedMethods)
}
actualResponse := rw.Body.String()
if actualResponse != tt.expectedResponse {
t.Fatalf("Expected response to equal %s but got %s", tt.expectedResponse, actualResponse)
}
})
}
}
@@ -411,27 +513,33 @@ func TestMiddlewareOnMultiSubrouter(t *testing.T) {
})
})
rw := NewRecorder()
req := newRequest("GET", "/first")
t.Run("/first uses first middleware", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/first")
router.ServeHTTP(rw, req)
if rw.Body.String() != first {
t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", first, rw.Body.String())
}
router.ServeHTTP(rw, req)
if rw.Body.String() != first {
t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", first, rw.Body.String())
}
})
rw = NewRecorder()
req = newRequest("GET", "/second")
t.Run("/second uses second middleware", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/second")
router.ServeHTTP(rw, req)
if rw.Body.String() != second {
t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", second, rw.Body.String())
}
router.ServeHTTP(rw, req)
if rw.Body.String() != second {
t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", second, rw.Body.String())
}
})
rw = NewRecorder()
req = newRequest("GET", "/second/not-exist")
t.Run("uses not found handler", func(t *testing.T) {
rw := NewRecorder()
req := newRequest("GET", "/second/not-exist")
router.ServeHTTP(rw, req)
if rw.Body.String() != notFound {
t.Fatalf("Notfound handler did not run: expected %s for not-exist, (got %s)", notFound, rw.Body.String())
}
router.ServeHTTP(rw, req)
if rw.Body.String() != notFound {
t.Fatalf("Notfound handler did not run: expected %s for not-exist, (got %s)", notFound, rw.Body.String())
}
})
}

6
mux.go
View File

@@ -111,10 +111,8 @@ func copyRouteConf(r routeConf) routeConf {
c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q))
}
c.matchers = make([]matcher, 0, len(r.matchers))
for _, m := range r.matchers {
c.matchers = append(c.matchers, m)
}
c.matchers = make([]matcher, len(r.matchers))
copy(c.matchers, r.matchers)
return c
}

View File

@@ -1943,7 +1943,7 @@ type TestA301ResponseWriter struct {
}
func (ho *TestA301ResponseWriter) Header() http.Header {
return http.Header(ho.hh)
return ho.hh
}
func (ho *TestA301ResponseWriter) Write(b []byte) (int, error) {

View File

@@ -113,6 +113,13 @@ func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*ro
if typ != regexpTypePrefix {
pattern.WriteByte('$')
}
var wildcardHostPort bool
if typ == regexpTypeHost {
if !strings.Contains(pattern.String(), ":") {
wildcardHostPort = true
}
}
reverse.WriteString(raw)
if endSlash {
reverse.WriteByte('/')
@@ -131,13 +138,14 @@ func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*ro
// Done!
return &routeRegexp{
template: template,
regexpType: typ,
options: options,
regexp: reg,
reverse: reverse.String(),
varsN: varsN,
varsR: varsR,
template: template,
regexpType: typ,
options: options,
regexp: reg,
reverse: reverse.String(),
varsN: varsN,
varsR: varsR,
wildcardHostPort: wildcardHostPort,
}, nil
}
@@ -158,11 +166,22 @@ type routeRegexp struct {
varsN []string
// Variable regexps (validators).
varsR []*regexp.Regexp
// Wildcard host-port (no strict port match in hostname)
wildcardHostPort bool
}
// Match matches the regexp against the URL host or path.
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
if r.regexpType != regexpTypeHost {
if r.regexpType == regexpTypeHost {
host := getHost(req)
if r.wildcardHostPort {
// Don't be strict on the port match
if i := strings.Index(host, ":"); i != -1 {
host = host[:i]
}
}
return r.regexp.MatchString(host)
} else {
if r.regexpType == regexpTypeQuery {
return r.matchQueryString(req)
}
@@ -172,8 +191,6 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
}
return r.regexp.MatchString(path)
}
return r.regexp.MatchString(getHost(req))
}
// url builds a URL part using the given values.