14
14
15
15
@testable import AsyncHTTPClient
16
16
import Logging
17
+ import NIOConcurrencyHelpers
17
18
import NIOCore
18
19
import NIOEmbedded
19
20
import NIOHTTP1
@@ -193,8 +194,61 @@ class HTTP1ConnectionTests: XCTestCase {
193
194
let eventLoop = eventLoopGroup. next ( )
194
195
defer { XCTAssertNoThrow ( try eventLoopGroup. syncShutdownGracefully ( ) ) }
195
196
197
+ let httpBin = HTTPBin ( handlerFactory: { _ in SuddenlySendsCloseHeaderChannelHandler ( closeOnRequest: 1 ) } )
198
+
199
+ var maybeChannel : Channel ?
200
+
201
+ XCTAssertNoThrow ( maybeChannel = try ClientBootstrap ( group: eventLoop) . connect ( host: " localhost " , port: httpBin. port) . wait ( ) )
202
+ let connectionDelegate = MockConnectionDelegate ( )
203
+ let logger = Logger ( label: " test " )
204
+ var maybeConnection : HTTP1Connection ?
205
+ XCTAssertNoThrow ( maybeConnection = try eventLoop. submit { try HTTP1Connection . start (
206
+ channel: XCTUnwrap ( maybeChannel) ,
207
+ connectionID: 0 ,
208
+ delegate: connectionDelegate,
209
+ configuration: . init( ) ,
210
+ logger: logger
211
+ ) } . wait ( ) )
212
+ guard let connection = maybeConnection else { return XCTFail ( " Expected to have a connection here " ) }
213
+
214
+ var maybeRequest : HTTPClient . Request ?
215
+ XCTAssertNoThrow ( maybeRequest = try HTTPClient . Request ( url: " http://localhost: \( httpBin. port) / " ) )
216
+ guard let request = maybeRequest else { return XCTFail ( " Expected to be able to create a request " ) }
217
+
218
+ let delegate = ResponseAccumulator ( request: request)
219
+ var maybeRequestBag : RequestBag < ResponseAccumulator > ?
220
+ XCTAssertNoThrow ( maybeRequestBag = try RequestBag (
221
+ request: request,
222
+ eventLoopPreference: . delegate( on: eventLoopGroup. next ( ) ) ,
223
+ task: . init( eventLoop: eventLoopGroup. next ( ) , logger: logger) ,
224
+ redirectHandler: nil ,
225
+ connectionDeadline: . now( ) + . seconds( 30 ) ,
226
+ idleReadTimeout: nil ,
227
+ delegate: delegate
228
+ ) )
229
+ guard let requestBag = maybeRequestBag else { return XCTFail ( " Expected to be able to create a request bag " ) }
230
+
231
+ connection. executeRequest ( requestBag)
232
+
233
+ var response : HTTPClient . Response ?
234
+ XCTAssertNoThrow ( response = try requestBag. task. futureResult. wait ( ) )
235
+ XCTAssertEqual ( response? . status, . ok)
236
+ XCTAssertEqual ( connectionDelegate. hitConnectionReleased, 0 )
237
+ XCTAssertNoThrow ( try XCTUnwrap ( maybeChannel) . closeFuture. wait ( ) )
238
+ XCTAssertEqual ( connectionDelegate. hitConnectionClosed, 1 )
239
+
240
+ // we need to wait a small amount of time to see the connection close on the server
241
+ try ! eventLoop. scheduleTask ( in: . milliseconds( 200 ) ) { } . futureResult. wait ( )
242
+ XCTAssertEqual ( httpBin. activeConnections, 0 )
243
+ }
244
+
245
+ func testConnectionClosesOnRandomlyAppearingCloseHeader( ) {
246
+ let eventLoopGroup = MultiThreadedEventLoopGroup ( numberOfThreads: 1 )
247
+ let eventLoop = eventLoopGroup. next ( )
248
+ defer { XCTAssertNoThrow ( try eventLoopGroup. syncShutdownGracefully ( ) ) }
249
+
196
250
let closeOnRequest = ( 30 ... 100 ) . randomElement ( ) !
197
- let httpBin = HTTPBin ( handlerFactory: { _ in SuddenlySendsCloseHeaderChannel ( closeOnRequest: closeOnRequest) } )
251
+ let httpBin = HTTPBin ( handlerFactory: { _ in SuddenlySendsCloseHeaderChannelHandler ( closeOnRequest: closeOnRequest) } )
198
252
199
253
var maybeChannel : Channel ?
200
254
@@ -216,7 +270,7 @@ class HTTP1ConnectionTests: XCTestCase {
216
270
counter += 1
217
271
218
272
var maybeRequest : HTTPClient . Request ?
219
- XCTAssertNoThrow ( maybeRequest = try HTTPClient . Request ( url: " http://localhost/ " ) )
273
+ XCTAssertNoThrow ( maybeRequest = try HTTPClient . Request ( url: " http://localhost: \( httpBin . port ) / " ) )
220
274
guard let request = maybeRequest else { return XCTFail ( " Expected to be able to create a request " ) }
221
275
222
276
let delegate = ResponseAccumulator ( request: request)
@@ -235,23 +289,80 @@ class HTTP1ConnectionTests: XCTestCase {
235
289
connection. executeRequest ( requestBag)
236
290
237
291
var response : HTTPClient . Response ?
238
- if counter <= closeOnRequest {
239
- XCTAssertNoThrow ( response = try requestBag. task. futureResult. wait ( ) )
240
- XCTAssertEqual ( response? . status, . ok)
241
-
242
- if response? . headers. first ( name: " connection " ) == " close " {
243
- XCTAssertEqual ( closeOnRequest, counter)
244
- XCTAssertEqual ( maybeChannel? . isActive, false )
245
- }
246
- } else {
247
- // io on close channel leads to error
248
- XCTAssertThrowsError ( try requestBag. task. futureResult. wait ( ) ) {
249
- XCTAssertEqual ( $0 as? ChannelError , . ioOnClosedChannel)
250
- }
292
+ XCTAssertNoThrow ( response = try requestBag. task. futureResult. wait ( ) )
293
+ XCTAssertEqual ( response? . status, . ok)
251
294
295
+ if response? . headers. first ( name: " connection " ) == " close " {
252
296
break // the loop
297
+ } else {
298
+ XCTAssertEqual ( httpBin. activeConnections, 1 )
299
+ XCTAssertEqual ( connectionDelegate. hitConnectionReleased, counter)
253
300
}
254
301
}
302
+
303
+ XCTAssertNoThrow ( try XCTUnwrap ( maybeChannel) . closeFuture. wait ( ) )
304
+ XCTAssertEqual ( connectionDelegate. hitConnectionClosed, 1 )
305
+ XCTAssertFalse ( try XCTUnwrap ( maybeChannel) . isActive)
306
+
307
+ XCTAssertEqual ( counter, closeOnRequest)
308
+ XCTAssertEqual ( connectionDelegate. hitConnectionClosed, 1 )
309
+ XCTAssertEqual ( connectionDelegate. hitConnectionReleased, counter - 1 ,
310
+ " If a close header is received connection release is not triggered. " )
311
+
312
+ // we need to wait a small amount of time to see the connection close on the server
313
+ try ! eventLoop. scheduleTask ( in: . milliseconds( 200 ) ) { } . futureResult. wait ( )
314
+ XCTAssertEqual ( httpBin. activeConnections, 0 )
315
+ }
316
+
317
+ func testConnectionClosesAfterTheRequestWithoutHavingSentAnCloseHeader( ) {
318
+ let eventLoopGroup = MultiThreadedEventLoopGroup ( numberOfThreads: 1 )
319
+ let eventLoop = eventLoopGroup. next ( )
320
+ defer { XCTAssertNoThrow ( try eventLoopGroup. syncShutdownGracefully ( ) ) }
321
+
322
+ let httpBin = HTTPBin ( handlerFactory: { _ in AfterRequestCloseConnectionChannelHandler ( ) } )
323
+
324
+ var maybeChannel : Channel ?
325
+
326
+ XCTAssertNoThrow ( maybeChannel = try ClientBootstrap ( group: eventLoop) . connect ( host: " localhost " , port: httpBin. port) . wait ( ) )
327
+ let connectionDelegate = MockConnectionDelegate ( )
328
+ let logger = Logger ( label: " test " )
329
+ var maybeConnection : HTTP1Connection ?
330
+ XCTAssertNoThrow ( maybeConnection = try eventLoop. submit { try HTTP1Connection . start (
331
+ channel: XCTUnwrap ( maybeChannel) ,
332
+ connectionID: 0 ,
333
+ delegate: connectionDelegate,
334
+ configuration: . init( ) ,
335
+ logger: logger
336
+ ) } . wait ( ) )
337
+ guard let connection = maybeConnection else { return XCTFail ( " Expected to have a connection here " ) }
338
+
339
+ var maybeRequest : HTTPClient . Request ?
340
+ XCTAssertNoThrow ( maybeRequest = try HTTPClient . Request ( url: " http://localhost: \( httpBin. port) / " ) )
341
+ guard let request = maybeRequest else { return XCTFail ( " Expected to be able to create a request " ) }
342
+
343
+ let delegate = ResponseAccumulator ( request: request)
344
+ var maybeRequestBag : RequestBag < ResponseAccumulator > ?
345
+ XCTAssertNoThrow ( maybeRequestBag = try RequestBag (
346
+ request: request,
347
+ eventLoopPreference: . delegate( on: eventLoopGroup. next ( ) ) ,
348
+ task: . init( eventLoop: eventLoopGroup. next ( ) , logger: logger) ,
349
+ redirectHandler: nil ,
350
+ connectionDeadline: . now( ) + . seconds( 30 ) ,
351
+ idleReadTimeout: nil ,
352
+ delegate: delegate
353
+ ) )
354
+ guard let requestBag = maybeRequestBag else { return XCTFail ( " Expected to be able to create a request bag " ) }
355
+
356
+ connection. executeRequest ( requestBag)
357
+
358
+ var response : HTTPClient . Response ?
359
+ XCTAssertNoThrow ( response = try requestBag. task. futureResult. wait ( ) )
360
+ XCTAssertEqual ( response? . status, . ok)
361
+ XCTAssertEqual ( connectionDelegate. hitConnectionReleased, 1 )
362
+
363
+ XCTAssertNoThrow ( try XCTUnwrap ( maybeChannel) . closeFuture. wait ( ) )
364
+ XCTAssertEqual ( connectionDelegate. hitConnectionClosed, 1 )
365
+ XCTAssertEqual ( httpBin. activeConnections, 0 )
255
366
}
256
367
}
257
368
@@ -268,7 +379,8 @@ class MockHTTP1ConnectionDelegate: HTTP1ConnectionDelegate {
268
379
}
269
380
}
270
381
271
- class SuddenlySendsCloseHeaderChannel : ChannelInboundHandler {
382
+ /// A channel handler that sends a connection close header but does not close the connection.
383
+ class SuddenlySendsCloseHeaderChannelHandler : ChannelInboundHandler {
272
384
typealias InboundIn = HTTPServerRequestPart
273
385
typealias OutboundOut = HTTPServerResponsePart
274
386
@@ -302,3 +414,58 @@ class SuddenlySendsCloseHeaderChannel: ChannelInboundHandler {
302
414
}
303
415
}
304
416
}
417
+
418
+ /// A channel handler that closes a connection after a successful request
419
+ class AfterRequestCloseConnectionChannelHandler : ChannelInboundHandler {
420
+ typealias InboundIn = HTTPServerRequestPart
421
+ typealias OutboundOut = HTTPServerResponsePart
422
+
423
+ init ( ) { }
424
+
425
+ func channelRead( context: ChannelHandlerContext , data: NIOAny ) {
426
+ switch self . unwrapInboundIn ( data) {
427
+ case . head( let head) :
428
+ XCTAssertTrue ( head. headers. contains ( name: " host " ) )
429
+ XCTAssertEqual ( head. method, . GET)
430
+ case . body:
431
+ break
432
+ case . end:
433
+ context. write ( self . wrapOutboundOut ( . head( . init( version: . http1_1, status: . ok) ) ) , promise: nil )
434
+ context. write ( self . wrapOutboundOut ( . end( nil ) ) , promise: nil )
435
+ context. flush ( )
436
+
437
+ context. eventLoop. scheduleTask ( in: . milliseconds( 20 ) ) {
438
+ context. close ( promise: nil )
439
+ }
440
+ }
441
+ }
442
+ }
443
+
444
+ class MockConnectionDelegate : HTTP1ConnectionDelegate {
445
+ private var lock = Lock ( )
446
+
447
+ private var _hitConnectionReleased = 0
448
+ private var _hitConnectionClosed = 0
449
+
450
+ var hitConnectionReleased : Int {
451
+ self . lock. withLock { self . _hitConnectionReleased }
452
+ }
453
+
454
+ var hitConnectionClosed : Int {
455
+ self . lock. withLock { self . _hitConnectionClosed }
456
+ }
457
+
458
+ init ( ) { }
459
+
460
+ func http1ConnectionReleased( _: HTTP1Connection ) {
461
+ self . lock. withLockVoid {
462
+ self . _hitConnectionReleased += 1
463
+ }
464
+ }
465
+
466
+ func http1ConnectionClosed( _: HTTP1Connection ) {
467
+ self . lock. withLockVoid {
468
+ self . _hitConnectionClosed += 1
469
+ }
470
+ }
471
+ }
0 commit comments