Skip to content

Commit b041fe3

Browse files
authored
Merge branch 'main' into ff-better-http-client-tests
2 parents 9f5f22f + ed58449 commit b041fe3

File tree

3 files changed

+185
-31
lines changed

3 files changed

+185
-31
lines changed

Diff for: Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests.swift

-15
Original file line numberDiff line numberDiff line change
@@ -465,18 +465,3 @@ class ReadEventHitHandler: ChannelOutboundHandler {
465465
context.read()
466466
}
467467
}
468-
469-
class MockConnectionDelegate: HTTP1ConnectionDelegate {
470-
private(set) var hitConnectionReleased = 0
471-
private(set) var hitConnectionClosed = 0
472-
473-
init() {}
474-
475-
func http1ConnectionReleased(_: HTTP1Connection) {
476-
self.hitConnectionReleased += 1
477-
}
478-
479-
func http1ConnectionClosed(_: HTTP1Connection) {
480-
self.hitConnectionClosed += 1
481-
}
482-
}

Diff for: Tests/AsyncHTTPClientTests/HTTP1ConnectionTests+XCTest.swift

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ extension HTTP1ConnectionTests {
3030
("testCreateNewConnectionFailureClosedIO", testCreateNewConnectionFailureClosedIO),
3131
("testGETRequest", testGETRequest),
3232
("testConnectionClosesOnCloseHeader", testConnectionClosesOnCloseHeader),
33+
("testConnectionClosesOnRandomlyAppearingCloseHeader", testConnectionClosesOnRandomlyAppearingCloseHeader),
34+
("testConnectionClosesAfterTheRequestWithoutHavingSentAnCloseHeader", testConnectionClosesAfterTheRequestWithoutHavingSentAnCloseHeader),
3335
]
3436
}
3537
}

Diff for: Tests/AsyncHTTPClientTests/HTTP1ConnectionTests.swift

+183-16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
@testable import AsyncHTTPClient
1616
import Logging
17+
import NIOConcurrencyHelpers
1718
import NIOCore
1819
import NIOEmbedded
1920
import 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

Comments
 (0)