Skip to content

Commit 1615805

Browse files
committed
feat: add read and write timeout annotations and update fox to v0.26.0
1 parent 0ce7b50 commit 1615805

File tree

7 files changed

+164
-98
lines changed

7 files changed

+164
-98
lines changed

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ package main
3131

3232
import (
3333
"errors"
34+
"fmt"
3435
"log"
3536
"net/http"
3637
"time"
@@ -40,23 +41,22 @@ import (
4041
)
4142

4243
func main() {
43-
f, err := fox.New(
44+
f := fox.MustRouter(
4445
fox.DefaultOptions(),
4546
fox.WithMiddleware(
4647
foxtimeout.Middleware(2*time.Second),
4748
),
4849
)
49-
if err != nil {
50-
panic(err)
51-
}
5250

53-
f.MustHandle(http.MethodGet, "/hello/{name}", func(c fox.Context) {
54-
_ = c.String(http.StatusOK, "hello %s\n", c.Param("name"))
51+
f.MustAdd(fox.MethodGet, "/hello/{name}", func(c *fox.Context) {
52+
_ = c.String(http.StatusOK, fmt.Sprintf("Hello %s\n", c.Param("name")))
5553
})
56-
f.MustHandle(http.MethodGet, "/download/{filepath}", DownloadHandler, foxtimeout.None())
57-
f.MustHandle(http.MethodGet, "/workflow/{id}/start", WorkflowHandler, foxtimeout.After(15*time.Second))
54+
// Disable timeout the middleware for this route
55+
f.MustAdd(fox.MethodGet, "/download/{filepath}", DownloadHandler, foxtimeout.HandlerTimeout(foxtimeout.NoTimeout))
56+
// Use 15s timeout instead of the global 2s for this route
57+
f.MustAdd(fox.MethodGet, "/workflow/{id}/start", WorkflowHandler, foxtimeout.HandlerTimeout(15*time.Second))
5858

59-
if err = http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
59+
if err := http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
6060
log.Fatalln(err)
6161
}
6262
}

annotation.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,38 @@ import (
88

99
type key struct{}
1010

11-
var ctxKey key
11+
var (
12+
timeoutKey key
13+
readTimeoutKey key
14+
writeTimeoutKey key
15+
)
16+
17+
const NoTimeout = time.Duration(0)
1218

13-
// After returns a RouteOption that sets a custom timeout duration for a specific route.
19+
// HandlerTimeout returns a RouteOption that sets a custom timeout duration for a specific route.
1420
// This allows individual routes to have different timeout values than the global timeout.
15-
func After(dt time.Duration) fox.RouteOption {
16-
return fox.WithAnnotation(ctxKey, dt)
21+
// Passing a value <= 0 (or NoTimeout) disables the timeout for this route.
22+
func HandlerTimeout(dt time.Duration) fox.RouteOption {
23+
return fox.WithAnnotation(timeoutKey, dt)
24+
}
25+
26+
// ReadTimeout returns a RouteOption that sets the read deadline for the underlying connection.
27+
// This controls how long the server will wait for the client to send request data.
28+
// A zero duration is not allowed and will return an error during route registration.
29+
func ReadTimeout(dt time.Duration) fox.RouteOption {
30+
return fox.WithAnnotation(readTimeoutKey, dt)
1731
}
1832

19-
// None returns a RouteOption that disables the timeout for a specific route.
20-
// This is useful for long-running operations like file uploads or SSE endpoints.
21-
func None() fox.RouteOption {
22-
return fox.WithAnnotation(ctxKey, time.Duration(0))
33+
// WriteTimeout returns a RouteOption that sets the write deadline for the underlying connection.
34+
// This controls how long the server will wait before timing out writes to the client.
35+
// A zero duration is not allowed and will return an error during route registration.
36+
func WriteTimeout(dt time.Duration) fox.RouteOption {
37+
return fox.WithAnnotation(writeTimeoutKey, dt)
2338
}
2439

25-
func unwrapRouteTimeout(r *fox.Route) (time.Duration, bool) {
40+
func unwrapRouteTimeout(r *fox.Route, k key) (time.Duration, bool) {
2641
if r != nil {
27-
dt := r.Annotation(ctxKey)
42+
dt := r.Annotation(k)
2843
if dt != nil {
2944
return dt.(time.Duration), true
3045
}

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ toolchain go1.24.2
66

77
require (
88
github.com/stretchr/testify v1.11.1
9-
github.com/tigerwill90/fox v0.25.0
9+
github.com/tigerwill90/fox v0.26.0
1010
)
1111

1212
require (
1313
github.com/davecgh/go-spew v1.1.1 // indirect
1414
github.com/kr/text v0.2.0 // indirect
1515
github.com/pmezard/go-difflib v1.0.0 // indirect
16-
golang.org/x/net v0.46.0 // indirect
17-
golang.org/x/sys v0.38.0 // indirect
18-
golang.org/x/text v0.30.0 // indirect
16+
golang.org/x/net v0.48.0 // indirect
17+
golang.org/x/sys v0.39.0 // indirect
18+
golang.org/x/text v0.32.0 // indirect
1919
gopkg.in/yaml.v3 v3.0.1 // indirect
2020
)

go.sum

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
44
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
55
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
6-
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
7-
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
6+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
7+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
88
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
99
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
1010
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1111
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
12-
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
13-
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
12+
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
13+
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
1414
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
1515
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
16-
github.com/tigerwill90/fox v0.25.0 h1:NRCHxo7PA1Oh5jQoDhknA/FpCCqrADSievOpDmvtd68=
17-
github.com/tigerwill90/fox v0.25.0/go.mod h1:dak3QMoa50lO3sFjZ268HRMmF9DxxaXfYuqakO/+QZw=
18-
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
19-
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
20-
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
21-
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
22-
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
23-
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
16+
github.com/tigerwill90/fox v0.26.0 h1:DeyDt2sn4GBuBDFD3sTAMaoN1C6plLZ3eh60UEiIWP8=
17+
github.com/tigerwill90/fox v0.26.0/go.mod h1:zXZwe2JG+YYD1hkVnOtphURr/RArlnIgUH1f9WeP43w=
18+
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
19+
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
20+
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
21+
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
22+
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
23+
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
2424
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2525
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
2626
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

options.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import (
1111
)
1212

1313
type config struct {
14-
resp fox.HandlerFunc
15-
filters []Filter
16-
enableAbortRequestBody bool
14+
resp fox.HandlerFunc
15+
filters []Filter
1716
}
1817

1918
type Option interface {
2019
apply(*config)
2120
}
2221

23-
type Filter func(c fox.Context) (skip bool)
22+
type Filter func(c *fox.Context) (skip bool)
2423

2524
type optionFunc func(*config)
2625

@@ -56,15 +55,6 @@ func WithResponse(h fox.HandlerFunc) Option {
5655
}
5756

5857
// DefaultTimeoutResponse sends a default 503 Service Unavailable response.
59-
func DefaultTimeoutResponse(c fox.Context) {
58+
func DefaultTimeoutResponse(c *fox.Context) {
6059
http.Error(c.Writer(), http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
6160
}
62-
63-
// WithAbortRequestBody controls whether to set a read deadline on the request
64-
// when a timeout occurs. When enabled, subsequent reads from the request body
65-
// will immediately fail after a timeout.
66-
func WithAbortRequestBody(enable bool) Option {
67-
return optionFunc(func(c *config) {
68-
c.enableAbortRequestBody = enable
69-
})
70-
}

timeout.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ type Timeout struct {
4242
//
4343
// The timeout middleware supports the [http.Pusher] interface but does not support the [http.Hijacker] or [http.Flusher] interfaces.
4444
//
45-
// Individual routes can override the timeout duration using the [After] option or disable it entirely using [None]:
45+
// Individual routes can override the timeout duration using the [HandlerTimeout] option. It's also possible to set the read
46+
// and write deadline for individual route using the [ReadTimeout] and [WriteTimeout] option.
47+
// If dt <= 0 (or NoTimeout), this is a passthrough middleware but per-route options remain effective.
4648
func Middleware(dt time.Duration, opts ...Option) fox.MiddlewareFunc {
4749
return create(dt, opts...).run
4850
}
@@ -61,7 +63,7 @@ func create(dt time.Duration, opts ...Option) *Timeout {
6163

6264
// run is the internal handler that applies the timeout logic.
6365
func (t *Timeout) run(next fox.HandlerFunc) fox.HandlerFunc {
64-
return func(c fox.Context) {
66+
return func(c *fox.Context) {
6567

6668
for _, f := range t.cfg.filters {
6769
if f(c) {
@@ -70,6 +72,7 @@ func (t *Timeout) run(next fox.HandlerFunc) fox.HandlerFunc {
7072
}
7173
}
7274

75+
t.setDeadline(c)
7376
dt := t.resolveTimeout(c)
7477
if dt <= 0 {
7578
next(c)
@@ -130,21 +133,27 @@ func (t *Timeout) run(next fox.HandlerFunc) fox.HandlerFunc {
130133
default:
131134
tw.err = err
132135
}
133-
if t.cfg.enableAbortRequestBody {
134-
_ = w.SetReadDeadline(time.Now())
135-
}
136136
t.cfg.resp(c)
137137
}
138138
}
139139
}
140140

141-
func (t *Timeout) resolveTimeout(c fox.Context) time.Duration {
142-
if dt, ok := unwrapRouteTimeout(c.Route()); ok {
141+
func (t *Timeout) resolveTimeout(c *fox.Context) time.Duration {
142+
if dt, ok := unwrapRouteTimeout(c.Route(), timeoutKey); ok {
143143
return dt
144144
}
145145
return t.dt
146146
}
147147

148+
func (t *Timeout) setDeadline(c *fox.Context) {
149+
if dt, ok := unwrapRouteTimeout(c.Route(), readTimeoutKey); ok {
150+
_ = c.Writer().SetReadDeadline(time.Now().Add(dt))
151+
}
152+
if dt, ok := unwrapRouteTimeout(c.Route(), writeTimeoutKey); ok {
153+
_ = c.Writer().SetWriteDeadline(time.Now().Add(dt))
154+
}
155+
}
156+
148157
func checkWriteHeaderCode(code int) {
149158
if code < 100 || code > 999 {
150159
panic(fmt.Sprintf("invalid status code %d", code))

0 commit comments

Comments
 (0)