Skip to content

Commit

Permalink
Global middlewares (#345)
Browse files Browse the repository at this point in the history
* Replace WithCorsMiddleware by WithGlobalMiddleware (#325)

* refactor: move accept header form mux to server options

* replace withcoresmiddleware by globalmiddleware

* remove cors middle ware form server structure

* Update serve.go

* Update server.go

* Fixes tests and formatting

* Add more tests and documentation for global middlewares

* Update documentation/docs/guides/middlewares.md

Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com>

* Update documentation/docs/guides/middlewares.md

Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com>

* Update documentation/docs/guides/middlewares.md

Co-authored-by: Dylan Hitt <dylan.hitt1@gmail.com>

---------

Co-authored-by: Ekuma Matthew <79433184+ekumamatthew@users.noreply.github.com>
Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com>
Co-authored-by: Dylan Hitt <dylan.hitt1@gmail.com>
  • Loading branch information
4 people authored Jan 15, 2025
1 parent 7e11551 commit 6752f86
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 30 deletions.
81 changes: 64 additions & 17 deletions documentation/docs/guides/middlewares.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,43 @@ including the ones from `chi` and `gorilla` can be used with Fuego! :fire:
You can use them to add functionalities to your routes, such as logging,
authentication, etc.

## App-level middlewares
Middlewares can be registered at 2 levels:

You can add middlewares to the whole server using the `Use` method:
- **Route middlewares**: applies only to registered routes, is scoped to a specific group, or several routes.
- **Global middlewares**: applied on every request, even non-matching routes (useful for CORS for example).

## Route middlewares

You can add middlewares to a single route.
They are treated as an option to the route handler.
They will be added in the [`http.ServeMux`](https://pkg.go.dev/net/http#ServeMux) in the order they are declared, when registering the route.

```go title="main.go" showLineNumbers {13-14}
package main

import (
"github.com/go-fuego/fuego"
)

func main() {
s := fuego.NewServer()

// Declare the middlewares after the route handler
fuego.Get(s, "/", myController,
option.QueryInt("page", "The page number"),
option.Middleware(middleware1),
option.Middleware(middleware2, middleware3),
)

s.Run()
}
```

### Apply on group or server

To mimic the well-known `Use` method from [`chi`](https://pkg.go.dev/github.com/go-chi/chi/v5#Mux.Use) and [`Gin`](https://pkg.go.dev/github.com/gin-gonic/gin#Engine.Use), Fuego provides a `Use` method to add middlewares to a Group or Server. They are treated as an option to the server or group handler and will be applied to all routes.

But we recommend using the `option.Middleware` method for better readability.

```go title="main.go" showLineNumbers
package main
Expand Down Expand Up @@ -41,10 +75,6 @@ func main() {
}
```

## Group middlewares

You can also add middlewares to a group of routes using the `Group` method:

```go title="main.go" showLineNumbers
package main

Expand Down Expand Up @@ -79,29 +109,46 @@ func main() {
}
```

## Route middlewares
## Global Middlewares

You can also add middlewares to a single route.
They are treated as an option to the route handler.
Simply add the middlewares as the last arguments of the route handler:
Global middlewares are applied to every request, even if the route does not match.
They are useful for CORS, for example as CORS are using the OPTION method even if not registered.
They are registered in the Server `Handler`, not the `Mux`, and just before
`Run` is called (not at route registration).

```go title="main.go" showLineNumbers {13-14}
```go title="main.go" showLineNumbers
package main

import (
"net/http"

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

func main() {
s := fuego.NewServer()

// Declare the middlewares after the route handler
fuego.Get(s, "/", myController,
option.QueryInt("page", "The page number"),
option.Middleware(middleware1),
option.Middleware(middleware2, middleware3),
)
// Add a global middleware
fuego.WithGlobalMiddlewares(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Hello", "World")
// Do something before the request
next.ServeHTTP(w, r)
// Do something after the request
})
})

fuego.Get(s, "/my-route", myController)

// Here, the global middleware is applied
s.Run()
}
```

This will work even if the user requests a route that does not exist:

```bash
curl -v -X GET http://localhost:3000/unknown-route
```

We can see the `X-Hello: World` header in the response.
5 changes: 3 additions & 2 deletions serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ func (s *Server) setup() error {
s.printStartupMessage()

s.Server.Handler = s.Mux
if s.corsMiddleware != nil {
s.Server.Handler = s.corsMiddleware(s.Server.Handler)

for _, middleware := range s.globalMiddlewares {
s.Server.Handler = middleware(s.Server.Handler)
}

return nil
Expand Down
31 changes: 21 additions & 10 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,9 @@ type Server struct {
// [http.ServeMux.Handle] can also be used to register routes.
Mux *http.ServeMux

// Not stored with the other middlewares because it is a special case :
// it applies on routes that are not registered.
// For example, it allows OPTIONS /foo even if it is not declared (only GET /foo is declared).
corsMiddleware func(http.Handler) http.Handler
// globalMiddlewares is used to store the options
// that will be applied on ALL routes.
globalMiddlewares []func(http.Handler) http.Handler

*Engine

Expand Down Expand Up @@ -177,22 +176,34 @@ func WithTemplateFS(fs fs.FS) func(*Server) {
return func(c *Server) { c.fs = fs }
}

// WithCorsMiddleware registers a middleware to handle CORS.
// It is not handled like other middlewares with [Use] because it applies routes that are not registered.
// For example:
// WithGlobalMiddlewares adds middleware(s) that will be executed on ALL requests,
// even those that don't match any registered routes.
// Global Middlewares are mounted on the [http.Server] Handler, when executing [Server.Run].
// Route Middlewares are mounted directly on [http.ServeMux] added at route registration.
//
// For example, to add CORS middleware:
//
// import "github.com/rs/cors"
//
// s := fuego.NewServer(
// WithCorsMiddleware(cors.New(cors.Options{
// WithGlobalMiddlewares(cors.New(cors.Options{
// AllowedOrigins: []string{"*"},
// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
// AllowedHeaders: []string{"*"},
// AllowCredentials: true,
// }).Handler)
// }).Handler),
// )
func WithGlobalMiddlewares(middlewares ...func(http.Handler) http.Handler) func(*Server) {
return func(c *Server) {
c.globalMiddlewares = append(c.globalMiddlewares, middlewares...)
}
}

// WithCorsMiddleware adds CORS middleware to the server.
//
// Deprecated: Please use [WithGlobalMiddlewares] instead.
func WithCorsMiddleware(corsMiddleware func(http.Handler) http.Handler) func(*Server) {
return func(c *Server) { c.corsMiddleware = corsMiddleware }
return WithGlobalMiddlewares(corsMiddleware)
}

// WithGlobalResponseTypes adds default response types to the server.
Expand Down
79 changes: 79 additions & 0 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,3 +764,82 @@ func TestDefaultLoggingMiddleware(t *testing.T) {
})
}
}

func TestWithSeveralGlobalMiddelwares(t *testing.T) {
s := NewServer(
WithGlobalMiddlewares(
dummyMiddleware,
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Global-Middleware", "1")
if r.FormValue("one") == "true" {
w.Write([]byte("one"))
return
}
next.ServeHTTP(w, r)
})
},
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Global-Middleware", "2")
if r.FormValue("two") == "true" {
w.Write([]byte("two"))
return
}
next.ServeHTTP(w, r)
})
}),
)

Get(s, "/my-route", dummyController)

err := s.setup()
require.NoError(t, err)

t.Run("global middlewares work even on non-matching routes (returns 404)", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/non-existing-route", nil)
res := httptest.NewRecorder()

s.Handler.ServeHTTP(res, req)

t.Log(res.Body.String())
require.Equal(t, 404, res.Code)
require.Equal(t, "1", res.Header().Get("X-Global-Middleware"))
require.Equal(t, "response", res.Header().Get("X-Test-Response"))
})

t.Run("global middlewares work on matching routes", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/my-route", nil)
res := httptest.NewRecorder()

s.Handler.ServeHTTP(res, req)

t.Log(res.Body.String())
require.Equal(t, 200, res.Code)
require.Equal(t, "1", res.Header().Get("X-Global-Middleware"))
})

t.Run("stop the chain of middlewares and return 200 instead of expected 404 (non-matching route)", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/non-existing-route?one=true", nil)
res := httptest.NewRecorder()

s.Handler.ServeHTTP(res, req)

t.Log(res.Body.String())
require.Equal(t, 200, res.Code)
require.Equal(t, "1", res.Header().Get("X-Global-Middleware"))
require.Equal(t, "one", res.Body.String())
})

t.Run("pass the first global middleware, then stop the chain of middlewares returning 200 instead of returning 404 (non-matching route)", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/non-existing-route?two=true", nil)
res := httptest.NewRecorder()

s.Handler.ServeHTTP(res, req)

t.Log(res.Body.String())
require.Equal(t, 200, res.Code)
require.Equal(t, "2", res.Header().Get("X-Global-Middleware"))
require.Equal(t, "two", res.Body.String())
})
}
2 changes: 1 addition & 1 deletion testing-from-outside/cors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
func TestCors(t *testing.T) {
s := fuego.NewServer(
fuego.WithoutLogger(),
fuego.WithCorsMiddleware(cors.New(cors.Options{
fuego.WithGlobalMiddlewares(cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET"},
}).Handler),
Expand Down

0 comments on commit 6752f86

Please sign in to comment.