1414
1515@testable import AsyncHTTPClient
1616import Logging
17+ import NIOConcurrencyHelpers
1718import NIOCore
1819import NIOEmbedded
1920import NIOHTTP1
@@ -193,8 +194,61 @@ class HTTP1ConnectionTests: XCTestCase {
193194 let eventLoop = eventLoopGroup. next ( )
194195 defer { XCTAssertNoThrow ( try eventLoopGroup. syncShutdownGracefully ( ) ) }
195196
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+
196250 let closeOnRequest = ( 30 ... 100 ) . randomElement ( ) !
197- let httpBin = HTTPBin ( handlerFactory: { _ in SuddenlySendsCloseHeaderChannel ( closeOnRequest: closeOnRequest) } )
251+ let httpBin = HTTPBin ( handlerFactory: { _ in SuddenlySendsCloseHeaderChannelHandler ( closeOnRequest: closeOnRequest) } )
198252
199253 var maybeChannel : Channel ?
200254
@@ -216,7 +270,7 @@ class HTTP1ConnectionTests: XCTestCase {
216270 counter += 1
217271
218272 var maybeRequest : HTTPClient . Request ?
219- XCTAssertNoThrow ( maybeRequest = try HTTPClient . Request ( url: " http://localhost/ " ) )
273+ XCTAssertNoThrow ( maybeRequest = try HTTPClient . Request ( url: " http://localhost: \( httpBin . port ) / " ) )
220274 guard let request = maybeRequest else { return XCTFail ( " Expected to be able to create a request " ) }
221275
222276 let delegate = ResponseAccumulator ( request: request)
@@ -235,23 +289,80 @@ class HTTP1ConnectionTests: XCTestCase {
235289 connection. executeRequest ( requestBag)
236290
237291 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)
251294
295+ if response? . headers. first ( name: " connection " ) == " close " {
252296 break // the loop
297+ } else {
298+ XCTAssertEqual ( httpBin. activeConnections, 1 )
299+ XCTAssertEqual ( connectionDelegate. hitConnectionReleased, counter)
253300 }
254301 }
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 )
255366 }
256367}
257368
@@ -268,7 +379,8 @@ class MockHTTP1ConnectionDelegate: HTTP1ConnectionDelegate {
268379 }
269380}
270381
271- class SuddenlySendsCloseHeaderChannel : ChannelInboundHandler {
382+ /// A channel handler that sends a connection close header but does not close the connection.
383+ class SuddenlySendsCloseHeaderChannelHandler : ChannelInboundHandler {
272384 typealias InboundIn = HTTPServerRequestPart
273385 typealias OutboundOut = HTTPServerResponsePart
274386
@@ -302,3 +414,58 @@ class SuddenlySendsCloseHeaderChannel: ChannelInboundHandler {
302414 }
303415 }
304416}
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