Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add middleware support as discussed in #293 #294

Merged
merged 3 commits into from
Jan 16, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv
* [Registered URLs](#registered-urls)
* [Walking Routes](#walking-routes)
* [Graceful Shutdown](#graceful-shutdown)
* [Middleware](#middleware)
* [Full Example](#full-example)

---
Expand Down Expand Up @@ -447,6 +448,82 @@ func main() {
}
```

### Middleware

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Add a quick paragraph describing what middleware is / what it can be used for, and link to https://github.com/gorilla/handlers
  • Version will be v1.6.1 if we merge this week (I'll cut a new release after this PR is merged in)

e.g.

Middleware describes intermediate handlers that can check, mutate or log the incoming request or outgoing response. mux makes it easy to add middleware to a Router or Subrouter...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, I forgot to mention I did not add the link because most handler don't actually implement mux.Middleware, as they require additional parameters. We could circunvent this using closures, but I think it would confuse some users instead of adding clarity. Maybe we could rework some handlers to make them comply with the interface?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of changing them we could add a XXXXMiddleware function for each one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, of course. We should not break existing handlers, so maybe wrapping them somehow, or refactoring the core logic into a different, private function and exposing both the actual and the Middleware flavours of it. I can give it a shot if you think it's worth it.

Since **vX.Y.Z**, mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed if a
match is found (including subrouters). Middlewares are defined using the de facto standard type:

```go
type MiddlewareFunc func(http.Handler) http.Handler
```

Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another use of parens that could be avoided.


A very basic middleware which logs the URI of the request being handled could be written as:

```go
func simpleMw(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do stuff here
log.Println(r.RequestURI)
// Call the next handler, which can be another middleware in the chain, or the final handler.
next.ServeHTTP(w, r)
})
}
```

Middlewares can be added to a router using `Router.AddMiddlewareFunc()`:

```go
r := mux.NewRouter()
r.HandleFunc("/", handler)
r.AddMiddleware(simpleMw)
```

A more complex authentication middleware, which maps session token to users, could be written as:

```go
// Define our struct
type authenticationMiddleware struct {
tokenUsers map[string]string
}

// Initialize it somewhere
func (amw *authenticationMiddleware) Populate() {
amw.tokenUsers["00000000"] = "user0"
amw.tokenUsers["aaaaaaaa"] = "userA"
amw.tokenUsers["05f717e5"] = "randomUser"
amw.tokenUsers["deadbeef"] = "user0"
}

// Middleware function, which will be called for each request
func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Session-Token")

if user, found := amw.tokenUsers[token]; found {
// We found the token in our map
log.Printf("Authenticated user %s\n", user)
next.ServeHTTP(w, r)
} else {
http.Error(w, "Forbidden", 403)
}
})
}
```

```go
r := mux.NewRouter()
r.HandleFunc("/", handler)

amw := authenticationMiddleware{}
amw.Populate()

r.AddMiddlewareFunc(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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"This can be used to avoid passing the request on, but make sure to return a response to the client from within the middleware"

Note: a library of useful logging, CORS and proxy-related middleware can be found in the gorilla/handlers package

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, but reworded "make sure" to "should", as some particular scenarios may apply and it is not a mux requirement.


## Full Example

Here's a complete, runnable example of a small `mux` based server:
Expand Down
65 changes: 65 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,5 +238,70 @@ as well:
url, err := r.Get("article").URL("subdomain", "news",
"category", "technology",
"id", "42")

Since **vX.Y.Z**, mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed if a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to align with the README

(Note to self: use tooling to make this automatic...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the version thingie. I'm not sure if its an important enough information to be in the readme. Feel free to re-add it if you think it is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good move. I think it's fair to assume that the documentation reflects HEAD.

Copy link

@dgellow dgellow Jan 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi there. I can see that the online documentation has the *vX.Y.Z*. Also the link [Router](...) hasn't been converted to HTML.

screen shot 2018-01-18 at 11 58 40

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes, I totally missed that one :/
I can fix it and create a new PR if you want, but maybe you can edit it quicker @elithrar

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll take a look today. The website doesn’t do Markdown, which is a bit of a pain.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks guys :)

match is found (including subrouters). Middlewares are defined using the de facto standard type:

type MiddlewareFunc func(http.Handler) http.Handler

Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created).

A very basic middleware which logs the URI of the request being handled could be written as:

func simpleMw(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do stuff here
log.Println(r.RequestURI)
// Call the next handler, which can be another middleware in the chain, or the final handler.
next.ServeHTTP(w, r)
})
}

Middlewares can be added to a router using `Router.AddMiddlewareFunc()`:

r := mux.NewRouter()
r.HandleFunc("/", handler)
r.AddMiddleware(simpleMw)

A more complex authentication middleware, which maps session token to users, could be written as:

// Define our struct
type authenticationMiddleware struct {
tokenUsers map[string]string
}

// Initialize it somewhere
func (amw *authenticationMiddleware) Populate() {
amw.tokenUsers["00000000"] = "user0"
amw.tokenUsers["aaaaaaaa"] = "userA"
amw.tokenUsers["05f717e5"] = "randomUser"
amw.tokenUsers["deadbeef"] = "user0"
}

// Middleware function, which will be called for each request
func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Session-Token")

if user, found := amw.tokenUsers[token]; found {
// We found the token in our map
log.Printf("Authenticated user %s\n", user)
next.ServeHTTP(w, r)
} else {
http.Error(w, "Forbidden", 403)
}
})
}

r := mux.NewRouter()
r.HandleFunc("/", handler)

amw := authenticationMiddleware{}
amw.Populate()

r.AddMiddlewareFunc(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.

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

import "net/http"

// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler.
// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed
// to it, and then calls the handler passed as parameter to the MiddlewareFunc.
type MiddlewareFunc func(http.Handler) http.Handler

// middleware interface is anything which implements a MiddlewareFunc named Middleware.
type middleware interface {
Middleware(handler http.Handler) http.Handler
}

// MiddlewareFunc also implements the Middleware interface.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update this? As we don't export the Middleware interface (middleware) this may be confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the comment or the implementation?

The implementation needs to be there in order to store MiddlewareFunc as Middleware in the middlewares slice. But the comment needs to be updated indeed, I slipped there :P

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the comment 👌🏼

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler {
return mw(handler)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Middleware adds a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router.

// AddMiddlewareFunc appends a MiddlewareFunc to the chain.
func (r *Router) AddMiddlewareFunc(mwf MiddlewareFunc) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddMiddleware, Add or UseMiddleware or just Use might be less verbose here.

  • chi and echo call their methods Use
  • express also calls it use
  • Downside to Use - not entirely self-documenting.

I'd be OK with just calling this function Middleware as "Add" is obvious, and subtracting it/removing it later is (mostly) a non-option.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the AddMiddlewareFunc name either, seems way too long to me. But I think we should keep the "func" part somewhere, as it makes what this function expects very clear, and avoids the need for a breaking change if the middleware interface is exported in the future.

What about UseFunc?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s just stick with Use - as that’s familiar and aligns with the Handle (takes a http.Handler) and HandleFunc (takes a HandlerFunc) on Router. This somewhat sits in the middle, but familiarity wins out in lieu of other objections.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I've renamed the internal addMiddleware to useInterface, to avoid collision with Use.

r.middlewares = append(r.middlewares, mwf)
}

// addMiddleware appends a middleware to the chain.
func (r *Router) addMiddleware(mw middleware) {
r.middlewares = append(r.middlewares, mw)
}
Loading