Skip to content

Commit 4e98910

Browse files
authored
🔥 feat: Add response time middleware (#3891)
* Fix responsetime lint warnings * Enforce response time header usage
1 parent cfa9882 commit 4e98910

File tree

7 files changed

+233
-0
lines changed

7 files changed

+233
-0
lines changed

.github/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ Here is a list of middleware that are included within the Fiber framework.
754754
| [recover](https://github.com/gofiber/fiber/tree/main/middleware/recover) | Recovers from panics anywhere in the stack chain and handles the control to the centralized ErrorHandler. |
755755
| [redirect](https://github.com/gofiber/fiber/tree/main/middleware/redirect) | Redirect middleware. |
756756
| [requestid](https://github.com/gofiber/fiber/tree/main/middleware/requestid) | Adds a request ID to every request. |
757+
| [responsetime](https://github.com/gofiber/fiber/tree/main/middleware/responsetime) | Measures request handling duration and writes it to a configurable response header. |
757758
| [rewrite](https://github.com/gofiber/fiber/tree/main/middleware/rewrite) | Rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. |
758759
| [session](https://github.com/gofiber/fiber/tree/main/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. |
759760
| [skip](https://github.com/gofiber/fiber/tree/main/middleware/skip) | Skip middleware that skips a wrapped handler if a predicate is true. |

constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ const (
280280
HeaderXPingback = "X-Pingback"
281281
HeaderXRequestID = "X-Request-ID"
282282
HeaderXRequestedWith = "X-Requested-With"
283+
HeaderXResponseTime = "X-Response-Time"
283284
HeaderXRobotsTag = "X-Robots-Tag"
284285
HeaderXUACompatible = "X-UA-Compatible"
285286
HeaderAccessControlAllowPrivateNetwork = "Access-Control-Allow-Private-Network"

docs/middleware/responsetime.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
id: responsetime
3+
---
4+
5+
# ResponseTime
6+
7+
Response time middleware for [Fiber](https://github.com/gofiber/fiber) that measures the time spent handling a request and exposes it via a response header.
8+
9+
## Signatures
10+
11+
```go
12+
func New(config ...Config) fiber.Handler
13+
```
14+
15+
## Examples
16+
17+
Import the package:
18+
19+
```go
20+
import (
21+
"github.com/gofiber/fiber/v3"
22+
"github.com/gofiber/fiber/v3/middleware/responsetime"
23+
)
24+
```
25+
26+
### Default config
27+
28+
```go
29+
app.Use(responsetime.New())
30+
```
31+
32+
### Custom header
33+
34+
```go
35+
app.Use(responsetime.New(responsetime.Config{
36+
Header: "X-Elapsed",
37+
}))
38+
```
39+
40+
### Skip logic
41+
42+
```go
43+
app.Use(responsetime.New(responsetime.Config{
44+
Next: func(c fiber.Ctx) bool {
45+
return c.Path() == "/healthz"
46+
},
47+
}))
48+
```
49+
50+
## Config
51+
52+
| Property | Type | Description | Default |
53+
| :------- | :--- | :---------- | :------ |
54+
| Next | `func(c fiber.Ctx) bool` | Defines a function to skip this middleware when it returns `true`. | `nil` |
55+
| Header | `string` | Header key used to store the measured response time. If left empty, the default header is used. | `"X-Response-Time"` |

docs/whats_new.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,12 @@ Cache keys are now redacted in logs and error messages by default, and a `Disabl
12441244
The deprecated `Store` and `Key` options have been removed in v3. Use `Storage` and `KeyGenerator` instead.
12451245
:::
12461246

1247+
### ResponseTime
1248+
1249+
A new response time middleware measures how long each request takes to process and adds the duration to the response headers.
1250+
By default it writes the elapsed time to `X-Response-Time`, and you can change the header name. A `Next` hook lets you skip
1251+
endpoints such as health checks.
1252+
12471253
### CORS
12481254

12491255
We've made some changes to the CORS middleware to improve its functionality and flexibility. Here's what's new:

middleware/responsetime/config.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package responsetime
2+
3+
import (
4+
"github.com/gofiber/fiber/v3"
5+
)
6+
7+
// Config defines the config for middleware.
8+
type Config struct {
9+
// Next defines a function to skip this middleware when returned true.
10+
//
11+
// Optional. Default: nil
12+
Next func(c fiber.Ctx) bool
13+
14+
// Header is the header key used to set the response time.
15+
//
16+
// Optional. Default: "X-Response-Time"
17+
Header string
18+
}
19+
20+
// ConfigDefault is the default config.
21+
var ConfigDefault = Config{
22+
Next: nil,
23+
Header: fiber.HeaderXResponseTime,
24+
}
25+
26+
// Helper function to set default values.
27+
func configDefault(config ...Config) Config {
28+
// Return default config if nothing provided
29+
if len(config) < 1 {
30+
return ConfigDefault
31+
}
32+
33+
// Override default config
34+
cfg := config[0]
35+
36+
// Set default values
37+
if cfg.Header == "" {
38+
cfg.Header = ConfigDefault.Header
39+
}
40+
41+
return cfg
42+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package responsetime
2+
3+
import (
4+
"time"
5+
6+
"github.com/gofiber/fiber/v3"
7+
)
8+
9+
// New creates a new middleware handler.
10+
func New(config ...Config) fiber.Handler {
11+
// Set default config
12+
cfg := configDefault(config...)
13+
14+
// Return new handler
15+
return func(c fiber.Ctx) error {
16+
// Don't execute middleware if Next returns true
17+
if cfg.Next != nil && cfg.Next(c) {
18+
return c.Next()
19+
}
20+
21+
start := time.Now()
22+
23+
err := c.Next()
24+
25+
c.Set(cfg.Header, time.Since(start).String())
26+
27+
return err
28+
}
29+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package responsetime
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
"time"
9+
10+
"github.com/gofiber/fiber/v3"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestResponseTimeMiddleware(t *testing.T) {
15+
t.Parallel()
16+
17+
boom := errors.New("boom")
18+
19+
tests := []struct {
20+
name string
21+
expectedStatus int
22+
useCustomErrorHandler bool
23+
returnError bool
24+
expectHeader bool
25+
skipWithNext bool
26+
}{
27+
{
28+
name: "sets duration header",
29+
expectedStatus: fiber.StatusOK,
30+
expectHeader: true,
31+
},
32+
{
33+
name: "skips when Next returns true",
34+
expectedStatus: fiber.StatusOK,
35+
expectHeader: false,
36+
skipWithNext: true,
37+
},
38+
{
39+
name: "propagates errors",
40+
expectedStatus: fiber.StatusTeapot,
41+
useCustomErrorHandler: true,
42+
returnError: true,
43+
expectHeader: true,
44+
},
45+
}
46+
47+
for _, tt := range tests {
48+
t.Run(tt.name, func(t *testing.T) {
49+
t.Parallel()
50+
51+
configs := []Config(nil)
52+
if tt.skipWithNext {
53+
configs = []Config{{
54+
Next: func(fiber.Ctx) bool {
55+
return true
56+
},
57+
}}
58+
}
59+
60+
appConfig := fiber.Config{}
61+
if tt.useCustomErrorHandler {
62+
appConfig.ErrorHandler = func(c fiber.Ctx, err error) error {
63+
t.Helper()
64+
require.ErrorIs(t, err, boom)
65+
66+
return c.Status(fiber.StatusTeapot).SendString(err.Error())
67+
}
68+
}
69+
70+
app := fiber.New(appConfig)
71+
app.Use(New(configs...))
72+
73+
app.Get("/", func(c fiber.Ctx) error {
74+
if tt.returnError {
75+
return boom
76+
}
77+
78+
return c.SendStatus(fiber.StatusOK)
79+
})
80+
81+
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
82+
83+
require.NoError(t, err)
84+
require.Equal(t, tt.expectedStatus, resp.StatusCode)
85+
86+
header := resp.Header.Get(fiber.HeaderXResponseTime)
87+
if tt.expectHeader {
88+
require.NotEmpty(t, header)
89+
90+
_, parseErr := time.ParseDuration(header)
91+
require.NoError(t, parseErr)
92+
93+
return
94+
}
95+
96+
require.Empty(t, header)
97+
})
98+
}
99+
}

0 commit comments

Comments
 (0)