diff --git a/XCode/Sources/HttpServerIO.swift b/XCode/Sources/HttpServerIO.swift index b20dc3fc..6ba89656 100644 --- a/XCode/Sources/HttpServerIO.swift +++ b/XCode/Sources/HttpServerIO.swift @@ -130,6 +130,16 @@ open class HttpServerIO { } } catch { print("Failed to send response: \(error)") + /* + The bodyWriter threw an exception, this could be because the socket is closed, in + which case we fail when we attempt to read from the socket again, or it could be + because the bodyWriter was unable to finish writing the body, session will be out + of sync. If we kept the connection alive, then the client will be waiting for the + remainder of the body until it gives up, and we'll never get the next request. + + Ideal action is to possibly abruptly close the connection. + */ + break } if let session = response.socketSession() { delegate?.socketConnectionReceived(socket) diff --git a/XCode/Swifter.xcodeproj/project.pbxproj b/XCode/Swifter.xcodeproj/project.pbxproj index 0bdf6e94..9e1932fc 100644 --- a/XCode/Swifter.xcodeproj/project.pbxproj +++ b/XCode/Swifter.xcodeproj/project.pbxproj @@ -110,6 +110,9 @@ 7CDAB8131BE2A1D400C8A977 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7CDAB80D1BE2A1D400C8A977 /* Main.storyboard */; }; 7CDAB8141BE2A1D400C8A977 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7CDAB80F1BE2A1D400C8A977 /* Images.xcassets */; }; 7CDAB8161BE2A1D400C8A977 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDAB8111BE2A1D400C8A977 /* ViewController.swift */; }; + AB83D32D2919F84500D4D434 /* ServerRawResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB83D32C2919F84500D4D434 /* ServerRawResponseTests.swift */; }; + AB83D32E2919F84500D4D434 /* ServerRawResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB83D32C2919F84500D4D434 /* ServerRawResponseTests.swift */; }; + AB83D32F2919F84500D4D434 /* ServerRawResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB83D32C2919F84500D4D434 /* ServerRawResponseTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -222,6 +225,7 @@ 7CDAB80F1BE2A1D400C8A977 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 7CDAB8101BE2A1D400C8A977 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7CDAB8111BE2A1D400C8A977 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + AB83D32C2919F84500D4D434 /* ServerRawResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerRawResponseTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -429,6 +433,7 @@ 0858E7F31D68BB2600491CD1 /* IOSafetyTests.swift */, 6A0D4511204E9988000A0726 /* MimeTypesTests.swift */, 0C1F3CAC2265FC470076B6F5 /* SwifterTestsHttpResponseBody.swift */, + AB83D32C2919F84500D4D434 /* ServerRawResponseTests.swift */, 7B55EC94226E0E4F00042D23 /* ServerThreadingTests.swift */, ); path = Tests; @@ -781,6 +786,7 @@ 043660CD21FED35200497989 /* SwifterTestsHttpRouter.swift in Sources */, 047F1F02226AB9AD00909B95 /* XCTestManifests.swift in Sources */, 043660CE21FED35500497989 /* SwifterTestsHttpParser.swift in Sources */, + AB83D32E2919F84500D4D434 /* ServerRawResponseTests.swift in Sources */, 043660D521FED36C00497989 /* MimeTypesTests.swift in Sources */, 0C1F3CAE2265FC470076B6F5 /* SwifterTestsHttpResponseBody.swift in Sources */, 7B55EC96226E0E4F00042D23 /* ServerThreadingTests.swift in Sources */, @@ -793,6 +799,7 @@ files = ( 043660E721FED51600497989 /* PingServer.swift in Sources */, 043660E921FED51B00497989 /* SwifterTestsWebSocketSession.swift in Sources */, + AB83D32F2919F84500D4D434 /* ServerRawResponseTests.swift in Sources */, 043660EA21FED51E00497989 /* IOSafetyTests.swift in Sources */, 043660E821FED51900497989 /* SwifterTestsStringExtensions.swift in Sources */, 043660E521FED51100497989 /* SwifterTestsHttpRouter.swift in Sources */, @@ -912,6 +919,7 @@ 7C4785E91C71D15600A9FE73 /* SwifterTestsWebSocketSession.swift in Sources */, 043660D421FED36900497989 /* IOSafetyTests.swift in Sources */, 7CCD87721C660B250068099B /* SwifterTestsStringExtensions.swift in Sources */, + AB83D32D2919F84500D4D434 /* ServerRawResponseTests.swift in Sources */, 7B11AD4B21C9A8A6002F8820 /* SwifterTestsHttpRouter.swift in Sources */, 0C1F3CAD2265FC470076B6F5 /* SwifterTestsHttpResponseBody.swift in Sources */, 7B55EC95226E0E4F00042D23 /* ServerThreadingTests.swift in Sources */, diff --git a/XCode/Sources/HttpParser.swift b/Xcode/Sources/HttpParser.swift similarity index 100% rename from XCode/Sources/HttpParser.swift rename to Xcode/Sources/HttpParser.swift diff --git a/XCode/Sources/HttpRequest.swift b/Xcode/Sources/HttpRequest.swift similarity index 100% rename from XCode/Sources/HttpRequest.swift rename to Xcode/Sources/HttpRequest.swift diff --git a/XCode/Sources/HttpResponse.swift b/Xcode/Sources/HttpResponse.swift similarity index 100% rename from XCode/Sources/HttpResponse.swift rename to Xcode/Sources/HttpResponse.swift diff --git a/XCode/Sources/Socket+Server.swift b/Xcode/Sources/Socket+Server.swift similarity index 100% rename from XCode/Sources/Socket+Server.swift rename to Xcode/Sources/Socket+Server.swift diff --git a/XCode/Sources/Socket.swift b/Xcode/Sources/Socket.swift similarity index 100% rename from XCode/Sources/Socket.swift rename to Xcode/Sources/Socket.swift diff --git a/XCode/Sources/String+File.swift b/Xcode/Sources/String+File.swift similarity index 100% rename from XCode/Sources/String+File.swift rename to Xcode/Sources/String+File.swift diff --git a/XCode/Tests/IOSafetyTests.swift b/Xcode/Tests/IOSafetyTests.swift similarity index 100% rename from XCode/Tests/IOSafetyTests.swift rename to Xcode/Tests/IOSafetyTests.swift diff --git a/XCode/Tests/MimeTypesTests.swift b/Xcode/Tests/MimeTypesTests.swift similarity index 100% rename from XCode/Tests/MimeTypesTests.swift rename to Xcode/Tests/MimeTypesTests.swift diff --git a/XCode/Tests/PingServer.swift b/Xcode/Tests/PingServer.swift similarity index 100% rename from XCode/Tests/PingServer.swift rename to Xcode/Tests/PingServer.swift diff --git a/Xcode/Tests/ServerRawResponseTests.swift b/Xcode/Tests/ServerRawResponseTests.swift new file mode 100644 index 00000000..de9bbac1 --- /dev/null +++ b/Xcode/Tests/ServerRawResponseTests.swift @@ -0,0 +1,134 @@ +// +// ServerRawResponseTests.swift +// Swifter +// +// Created by Stuart Espey on 11/08/22. + +import XCTest +#if os(Linux) +import FoundationNetworking +#endif +@testable import Swifter + +class ServerRawResponseTests: XCTestCase { + + public enum TestError: Error { + case aborted + } + + var server: HttpServer! + + override func setUp() { + super.setUp() + server = HttpServer() + } + + override func tearDown() { + if server.operating { + server.stop() + } + server = nil + super.tearDown() + } + + /** + Adds the handler, at the teststring path, then expects that XXX-CustomHeader is equal to testString, and the + body. Test should pass instantly-ish. + + If expectError is true, then an error is expected, and response and body should be nil + */ + func doHandlerTest( handler: @escaping (HttpRequest) -> HttpResponse, testString: String, expectError: Bool = false) { + let path = "/\(testString)" + server.GET[path] = handler + + var requestExpectation: XCTestExpectation? = expectation(description: "Should handle the request quickly") + + do { + try server.start() + + DispatchQueue.global().async { + + let task = URLSession.shared.executeAsyncTask(path: path) { (body, response, error ) in + + if expectError { + XCTAssertNil(body) + XCTAssertNil(response) + XCTAssertNotNil(error) + } else { + XCTAssertNil(error) + let response = response as? HTTPURLResponse + XCTAssertNotNil(response) + + let statusCode = response?.statusCode + XCTAssertNotNil(statusCode) + XCTAssertEqual(statusCode, 200) + #if !os(Linux) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, *) { + let header = response?.value(forHTTPHeaderField: "XXX-Custom-Header") ?? "" + XCTAssertEqual(header, testString) + } + #endif + + XCTAssertEqual(body, testString.data(using: .utf8)) + } + + requestExpectation?.fulfill() + requestExpectation = nil + } + + task.resume() + } + + } catch let error { + XCTFail("\(error)") + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testRawResponseWithBodyWriter() { + + let testString = "normal" + + let handler: ((HttpRequest) -> HttpResponse) = { _ in + return HttpResponse.raw(200, "OK", ["XXX-Custom-Header": testString], { + try $0.write([UInt8]((testString) .utf8)) + }) + } + + doHandlerTest(handler: handler, testString: testString) + } + + func testFailedUnboundedResponseShouldNotHang() { + + let testString = "unbounded" + let handler: ((HttpRequest) -> HttpResponse) = { _ in + return HttpResponse.raw(200, "OK", ["XXX-Custom-Header": testString], { + try $0.write([UInt8]((testString) .utf8)) + + // simulates the body writer not being able to finish the body. + throw TestError.aborted + }) + } + + doHandlerTest(handler: handler, testString: testString) + } + + func testFailedBoundedResponseShouldNotHang() { + + let testString = "bounded" // content-length: 7 + let handler: ((HttpRequest) -> HttpResponse) = { _ in + return HttpResponse.raw(200, "OK", [ + "XXX-Custom-Header": testString, + "Content-Length": "\(testString.utf8.count+1)" // we'll be missing one byte + ], { + try $0.write([UInt8]((testString) .utf8)) + + throw TestError.aborted + }) + } + + // because the length is known, we should trigger a network failure + doHandlerTest(handler: handler, testString: testString, expectError: true) + } +} diff --git a/XCode/Tests/ServerThreadingTests.swift b/Xcode/Tests/ServerThreadingTests.swift similarity index 100% rename from XCode/Tests/ServerThreadingTests.swift rename to Xcode/Tests/ServerThreadingTests.swift diff --git a/XCode/Tests/SwifterTestsHttpParser.swift b/Xcode/Tests/SwifterTestsHttpParser.swift similarity index 100% rename from XCode/Tests/SwifterTestsHttpParser.swift rename to Xcode/Tests/SwifterTestsHttpParser.swift diff --git a/XCode/Tests/SwifterTestsHttpResponseBody.swift b/Xcode/Tests/SwifterTestsHttpResponseBody.swift similarity index 100% rename from XCode/Tests/SwifterTestsHttpResponseBody.swift rename to Xcode/Tests/SwifterTestsHttpResponseBody.swift diff --git a/XCode/Tests/SwifterTestsHttpRouter.swift b/Xcode/Tests/SwifterTestsHttpRouter.swift similarity index 100% rename from XCode/Tests/SwifterTestsHttpRouter.swift rename to Xcode/Tests/SwifterTestsHttpRouter.swift