Skip to content

Commit d76c9c6

Browse files
authored
Merge pull request #152 from monzo/configure-server-timeouts
Add support for server timeouts
2 parents 6273fac + e4fe329 commit d76c9c6

File tree

8 files changed

+121
-21
lines changed

8 files changed

+121
-21
lines changed

e2e_http1_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import (
66
"fmt"
77
"testing"
88

9+
"github.com/monzo/terrors"
10+
"github.com/stretchr/testify/assert"
11+
912
"github.com/stretchr/testify/require"
1013
)
1114

1215
type http1Flavour struct {
1316
T *testing.T
1417
}
1518

16-
func (f http1Flavour) Serve(svc Service) *Server {
17-
s, err := Listen(svc, "localhost:0")
19+
func (f http1Flavour) Serve(svc Service, opts ...ServerOption) *Server {
20+
s, err := Listen(svc, "localhost:0", opts...)
1821
require.NoError(f.T, err)
1922
return s
2023
}
@@ -31,17 +34,22 @@ func (f http1Flavour) Context() (context.Context, func()) {
3134
return context.WithCancel(context.Background())
3235
}
3336

37+
func (f http1Flavour) AssertConnectionResetError(t *testing.T, terr *terrors.Error) {
38+
assert.Equal(t, terrors.ErrInternalService, terr.Code)
39+
assert.Equal(t, "EOF", terr.Message)
40+
}
41+
3442
type http1TLSFlavour struct {
3543
T *testing.T
3644
cert tls.Certificate
3745
}
3846

39-
func (f http1TLSFlavour) Serve(svc Service) *Server {
47+
func (f http1TLSFlavour) Serve(svc Service, opts ...ServerOption) *Server {
4048
l, err := tls.Listen("tcp", "localhost:0", &tls.Config{
4149
Certificates: []tls.Certificate{f.cert},
4250
ClientAuth: tls.NoClientCert})
4351
require.NoError(f.T, err)
44-
s, err := Serve(svc, l)
52+
s, err := Serve(svc, l, opts...)
4553
require.NoError(f.T, err)
4654
return s
4755
}
@@ -57,3 +65,8 @@ func (f http1TLSFlavour) Proto() string {
5765
func (f http1TLSFlavour) Context() (context.Context, func()) {
5866
return context.WithCancel(context.Background())
5967
}
68+
69+
func (f http1TLSFlavour) AssertConnectionResetError(t *testing.T, terr *terrors.Error) {
70+
assert.Equal(t, terrors.ErrInternalService, terr.Code)
71+
assert.Equal(t, "local error: tls: bad record MAC", terr.Message)
72+
}

e2e_http2_test.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"fmt"
77
"testing"
88

9+
"github.com/monzo/terrors"
10+
"github.com/stretchr/testify/assert"
11+
912
"github.com/stretchr/testify/require"
1013
)
1114

@@ -14,9 +17,9 @@ type http2H2cFlavour struct {
1417
client Service
1518
}
1619

17-
func (f http2H2cFlavour) Serve(svc Service) *Server {
20+
func (f http2H2cFlavour) Serve(svc Service, opts ...ServerOption) *Server {
1821
svc = svc.Filter(H2cFilter)
19-
s, err := Listen(svc, "localhost:0")
22+
s, err := Listen(svc, "localhost:0", opts...)
2023
require.NoError(f.T, err)
2124
return s
2225
}
@@ -33,14 +36,19 @@ func (f http2H2cFlavour) Context() (context.Context, func()) {
3336
return context.WithCancel(context.Background())
3437
}
3538

39+
func (f http2H2cFlavour) AssertConnectionResetError(t *testing.T, terr *terrors.Error) {
40+
assert.Equal(t, terrors.ErrInternalService, terr.Code)
41+
assert.Contains(t, terr.Message, "INTERNAL_ERROR")
42+
}
43+
3644
type http2H2cPriorKnowledgeFlavour struct {
3745
T *testing.T
3846
client Service
3947
}
4048

41-
func (f http2H2cPriorKnowledgeFlavour) Serve(svc Service) *Server {
49+
func (f http2H2cPriorKnowledgeFlavour) Serve(svc Service, opts ...ServerOption) *Server {
4250
svc = svc.Filter(H2cFilter)
43-
s, err := Listen(svc, "localhost:0")
51+
s, err := Listen(svc, "localhost:0", opts...)
4452
require.NoError(f.T, err)
4553
return s
4654
}
@@ -59,19 +67,24 @@ func (f http2H2cPriorKnowledgeFlavour) Context() (context.Context, func()) {
5967
return ctx, cancel
6068
}
6169

70+
func (f http2H2cPriorKnowledgeFlavour) AssertConnectionResetError(t *testing.T, terr *terrors.Error) {
71+
assert.Equal(t, terrors.ErrInternalService, terr.Code)
72+
assert.Equal(t, "EOF", terr.Message)
73+
}
74+
6275
type http2H2Flavour struct {
6376
T *testing.T
6477
client Service
6578
cert tls.Certificate
6679
}
6780

68-
func (f http2H2Flavour) Serve(svc Service) *Server {
81+
func (f http2H2Flavour) Serve(svc Service, opts ...ServerOption) *Server {
6982
l, err := tls.Listen("tcp", "localhost:0", &tls.Config{
7083
Certificates: []tls.Certificate{f.cert},
7184
ClientAuth: tls.NoClientCert,
7285
NextProtos: []string{"h2"}})
7386
require.NoError(f.T, err)
74-
s, err := Serve(svc, l)
87+
s, err := Serve(svc, l, opts...)
7588
require.NoError(f.T, err)
7689
return s
7790
}
@@ -87,3 +100,8 @@ func (f http2H2Flavour) Proto() string {
87100
func (f http2H2Flavour) Context() (context.Context, func()) {
88101
return context.WithCancel(context.Background())
89102
}
103+
104+
func (f http2H2Flavour) AssertConnectionResetError(t *testing.T, terr *terrors.Error) {
105+
assert.Equal(t, terrors.ErrInternalService, terr.Code)
106+
assert.Contains(t, terr.Message, "INTERNAL_ERROR")
107+
}

e2e_test.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import (
2323
)
2424

2525
type e2eFlavour interface {
26-
Serve(Service) *Server
26+
Serve(Service, ...ServerOption) *Server
2727
URL(*Server) string
2828
Proto() string
2929
Context() (context.Context, func())
30+
31+
AssertConnectionResetError(t *testing.T, terr *terrors.Error)
3032
}
3133

3234
// flavours runs the passed E2E test with all test flavours (HTTP/1.1, HTTP/2.0/h2c, etc.)
@@ -805,3 +807,36 @@ func TestE2EDraining(t *testing.T) {
805807
<-serverClosed
806808
})
807809
}
810+
811+
func TestE2EServerTimeouts(t *testing.T) {
812+
someFlavours(t, []string{
813+
"http1.1",
814+
"http1.1-tls",
815+
"http2.0-h2",
816+
817+
// The Go h2c implementation doesn't currently support server timeouts
818+
// See https://github.com/golang/go/issues/52868
819+
//"http2.0-h2c",
820+
//"http2.0-h2c-prior-knowledge",
821+
}, func(t *testing.T, flav e2eFlavour) {
822+
ctx, cancel := flav.Context()
823+
defer cancel()
824+
825+
srv := Service(func(req Request) Response {
826+
time.Sleep(1 * time.Second)
827+
return NewResponse(req)
828+
})
829+
srv = srv.Filter(ErrorFilter)
830+
s := flav.Serve(srv, WithTimeout(TimeoutOptions{Write: 10 * time.Millisecond}))
831+
defer s.Stop(ctx)
832+
833+
req := NewRequest(ctx, "GET", flav.URL(s), nil)
834+
rsp := req.Send().Response()
835+
if assert.Error(t, rsp.Error) {
836+
terr, ok := rsp.Error.(*terrors.Error)
837+
if assert.Truef(t, ok, "expected terror, got %T", rsp.Error) {
838+
flav.AssertConnectionResetError(t, terr)
839+
}
840+
}
841+
})
842+
}

examples/simple.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func main() {
2222
svc := router.Serve().
2323
Filter(typhon.ErrorFilter).
2424
Filter(typhon.H2cFilter)
25-
srv, err := typhon.Listen(svc, ":8000")
25+
srv, err := typhon.Listen(svc, ":8000", typhon.WithTimeout(typhon.TimeoutOptions{Read: time.Second * 10}))
2626
if err != nil {
2727
panic(err)
2828
}

h2c.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"net/textproto"
1010
"sync"
1111

12-
"github.com/deckarep/golang-set"
12+
mapset "github.com/deckarep/golang-set"
1313
"github.com/monzo/terrors"
1414
"golang.org/x/net/http/httpguts"
1515
"golang.org/x/net/http2"

request_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import (
55
"bytes"
66
"context"
77
"encoding/json"
8-
"github.com/monzo/terrors"
98
"io/ioutil"
109
"math"
1110
"strings"
1211
"testing"
1312

13+
"github.com/monzo/terrors"
14+
1415
legacyproto "github.com/golang/protobuf/proto"
1516
"github.com/stretchr/testify/assert"
1617
"github.com/stretchr/testify/require"

response_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import (
55
"context"
66
"encoding/json"
77
"errors"
8-
legacyproto "github.com/golang/protobuf/proto"
9-
"github.com/monzo/typhon/legacyprototest"
108
"io"
119
"io/ioutil"
1210
"math"
1311
"net/http"
1412
"strings"
1513
"testing"
1614

15+
legacyproto "github.com/golang/protobuf/proto"
16+
"github.com/monzo/typhon/legacyprototest"
17+
1718
"github.com/monzo/terrors"
1819
"github.com/monzo/typhon/prototest"
1920
"github.com/stretchr/testify/assert"

server.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"strconv"
1010
"sync"
11+
"time"
1112

1213
"github.com/monzo/slog"
1314
)
@@ -21,6 +22,9 @@ type Server struct {
2122
shutdownFuncsM sync.Mutex
2223
}
2324

25+
// ServerOption allows customizing the underling http.Server
26+
type ServerOption func(*Server)
27+
2428
// Listener returns the network listener that this server is active on.
2529
func (s *Server) Listener() net.Listener {
2630
return s.l
@@ -72,8 +76,8 @@ func (s *Server) addShutdownFunc(f func(context.Context)) {
7276
s.shutdownFuncs = append(s.shutdownFuncs, f)
7377
}
7478

75-
// Serve starts a HTTP server, binding the passed Service to the passed listener.
76-
func Serve(svc Service, l net.Listener) (*Server, error) {
79+
// Serve starts a HTTP server, binding the passed Service to the passed listener and applying the passed ServerOptions.
80+
func Serve(svc Service, l net.Listener, opts ...ServerOption) (*Server, error) {
7781
s := &Server{
7882
l: l,
7983
shuttingDown: make(chan struct{})}
@@ -83,7 +87,14 @@ func Serve(svc Service, l net.Listener) (*Server, error) {
8387
})
8488
s.srv = &http.Server{
8589
Handler: HttpHandler(svc),
86-
MaxHeaderBytes: http.DefaultMaxHeaderBytes}
90+
MaxHeaderBytes: http.DefaultMaxHeaderBytes,
91+
}
92+
93+
// Apply any given ServerOptions
94+
for _, opt := range opts {
95+
opt(s)
96+
}
97+
8798
go func() {
8899
err := s.srv.Serve(l)
89100
if err != nil && err != http.ErrServerClosed {
@@ -97,7 +108,7 @@ func Serve(svc Service, l net.Listener) (*Server, error) {
97108
return s, nil
98109
}
99110

100-
func Listen(svc Service, addr string) (*Server, error) {
111+
func Listen(svc Service, addr string, opts ...ServerOption) (*Server, error) {
101112
// Determine on which address to listen, choosing in order one of:
102113
// 1. The passed addr
103114
// 2. PORT variable (listening on all interfaces)
@@ -120,5 +131,26 @@ func Listen(svc Service, addr string) (*Server, error) {
120131
if err != nil {
121132
return nil, err
122133
}
123-
return Serve(svc, l)
134+
return Serve(svc, l, opts...)
135+
}
136+
137+
// TimeoutOptions specifies various server timeouts. See http.Server for details of what these do.
138+
// There's a nice post explaining them here: https://ieftimov.com/posts/make-resilient-golang-net-http-servers-using-timeouts-deadlines-context-cancellation/#server-timeouts---first-principles
139+
// WARNING: Due to a Go bug, connections using h2c do not respect these timeouts.
140+
// See https://github.com/golang/go/issues/52868
141+
type TimeoutOptions struct {
142+
Read time.Duration
143+
ReadHeader time.Duration
144+
Write time.Duration
145+
Idle time.Duration
146+
}
147+
148+
// WithTimeout sets the server timeouts.
149+
func WithTimeout(opts TimeoutOptions) ServerOption {
150+
return func(s *Server) {
151+
s.srv.ReadTimeout = opts.Read
152+
s.srv.ReadHeaderTimeout = opts.ReadHeader
153+
s.srv.WriteTimeout = opts.Write
154+
s.srv.IdleTimeout = opts.Idle
155+
}
124156
}

0 commit comments

Comments
 (0)