@@ -154,3 +154,81 @@ func WithTimeout(opts TimeoutOptions) ServerOption {
154154 s .srv .IdleTimeout = opts .Idle
155155 }
156156}
157+
158+ var (
159+ connectionStartTimeHeaderKey = "X-Typhon-Connection-Start"
160+ // addConnectionStartTimeHeader is set to true within tests to
161+ // make it easier to test the server option.
162+ addConnectionStartTimeHeader = false
163+ )
164+
165+ // WithMaxConnectionAge returns a server option that will enforce a max
166+ // connection age. When a connection has reached the max connection age
167+ // then the next request that is processed on that connection will result
168+ // in the connection being gracefully closed. This does mean that if a
169+ // connection is not being used then it can outlive the maximum connection
170+ // age.
171+ func WithMaxConnectionAge (maxAge time.Duration ) ServerOption {
172+ // We have no ability within a handler to get access to the
173+ // underlying net.Conn that the request came on. However,
174+ // the http.Server has a ConnContext field that can be used
175+ // to specify a function that can modify the context used for
176+ // that connection. We can use this to store the connection
177+ // start time in the context and then in the handler we can
178+ // read that out and whenever the maxAge has been exceeded we
179+ // can close the connection.
180+ //
181+ // We could close the connection by calling the Close method
182+ // on the net.Conn. This would have the benefit that we could
183+ // close the connection exactly at the expiry but would have
184+ // the disadvantage that it does not gracefully close the
185+ // connection – it would kill all in-flight requests. Instead,
186+ // we set the 'Connection: close' response header which will
187+ // be translated into an HTTP2 GOAWAY frame and result in the
188+ // connection being gracefully closed.
189+
190+ return func (s * Server ) {
191+ // Wrap the current ConnContext (if set) to store a reference
192+ // to the connection start time in the context.
193+ origConnContext := s .srv .ConnContext
194+ s .srv .ConnContext = func (ctx context.Context , conn net.Conn ) context.Context {
195+ if origConnContext != nil {
196+ ctx = origConnContext (ctx , conn )
197+ }
198+
199+ return setConnectionStartTimeInContext (ctx , time .Now ())
200+ }
201+
202+ // Wrap the handler to set the 'Connection: close' response
203+ // header if the max age has been exceeded.
204+ origHandler := s .srv .Handler
205+ s .srv .Handler = http .HandlerFunc (func (writer http.ResponseWriter , request * http.Request ) {
206+ connectionStart , ok := readConnectionStartTimeFromContext (request .Context ())
207+ if ok {
208+ if time .Since (connectionStart ) > maxAge {
209+ h := writer .Header ()
210+ h .Add ("Connection" , "close" )
211+ }
212+
213+ // This is used within tests
214+ if addConnectionStartTimeHeader {
215+ h := writer .Header ()
216+ h .Add (connectionStartTimeHeaderKey , connectionStart .String ())
217+ }
218+ }
219+
220+ origHandler .ServeHTTP (writer , request )
221+ })
222+ }
223+ }
224+
225+ type connectionContextKey struct {}
226+
227+ func setConnectionStartTimeInContext (parent context.Context , t time.Time ) context.Context {
228+ return context .WithValue (parent , connectionContextKey {}, t )
229+ }
230+
231+ func readConnectionStartTimeFromContext (ctx context.Context ) (time.Time , bool ) {
232+ conn , ok := ctx .Value (connectionContextKey {}).(time.Time )
233+ return conn , ok
234+ }
0 commit comments