@@ -154,3 +154,81 @@ func WithTimeout(opts TimeoutOptions) ServerOption {
154
154
s .srv .IdleTimeout = opts .Idle
155
155
}
156
156
}
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