From 563595b5197188a6770e2482ac3e6d665c5a1578 Mon Sep 17 00:00:00 2001 From: Jon Shier Date: Sat, 27 Jan 2024 18:53:02 -0500 Subject: [PATCH] Refactor Project Structure, Break Request Types Out Into Separate Files (#3819) ### Goals :soccer: Alamofire's project structure has grown from its original single-file origin into many files on disk. Unfortunately, as the project has grown, the `Request.swift` file has grown rather gigantic, topping 2600 lines after the recent addition of `WebSocketRequest`. This PR breaks up `Request.swift` into separate files for each request type. It also moves all the previous serialization extensions from `ResponseSerialization.swift` to their associated type's files. This PR also restructures some of the project's layout on disk to make some Xcode groups proper folders. This PR also creates the `AFInfo` namespace and moves the previously internal `version` value into it to make it public. This PR also adds primary associated types to the serialization protocols. --- .spi.yml | 5 + Alamofire.xcodeproj/project.pbxproj | 189 +- .../xcschemes/Alamofire iOS.xcscheme | 2 +- .../xcschemes/Alamofire macOS.xcscheme | 2 +- .../xcschemes/Alamofire tvOS.xcscheme | 2 +- .../xcschemes/Alamofire visionOS.xcscheme | 2 +- .../xcschemes/Alamofire watchOS.xcscheme | 2 +- .../xcschemes/iOS Example.xcscheme | 2 +- Package.swift | 4 +- Package@swift-5.9.swift | 50 + Source/Alamofire.swift | 7 +- Source/{ => Core}/AFError.swift | 0 Source/Core/DataRequest.swift | 448 +++ Source/Core/DataStreamRequest.swift | 584 ++++ Source/Core/DownloadRequest.swift | 588 ++++ Source/{ => Core}/HTTPHeaders.swift | 8 +- Source/{ => Core}/HTTPMethod.swift | 0 Source/{ => Core}/Notifications.swift | 0 Source/{ => Core}/ParameterEncoder.swift | 0 Source/{ => Core}/ParameterEncoding.swift | 0 Source/{ => Core}/Protected.swift | 0 Source/Core/Request.swift | 1090 +++++++ Source/{ => Core}/RequestTaskMap.swift | 0 Source/{ => Core}/Response.swift | 0 Source/{ => Core}/Session.swift | 0 Source/{ => Core}/SessionDelegate.swift | 0 ...URLConvertible+URLRequestConvertible.swift | 0 Source/Core/UploadRequest.swift | 174 ++ Source/Core/WebSocketRequest.swift | 560 ++++ .../DispatchQueue+Alamofire.swift | 0 .../OperationQueue+Alamofire.swift | 0 .../{ => Extensions}/Result+Alamofire.swift | 0 .../StringEncoding+Alamofire.swift | 0 .../URLRequest+Alamofire.swift | 0 .../URLSessionConfiguration+Alamofire.swift | 0 Source/{ => Features}/AlamofireExtended.swift | 0 .../AuthenticationInterceptor.swift | 0 .../CachedResponseHandler.swift | 0 Source/{ => Features}/Combine.swift | 0 Source/{ => Features}/Concurrency.swift | 0 Source/{ => Features}/EventMonitor.swift | 0 Source/{ => Features}/MultipartFormData.swift | 0 Source/{ => Features}/MultipartUpload.swift | 0 .../NetworkReachabilityManager.swift | 0 Source/{ => Features}/RedirectHandler.swift | 0 .../{ => Features}/RequestCompression.swift | 0 .../{ => Features}/RequestInterceptor.swift | 0 Source/Features/ResponseSerialization.swift | 525 ++++ Source/{ => Features}/RetryPolicy.swift | 0 .../ServerTrustEvaluation.swift | 0 .../URLEncodedFormEncoder.swift | 0 Source/{ => Features}/Validation.swift | 0 Source/Request.swift | 2603 ----------------- Source/ResponseSerialization.swift | 1270 -------- Tests/ConcurrencyTests.swift | 2 +- Tests/DownloadTests.swift | 16 +- Tests/ResponseTests.swift | 4 +- Tests/SessionTests.swift | 50 +- Tests/TestHelpers.swift | 31 +- Tests/ValidationTests.swift | 109 - .../watchOS Example WatchKit App.xcscheme | 2 +- 61 files changed, 4245 insertions(+), 4086 deletions(-) create mode 100644 .spi.yml create mode 100644 Package@swift-5.9.swift rename Source/{ => Core}/AFError.swift (100%) create mode 100644 Source/Core/DataRequest.swift create mode 100644 Source/Core/DataStreamRequest.swift create mode 100644 Source/Core/DownloadRequest.swift rename Source/{ => Core}/HTTPHeaders.swift (98%) rename Source/{ => Core}/HTTPMethod.swift (100%) rename Source/{ => Core}/Notifications.swift (100%) rename Source/{ => Core}/ParameterEncoder.swift (100%) rename Source/{ => Core}/ParameterEncoding.swift (100%) rename Source/{ => Core}/Protected.swift (100%) create mode 100644 Source/Core/Request.swift rename Source/{ => Core}/RequestTaskMap.swift (100%) rename Source/{ => Core}/Response.swift (100%) rename Source/{ => Core}/Session.swift (100%) rename Source/{ => Core}/SessionDelegate.swift (100%) rename Source/{ => Core}/URLConvertible+URLRequestConvertible.swift (100%) create mode 100644 Source/Core/UploadRequest.swift create mode 100644 Source/Core/WebSocketRequest.swift rename Source/{ => Extensions}/DispatchQueue+Alamofire.swift (100%) rename Source/{ => Extensions}/OperationQueue+Alamofire.swift (100%) rename Source/{ => Extensions}/Result+Alamofire.swift (100%) rename Source/{ => Extensions}/StringEncoding+Alamofire.swift (100%) rename Source/{ => Extensions}/URLRequest+Alamofire.swift (100%) rename Source/{ => Extensions}/URLSessionConfiguration+Alamofire.swift (100%) rename Source/{ => Features}/AlamofireExtended.swift (100%) rename Source/{ => Features}/AuthenticationInterceptor.swift (100%) rename Source/{ => Features}/CachedResponseHandler.swift (100%) rename Source/{ => Features}/Combine.swift (100%) rename Source/{ => Features}/Concurrency.swift (100%) rename Source/{ => Features}/EventMonitor.swift (100%) rename Source/{ => Features}/MultipartFormData.swift (100%) rename Source/{ => Features}/MultipartUpload.swift (100%) rename Source/{ => Features}/NetworkReachabilityManager.swift (100%) rename Source/{ => Features}/RedirectHandler.swift (100%) rename Source/{ => Features}/RequestCompression.swift (100%) rename Source/{ => Features}/RequestInterceptor.swift (100%) create mode 100644 Source/Features/ResponseSerialization.swift rename Source/{ => Features}/RetryPolicy.swift (100%) rename Source/{ => Features}/ServerTrustEvaluation.swift (100%) rename Source/{ => Features}/URLEncodedFormEncoder.swift (100%) rename Source/{ => Features}/Validation.swift (100%) delete mode 100644 Source/Request.swift delete mode 100644 Source/ResponseSerialization.swift diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 000000000..9eb5368cc --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +external_links: + documentation: "https://alamofire.github.io/Alamofire/" +metadata: + authors: "Jon Shier, Christian Noon" diff --git a/Alamofire.xcodeproj/project.pbxproj b/Alamofire.xcodeproj/project.pbxproj index ee1a25922..4831c7220 100644 --- a/Alamofire.xcodeproj/project.pbxproj +++ b/Alamofire.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 63; objects = { /* Begin PBXBuildFile section */ @@ -340,6 +340,31 @@ 317A6A7620B2207F00A9FEC5 /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */; }; 317A6A7720B2208000A9FEC5 /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */; }; 317A6A7820B2208000A9FEC5 /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */; }; + 318702EC2B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702EB2B0AEDBB00C10A8C /* WebSocketRequest.swift */; }; + 318702ED2B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702EB2B0AEDBB00C10A8C /* WebSocketRequest.swift */; }; + 318702EE2B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702EB2B0AEDBB00C10A8C /* WebSocketRequest.swift */; }; + 318702EF2B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702EB2B0AEDBB00C10A8C /* WebSocketRequest.swift */; }; + 318702F02B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702EB2B0AEDBB00C10A8C /* WebSocketRequest.swift */; }; + 318702F22B0AEE3700C10A8C /* DataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F12B0AEE3700C10A8C /* DataRequest.swift */; }; + 318702F32B0AEE3700C10A8C /* DataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F12B0AEE3700C10A8C /* DataRequest.swift */; }; + 318702F42B0AEE3700C10A8C /* DataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F12B0AEE3700C10A8C /* DataRequest.swift */; }; + 318702F52B0AEE3700C10A8C /* DataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F12B0AEE3700C10A8C /* DataRequest.swift */; }; + 318702F62B0AEE3700C10A8C /* DataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F12B0AEE3700C10A8C /* DataRequest.swift */; }; + 318702F82B0AEEE400C10A8C /* UploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F72B0AEEE400C10A8C /* UploadRequest.swift */; }; + 318702F92B0AEEE400C10A8C /* UploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F72B0AEEE400C10A8C /* UploadRequest.swift */; }; + 318702FA2B0AEEE400C10A8C /* UploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F72B0AEEE400C10A8C /* UploadRequest.swift */; }; + 318702FB2B0AEEE400C10A8C /* UploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F72B0AEEE400C10A8C /* UploadRequest.swift */; }; + 318702FC2B0AEEE400C10A8C /* UploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702F72B0AEEE400C10A8C /* UploadRequest.swift */; }; + 318702FE2B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702FD2B0AEF1D00C10A8C /* DataStreamRequest.swift */; }; + 318702FF2B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702FD2B0AEF1D00C10A8C /* DataStreamRequest.swift */; }; + 318703002B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702FD2B0AEF1D00C10A8C /* DataStreamRequest.swift */; }; + 318703012B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702FD2B0AEF1D00C10A8C /* DataStreamRequest.swift */; }; + 318703022B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318702FD2B0AEF1D00C10A8C /* DataStreamRequest.swift */; }; + 318703042B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318703032B0AEF4B00C10A8C /* DownloadRequest.swift */; }; + 318703052B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318703032B0AEF4B00C10A8C /* DownloadRequest.swift */; }; + 318703062B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318703032B0AEF4B00C10A8C /* DownloadRequest.swift */; }; + 318703072B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318703032B0AEF4B00C10A8C /* DownloadRequest.swift */; }; + 318703082B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318703032B0AEF4B00C10A8C /* DownloadRequest.swift */; }; 318DD40F2439780500963291 /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318DD40E2439780500963291 /* Combine.swift */; }; 318DD4102439780500963291 /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318DD40E2439780500963291 /* Combine.swift */; }; 318DD4112439780500963291 /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318DD40E2439780500963291 /* Combine.swift */; }; @@ -641,6 +666,11 @@ 317338F42A43BE5F00D4EA0A /* Alamofire visionOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Alamofire visionOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 317339432A43BF9E00D4EA0A /* visionOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = visionOS.xctestplan; sourceTree = ""; }; 31762DC9247738FA0025C704 /* LeaksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaksTests.swift; sourceTree = ""; }; + 318702EB2B0AEDBB00C10A8C /* WebSocketRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketRequest.swift; sourceTree = ""; }; + 318702F12B0AEE3700C10A8C /* DataRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataRequest.swift; sourceTree = ""; }; + 318702F72B0AEEE400C10A8C /* UploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadRequest.swift; sourceTree = ""; }; + 318702FD2B0AEF1D00C10A8C /* DataStreamRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStreamRequest.swift; sourceTree = ""; }; + 318703032B0AEF4B00C10A8C /* DownloadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadRequest.swift; sourceTree = ""; }; 318DD40E2439780500963291 /* Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combine.swift; sourceTree = ""; }; 3191B5741F5F53A6003960A8 /* Protected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protected.swift; sourceTree = ""; }; 31991790209CDA7F00103A19 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; @@ -890,6 +920,68 @@ path = ISSUE_TEMPLATE; sourceTree = ""; }; + 31B6FE482B65812D003673E3 /* Core */ = { + isa = PBXGroup; + children = ( + 4C1DC8531B68908E00476DE3 /* AFError.swift */, + 318702F12B0AEE3700C10A8C /* DataRequest.swift */, + 318702FD2B0AEF1D00C10A8C /* DataStreamRequest.swift */, + 318703032B0AEF4B00C10A8C /* DownloadRequest.swift */, + 319917A9209CDCB000103A19 /* HTTPHeaders.swift */, + 31727417218BAEC90039FFCC /* HTTPMethod.swift */, + 4CB928281C66BFBC00CE5F08 /* Notifications.swift */, + 3172741C218BB1790039FFCC /* ParameterEncoder.swift */, + 4CE2724E1AF88FB500F1D59A /* ParameterEncoding.swift */, + 3191B5741F5F53A6003960A8 /* Protected.swift */, + 31991790209CDA7F00103A19 /* Request.swift */, + 319917A4209CDAC400103A19 /* RequestTaskMap.swift */, + 31991791209CDA7F00103A19 /* Response.swift */, + 31991792209CDA7F00103A19 /* Session.swift */, + 31991793209CDA7F00103A19 /* SessionDelegate.swift */, + 318702F72B0AEEE400C10A8C /* UploadRequest.swift */, + 31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */, + 318702EB2B0AEDBB00C10A8C /* WebSocketRequest.swift */, + ); + path = Core; + sourceTree = ""; + }; + 31B6FE492B65814D003673E3 /* Extensions */ = { + isa = PBXGroup; + children = ( + 4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */, + 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */, + 4196936122FA1E05001EA5D5 /* Result+Alamofire.swift */, + 315A4C55241EF28B00D57C7A /* StringEncoding+Alamofire.swift */, + 4C0CB640220CA89400604EDC /* URLRequest+Alamofire.swift */, + 31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 31B6FE4A2B65816C003673E3 /* Features */ = { + isa = PBXGroup; + children = ( + 31DADDFA224811ED0051390F /* AlamofireExtended.swift */, + 4C67D1352454B12A00CBA725 /* AuthenticationInterceptor.swift */, + 4C4466EA21F8F5D800AC9703 /* CachedResponseHandler.swift */, + 318DD40E2439780500963291 /* Combine.swift */, + 31B3DE3A25C11CEA00760641 /* Concurrency.swift */, + 3111CE8720A77843008315E2 /* EventMonitor.swift */, + 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */, + 311B198F20B0D3B40036823B /* MultipartUpload.swift */, + 4C3D00531C66A63000D1F709 /* NetworkReachabilityManager.swift */, + 4C0CB645220CA8A400604EDC /* RedirectHandler.swift */, + 3165407229AEBC0400C9BE08 /* RequestCompression.swift */, + 4C256A0521EEB69000AD5D87 /* RequestInterceptor.swift */, + 4CDE2C451AF89FF300BABAE5 /* ResponseSerialization.swift */, + 4C256A1921F1449C00AD5D87 /* RetryPolicy.swift */, + 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */, + 31FB2F8622C828D8007FD6D5 /* URLEncodedFormEncoder.swift */, + 4CDE2C421AF89F0900BABAE5 /* Validation.swift */, + ); + path = Features; + sourceTree = ""; + }; 31EF4BF4279646000048A19D /* Test Plans */ = { isa = PBXGroup; children = ( @@ -983,19 +1075,6 @@ path = Resources/Images; sourceTree = ""; }; - 4C4366991D7BB92700C38AAD /* Extensions */ = { - isa = PBXGroup; - children = ( - 4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */, - 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */, - 4196936122FA1E05001EA5D5 /* Result+Alamofire.swift */, - 315A4C55241EF28B00D57C7A /* StringEncoding+Alamofire.swift */, - 4C0CB640220CA89400604EDC /* URLRequest+Alamofire.swift */, - 31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */, - ); - name = Extensions; - sourceTree = ""; - }; 4C7C8D201B9D0D7300948136 /* Extensions */ = { isa = PBXGroup; children = ( @@ -1019,50 +1098,6 @@ name = "Migration Guides"; sourceTree = ""; }; - 4CDE2C481AF8A14A00BABAE5 /* Core */ = { - isa = PBXGroup; - children = ( - 4C1DC8531B68908E00476DE3 /* AFError.swift */, - 319917A9209CDCB000103A19 /* HTTPHeaders.swift */, - 31727417218BAEC90039FFCC /* HTTPMethod.swift */, - 4CB928281C66BFBC00CE5F08 /* Notifications.swift */, - 3172741C218BB1790039FFCC /* ParameterEncoder.swift */, - 4CE2724E1AF88FB500F1D59A /* ParameterEncoding.swift */, - 3191B5741F5F53A6003960A8 /* Protected.swift */, - 31991790209CDA7F00103A19 /* Request.swift */, - 319917A4209CDAC400103A19 /* RequestTaskMap.swift */, - 31991791209CDA7F00103A19 /* Response.swift */, - 31991792209CDA7F00103A19 /* Session.swift */, - 31991793209CDA7F00103A19 /* SessionDelegate.swift */, - 31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */, - ); - name = Core; - sourceTree = ""; - }; - 4CDE2C491AF8A14E00BABAE5 /* Features */ = { - isa = PBXGroup; - children = ( - 31DADDFA224811ED0051390F /* AlamofireExtended.swift */, - 4C67D1352454B12A00CBA725 /* AuthenticationInterceptor.swift */, - 4C4466EA21F8F5D800AC9703 /* CachedResponseHandler.swift */, - 318DD40E2439780500963291 /* Combine.swift */, - 31B3DE3A25C11CEA00760641 /* Concurrency.swift */, - 3111CE8720A77843008315E2 /* EventMonitor.swift */, - 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */, - 311B198F20B0D3B40036823B /* MultipartUpload.swift */, - 4C3D00531C66A63000D1F709 /* NetworkReachabilityManager.swift */, - 4C0CB645220CA8A400604EDC /* RedirectHandler.swift */, - 3165407229AEBC0400C9BE08 /* RequestCompression.swift */, - 4C256A0521EEB69000AD5D87 /* RequestInterceptor.swift */, - 4CDE2C451AF89FF300BABAE5 /* ResponseSerialization.swift */, - 4C256A1921F1449C00AD5D87 /* RetryPolicy.swift */, - 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */, - 31FB2F8622C828D8007FD6D5 /* URLEncodedFormEncoder.swift */, - 4CDE2C421AF89F0900BABAE5 /* Validation.swift */, - ); - name = Features; - sourceTree = ""; - }; 4CE292301EF4A386008DA555 /* Documentation */ = { isa = PBXGroup; children = ( @@ -1152,9 +1187,9 @@ isa = PBXGroup; children = ( F897FF4019AA800700AB5182 /* Alamofire.swift */, - 4CDE2C481AF8A14A00BABAE5 /* Core */, - 4C4366991D7BB92700C38AAD /* Extensions */, - 4CDE2C491AF8A14E00BABAE5 /* Features */, + 31B6FE482B65812D003673E3 /* Core */, + 31B6FE492B65814D003673E3 /* Extensions */, + 31B6FE4A2B65816C003673E3 /* Features */, F8111E3619A95C8B0040E7D1 /* Supporting Files */, ); path = Source; @@ -1379,7 +1414,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1500; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1530; ORGANIZATIONNAME = Alamofire; TargetAttributes = { 31293064263E17D600473CEA = { @@ -1423,7 +1458,7 @@ }; }; buildConfigurationList = F8111E2D19A95C8B0040E7D1 /* Build configuration list for PBXProject "Alamofire" */; - compatibilityVersion = "Xcode 12.0"; + compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -1431,6 +1466,7 @@ Base, ); mainGroup = F8111E2919A95C8B0040E7D1; + minimizedProjectReferenceProxies = 1; productRefGroup = F8111E3419A95C8B0040E7D1 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1732,8 +1768,10 @@ 317338CC2A43A51100D4EA0A /* AFError.swift in Sources */, 317338CD2A43A51100D4EA0A /* HTTPHeaders.swift in Sources */, 317338CE2A43A51100D4EA0A /* HTTPMethod.swift in Sources */, + 318702FC2B0AEEE400C10A8C /* UploadRequest.swift in Sources */, 317338CF2A43A51100D4EA0A /* Notifications.swift in Sources */, 317338D02A43A51100D4EA0A /* ParameterEncoder.swift in Sources */, + 318703082B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */, 317338D12A43A51100D4EA0A /* ParameterEncoding.swift in Sources */, 317338D22A43A51100D4EA0A /* Protected.swift in Sources */, 317338D32A43A51100D4EA0A /* Request.swift in Sources */, @@ -1745,7 +1783,10 @@ 317338D92A43A51100D4EA0A /* DispatchQueue+Alamofire.swift in Sources */, 317338DA2A43A51100D4EA0A /* OperationQueue+Alamofire.swift in Sources */, 317338DB2A43A51100D4EA0A /* Result+Alamofire.swift in Sources */, + 318702F02B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */, + 318702F62B0AEE3700C10A8C /* DataRequest.swift in Sources */, 317338DC2A43A51100D4EA0A /* StringEncoding+Alamofire.swift in Sources */, + 318703022B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */, 317338DD2A43A51100D4EA0A /* URLRequest+Alamofire.swift in Sources */, 317338DE2A43A51100D4EA0A /* URLSessionConfiguration+Alamofire.swift in Sources */, 317338DF2A43A51100D4EA0A /* AlamofireExtended.swift in Sources */, @@ -1822,8 +1863,10 @@ 3191B5771F5F53A6003960A8 /* Protected.swift in Sources */, 3199179A209CDA7F00103A19 /* Response.swift in Sources */, 31D83FD020D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, + 318702FA2B0AEEE400C10A8C /* UploadRequest.swift in Sources */, 31B3DE3D25C11CEA00760641 /* Concurrency.swift in Sources */, 319917A7209CDAC400103A19 /* RequestTaskMap.swift in Sources */, + 318703062B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */, 4CF627131BA7CBF60011A099 /* Validation.swift in Sources */, 31F5085F20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */, 4196936422FA1E05001EA5D5 /* Result+Alamofire.swift in Sources */, @@ -1835,7 +1878,10 @@ 4C0CB648220CA8A400604EDC /* RedirectHandler.swift in Sources */, 4CF6270E1BA7CBF60011A099 /* MultipartFormData.swift in Sources */, 4CB9282B1C66BFBC00CE5F08 /* Notifications.swift in Sources */, + 318702EE2B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */, + 318702F42B0AEE3700C10A8C /* DataRequest.swift in Sources */, 31FB2F8922C828D8007FD6D5 /* URLEncodedFormEncoder.swift in Sources */, + 318703002B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */, 4C0CB643220CA89400604EDC /* URLRequest+Alamofire.swift in Sources */, 4CF6270F1BA7CBF60011A099 /* ResponseSerialization.swift in Sources */, 4C256A0821EEB69000AD5D87 /* RequestInterceptor.swift in Sources */, @@ -1912,8 +1958,10 @@ 3191B5761F5F53A6003960A8 /* Protected.swift in Sources */, 4CDE2C471AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */, 31991799209CDA7F00103A19 /* Response.swift in Sources */, + 318702F92B0AEEE400C10A8C /* UploadRequest.swift in Sources */, 31B3DE3C25C11CEA00760641 /* Concurrency.swift in Sources */, 31D83FCF20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, + 318703052B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */, 319917A6209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4C1DC8551B68908E00476DE3 /* AFError.swift in Sources */, 31F5085E20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */, @@ -1925,7 +1973,10 @@ 31727419218BAEC90039FFCC /* HTTPMethod.swift in Sources */, 4C0CB647220CA8A400604EDC /* RedirectHandler.swift in Sources */, 4CB9282A1C66BFBC00CE5F08 /* Notifications.swift in Sources */, + 318702ED2B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */, + 318702F32B0AEE3700C10A8C /* DataRequest.swift in Sources */, 4DD67C251A5C590000ED2280 /* Alamofire.swift in Sources */, + 318702FF2B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */, 31FB2F8822C828D8007FD6D5 /* URLEncodedFormEncoder.swift in Sources */, 4C0CB642220CA89400604EDC /* URLRequest+Alamofire.swift in Sources */, 4C23EB441B327C5B0090E0BC /* MultipartFormData.swift in Sources */, @@ -1956,8 +2007,10 @@ E4202FD01B667AA100C997FB /* ParameterEncoding.swift in Sources */, 3191B5781F5F53A6003960A8 /* Protected.swift in Sources */, 3199179B209CDA7F00103A19 /* Response.swift in Sources */, + 318702FB2B0AEEE400C10A8C /* UploadRequest.swift in Sources */, 31B3DE3E25C11CEA00760641 /* Concurrency.swift in Sources */, 31D83FD120D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, + 318703072B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */, 319917A8209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4CEC605A1B745C9100E684F4 /* AFError.swift in Sources */, 31F5086020B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */, @@ -1969,7 +2022,10 @@ 3172741B218BAEC90039FFCC /* HTTPMethod.swift in Sources */, 4C0CB649220CA8A400604EDC /* RedirectHandler.swift in Sources */, E4202FD21B667AA100C997FB /* ResponseSerialization.swift in Sources */, + 318702EF2B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */, + 318702F52B0AEE3700C10A8C /* DataRequest.swift in Sources */, 4CB9282C1C66BFBC00CE5F08 /* Notifications.swift in Sources */, + 318703012B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */, 31FB2F8A22C828D8007FD6D5 /* URLEncodedFormEncoder.swift in Sources */, 4C0CB644220CA89400604EDC /* URLRequest+Alamofire.swift in Sources */, 4C256A0921EEB69000AD5D87 /* RequestInterceptor.swift in Sources */, @@ -2000,8 +2056,10 @@ 3191B5751F5F53A6003960A8 /* Protected.swift in Sources */, 4CDE2C461AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */, 31991798209CDA7F00103A19 /* Response.swift in Sources */, + 318702F82B0AEEE400C10A8C /* UploadRequest.swift in Sources */, 31B3DE3B25C11CEA00760641 /* Concurrency.swift in Sources */, 31D83FCE20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, + 318703042B0AEF4B00C10A8C /* DownloadRequest.swift in Sources */, 319917A5209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4C1DC8541B68908E00476DE3 /* AFError.swift in Sources */, 31F5085D20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */, @@ -2013,7 +2071,10 @@ 31727418218BAEC90039FFCC /* HTTPMethod.swift in Sources */, 4C0CB646220CA8A400604EDC /* RedirectHandler.swift in Sources */, F897FF4119AA800700AB5182 /* Alamofire.swift in Sources */, + 318702EC2B0AEDBB00C10A8C /* WebSocketRequest.swift in Sources */, + 318702F22B0AEE3700C10A8C /* DataRequest.swift in Sources */, 4C23EB431B327C5B0090E0BC /* MultipartFormData.swift in Sources */, + 318702FE2B0AEF1D00C10A8C /* DataStreamRequest.swift in Sources */, 31FB2F8722C828D8007FD6D5 /* URLEncodedFormEncoder.swift in Sources */, 4C0CB641220CA89400604EDC /* URLRequest+Alamofire.swift in Sources */, 4C811F8D1B51856D00E0F59A /* ServerTrustEvaluation.swift in Sources */, @@ -2458,6 +2519,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -2531,6 +2593,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme index c89f426a8..e6ef66fe2 100644 --- a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme +++ b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme @@ -1,6 +1,6 @@ Void) -> Void)? + } + + private let dataMutableState = Protected(DataMutableState()) + + /// Creates a `DataRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this instance. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + init(id: UUID = UUID(), + convertible: URLRequestConvertible, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.convertible = convertible + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func reset() { + super.reset() + + dataMutableState.write { mutableState in + mutableState.data = nil + } + } + + /// Called when `Data` is received by this instance. + /// + /// - Note: Also calls `updateDownloadProgress`. + /// + /// - Parameter data: The `Data` received. + func didReceive(data: Data) { + dataMutableState.write { mutableState in + if mutableState.data == nil { + mutableState.data = data + } else { + mutableState.data?.append(data) + } + } + + updateDownloadProgress() + } + + func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + dataMutableState.read { dataMutableState in + guard let httpResponseHandler = dataMutableState.httpResponseHandler else { + underlyingQueue.async { completionHandler(.allow) } + return + } + + httpResponseHandler.queue.async { + httpResponseHandler.handler(response) { disposition in + if disposition == .cancel { + self.mutableState.write { mutableState in + mutableState.state = .cancelled + mutableState.error = mutableState.error ?? AFError.explicitlyCancelled + } + } + + self.underlyingQueue.async { + completionHandler(disposition.sessionDisposition) + } + } + } + } + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + let copiedRequest = request + return session.dataTask(with: copiedRequest) + } + + /// Called to update the `downloadProgress` of the instance. + func updateDownloadProgress() { + let totalBytesReceived = Int64(data?.count ?? 0) + let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown + + downloadProgress.totalUnitCount = totalBytesExpected + downloadProgress.completedUnitCount = totalBytesReceived + + downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } + } + + /// Validates the request, using the specified closure. + /// + /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter validation: `Validation` closure used to validate the response. + /// + /// - Returns: The instance. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard error == nil, let response else { return } + + let result = validation(request, response, data) + + if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) } + + eventMonitor?.request(self, + didValidateRequest: request, + response: response, + data: data, + withResult: result) + } + + validators.write { $0.append(validator) } + + return self + } + + /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse` and providing a completion + /// handler to return a `ResponseDisposition` value. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. + /// - handler: Closure called when the instance produces an `HTTPURLResponse`. The `completionHandler` provided + /// MUST be called, otherwise the request will never complete. + /// + /// - Returns: The instance. + @_disfavoredOverload + @discardableResult + public func onHTTPResponse( + on queue: DispatchQueue = .main, + perform handler: @escaping (_ response: HTTPURLResponse, + _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void + ) -> Self { + dataMutableState.write { mutableState in + mutableState.httpResponseHandler = (queue, handler) + } + + return self + } + + /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse`. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. + /// - handler: Closure called when the instance produces an `HTTPURLResponse`. + /// + /// - Returns: The instance. + @discardableResult + public func onHTTPResponse(on queue: DispatchQueue = .main, + perform handler: @escaping (HTTPURLResponse) -> Void) -> Self { + onHTTPResponse(on: queue) { response, completionHandler in + handler(response) + completionHandler(.allow) + } + + return self + } + + // MARK: Response Serialization + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let result = AFResult(value: self.data, error: self.error) + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: 0, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + } + } + + return self + } + + private func _response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDataResponse) -> Void) + -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let start = ProcessInfo.processInfo.systemUptime + let result: AFResult = Result { + try responseSerializer.serialize(request: self.request, + response: self.response, + data: self.data, + error: self.error) + }.mapError { error in + error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) + } + + let end = ProcessInfo.processInfo.systemUptime + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + guard !self.isCancelled, let serializerError = result.failure, let delegate = self.delegate else { + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + return + } + + delegate.retryResult(for: self, dueTo: serializerError) { retryResult in + var didComplete: (() -> Void)? + + defer { + if let didComplete { + self.responseSerializerDidComplete { queue.async { didComplete() } } + } + } + + switch retryResult { + case .doNotRetry: + didComplete = { completionHandler(response) } + + case let .doNotRetryWithError(retryError): + let result: AFResult = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError")) + + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + didComplete = { completionHandler(response) } + + case .retry, .retryWithDelay: + delegate.retryRequest(self, withDelay: retryResult.delay) + } + } + } + } + + return self + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDataResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDataResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } + + /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is called. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseData(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } + + /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseString(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } + + /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments` + /// by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") + @discardableResult + public func responseJSON(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, + options: JSONSerialization.ReadingOptions = .allowFragments, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods, + options: options), + completionHandler: completionHandler) + } + + /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseDecodable(of type: T.Type = T.self, + queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} diff --git a/Source/Core/DataStreamRequest.swift b/Source/Core/DataStreamRequest.swift new file mode 100644 index 000000000..fd11ca004 --- /dev/null +++ b/Source/Core/DataStreamRequest.swift @@ -0,0 +1,584 @@ +// +// DataStreamRequest.swift +// +// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `Request` subclass which streams HTTP response `Data` through a `Handler` closure. +public final class DataStreamRequest: Request { + /// Closure type handling `DataStreamRequest.Stream` values. + public typealias Handler = (Stream) throws -> Void + + /// Type encapsulating an `Event` as it flows through the stream, as well as a `CancellationToken` which can be used + /// to stop the stream at any time. + public struct Stream { + /// Latest `Event` from the stream. + public let event: Event + /// Token used to cancel the stream. + public let token: CancellationToken + + /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. + public func cancel() { + token.cancel() + } + } + + /// Type representing an event flowing through the stream. Contains either the `Result` of processing streamed + /// `Data` or the completion of the stream. + public enum Event { + /// Output produced every time the instance receives additional `Data`. The associated value contains the + /// `Result` of processing the incoming `Data`. + case stream(Result) + /// Output produced when the instance has completed, whether due to stream end, cancellation, or an error. + /// Associated `Completion` value contains the final state. + case complete(Completion) + } + + /// Value containing the state of a `DataStreamRequest` when the stream was completed. + public struct Completion { + /// Last `URLRequest` issued by the instance. + public let request: URLRequest? + /// Last `HTTPURLResponse` received by the instance. + public let response: HTTPURLResponse? + /// Last `URLSessionTaskMetrics` produced for the instance. + public let metrics: URLSessionTaskMetrics? + /// `AFError` produced for the instance, if any. + public let error: AFError? + } + + /// Type used to cancel an ongoing stream. + public struct CancellationToken { + weak var request: DataStreamRequest? + + init(_ request: DataStreamRequest) { + self.request = request + } + + /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. + public func cancel() { + request?.cancel() + } + } + + /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. + public let convertible: URLRequestConvertible + /// Whether or not the instance will be cancelled if stream parsing encounters an error. + public let automaticallyCancelOnStreamError: Bool + + /// Internal mutable state specific to this type. + struct StreamMutableState { + /// `OutputStream` bound to the `InputStream` produced by `asInputStream`, if it has been called. + var outputStream: OutputStream? + /// Stream closures called as `Data` is received. + var streams: [(_ data: Data) -> Void] = [] + /// Number of currently executing streams. Used to ensure completions are only fired after all streams are + /// enqueued. + var numberOfExecutingStreams = 0 + /// Completion calls enqueued while streams are still executing. + var enqueuedCompletionEvents: [() -> Void] = [] + /// Handler for any `HTTPURLResponse`s received. + var httpResponseHandler: (queue: DispatchQueue, + handler: (_ response: HTTPURLResponse, + _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void)? + } + + let streamMutableState = Protected(StreamMutableState()) + + /// Creates a `DataStreamRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` + /// by default. + /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this + /// instance. + /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance will be cancelled when an `Error` + /// is thrown while serializing stream `Data`. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default + /// targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by + /// the `Request`. + init(id: UUID = UUID(), + convertible: URLRequestConvertible, + automaticallyCancelOnStreamError: Bool, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.convertible = convertible + self.automaticallyCancelOnStreamError = automaticallyCancelOnStreamError + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + let copiedRequest = request + return session.dataTask(with: copiedRequest) + } + + override func finish(error: AFError? = nil) { + streamMutableState.write { state in + state.outputStream?.close() + } + + super.finish(error: error) + } + + func didReceive(data: Data) { + streamMutableState.write { state in + #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. + if let stream = state.outputStream { + underlyingQueue.async { + var bytes = Array(data) + stream.write(&bytes, maxLength: bytes.count) + } + } + #endif + state.numberOfExecutingStreams += state.streams.count + let localState = state + underlyingQueue.async { localState.streams.forEach { $0(data) } } + } + } + + func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + streamMutableState.read { dataMutableState in + guard let httpResponseHandler = dataMutableState.httpResponseHandler else { + underlyingQueue.async { completionHandler(.allow) } + return + } + + httpResponseHandler.queue.async { + httpResponseHandler.handler(response) { disposition in + if disposition == .cancel { + self.mutableState.write { mutableState in + mutableState.state = .cancelled + mutableState.error = mutableState.error ?? AFError.explicitlyCancelled + } + } + + self.underlyingQueue.async { + completionHandler(disposition.sessionDisposition) + } + } + } + } + } + + /// Validates the `URLRequest` and `HTTPURLResponse` received for the instance using the provided `Validation` closure. + /// + /// - Parameter validation: `Validation` closure used to validate the request and response. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard error == nil, let response else { return } + + let result = validation(request, response) + + if case let .failure(error) = result { + self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) + } + + eventMonitor?.request(self, + didValidateRequest: request, + response: response, + withResult: result) + } + + validators.write { $0.append(validator) } + + return self + } + + #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. + /// Produces an `InputStream` that receives the `Data` received by the instance. + /// + /// - Note: The `InputStream` produced by this method must have `open()` called before being able to read `Data`. + /// Additionally, this method will automatically call `resume()` on the instance, regardless of whether or + /// not the creating session has `startRequestsImmediately` set to `true`. + /// + /// - Parameter bufferSize: Size, in bytes, of the buffer between the `OutputStream` and `InputStream`. + /// + /// - Returns: The `InputStream` bound to the internal `OutboundStream`. + public func asInputStream(bufferSize: Int = 1024) -> InputStream? { + defer { resume() } + + var inputStream: InputStream? + streamMutableState.write { state in + Foundation.Stream.getBoundStreams(withBufferSize: bufferSize, + inputStream: &inputStream, + outputStream: &state.outputStream) + state.outputStream?.open() + } + + return inputStream + } + #endif + + /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse` and providing a completion + /// handler to return a `ResponseDisposition` value. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. + /// - handler: Closure called when the instance produces an `HTTPURLResponse`. The `completionHandler` provided + /// MUST be called, otherwise the request will never complete. + /// + /// - Returns: The instance. + @_disfavoredOverload + @discardableResult + public func onHTTPResponse( + on queue: DispatchQueue = .main, + perform handler: @escaping (_ response: HTTPURLResponse, + _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void + ) -> Self { + streamMutableState.write { mutableState in + mutableState.httpResponseHandler = (queue, handler) + } + + return self + } + + /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse`. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. + /// - handler: Closure called when the instance produces an `HTTPURLResponse`. + /// + /// - Returns: The instance. + @discardableResult + public func onHTTPResponse(on queue: DispatchQueue = .main, + perform handler: @escaping (HTTPURLResponse) -> Void) -> Self { + onHTTPResponse(on: queue) { response, completionHandler in + handler(response) + completionHandler(.allow) + } + + return self + } + + func capturingError(from closure: () throws -> Void) { + do { + try closure() + } catch { + self.error = error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) + cancel() + } + } + + func appendStreamCompletion(on queue: DispatchQueue, + stream: @escaping Handler) { + appendResponseSerializer { + self.underlyingQueue.async { + self.responseSerializerDidComplete { + self.streamMutableState.write { state in + guard state.numberOfExecutingStreams == 0 else { + state.enqueuedCompletionEvents.append { + self.enqueueCompletion(on: queue, stream: stream) + } + + return + } + + self.enqueueCompletion(on: queue, stream: stream) + } + } + } + } + } + + func enqueueCompletion(on queue: DispatchQueue, + stream: @escaping Handler) { + queue.async { + do { + let completion = Completion(request: self.request, + response: self.response, + metrics: self.metrics, + error: self.error) + try stream(.init(event: .complete(completion), token: .init(self))) + } catch { + // Ignore error, as errors on Completion can't be handled anyway. + } + } + } + + // MARK: Response Serialization + + /// Adds a `StreamHandler` which performs no parsing on incoming `Data`. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStream(on queue: DispatchQueue = .main, stream: @escaping Handler) -> Self { + let parser = { [unowned self] (data: Data) in + queue.async { + self.capturingError { + try stream(.init(event: .stream(.success(data)), token: .init(self))) + } + + self.updateAndCompleteIfPossible() + } + } + + streamMutableState.write { $0.streams.append(parser) } + appendStreamCompletion(on: queue, stream: stream) + + return self + } + + /// Adds a `StreamHandler` which uses the provided `DataStreamSerializer` to process incoming `Data`. + /// + /// - Parameters: + /// - serializer: `DataStreamSerializer` used to process incoming `Data`. Its work is done on the `serializationQueue`. + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStream(using serializer: Serializer, + on queue: DispatchQueue = .main, + stream: @escaping Handler) -> Self { + let parser = { [unowned self] (data: Data) in + serializationQueue.async { + // Start work on serialization queue. + let result = Result { try serializer.serialize(data) } + .mapError { $0.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: $0))) } + // End work on serialization queue. + self.underlyingQueue.async { + self.eventMonitor?.request(self, didParseStream: result) + + if result.isFailure, self.automaticallyCancelOnStreamError { + self.cancel() + } + + queue.async { + self.capturingError { + try stream(.init(event: .stream(result), token: .init(self))) + } + + self.updateAndCompleteIfPossible() + } + } + } + } + + streamMutableState.write { $0.streams.append(parser) } + appendStreamCompletion(on: queue, stream: stream) + + return self + } + + /// Adds a `StreamHandler` which parses incoming `Data` as a UTF8 `String`. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStreamString(on queue: DispatchQueue = .main, + stream: @escaping Handler) -> Self { + let parser = { [unowned self] (data: Data) in + serializationQueue.async { + // Start work on serialization queue. + let string = String(decoding: data, as: UTF8.self) + // End work on serialization queue. + self.underlyingQueue.async { + self.eventMonitor?.request(self, didParseStream: .success(string)) + + queue.async { + self.capturingError { + try stream(.init(event: .stream(.success(string)), token: .init(self))) + } + + self.updateAndCompleteIfPossible() + } + } + } + } + + streamMutableState.write { $0.streams.append(parser) } + appendStreamCompletion(on: queue, stream: stream) + + return self + } + + private func updateAndCompleteIfPossible() { + streamMutableState.write { state in + state.numberOfExecutingStreams -= 1 + + guard state.numberOfExecutingStreams == 0, !state.enqueuedCompletionEvents.isEmpty else { return } + + let completionEvents = state.enqueuedCompletionEvents + self.underlyingQueue.async { completionEvents.forEach { $0() } } + state.enqueuedCompletionEvents.removeAll() + } + } + + /// Adds a `StreamHandler` which parses incoming `Data` using the provided `DataDecoder`. + /// + /// - Parameters: + /// - type: `Decodable` type to parse incoming `Data` into. + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - decoder: `DataDecoder` used to decode the incoming `Data`. + /// - preprocessor: `DataPreprocessor` used to process the incoming `Data` before it's passed to the `decoder`. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStreamDecodable(of type: T.Type = T.self, + on queue: DispatchQueue = .main, + using decoder: DataDecoder = JSONDecoder(), + preprocessor: DataPreprocessor = PassthroughPreprocessor(), + stream: @escaping Handler) -> Self { + responseStream(using: DecodableStreamSerializer(decoder: decoder, dataPreprocessor: preprocessor), + stream: stream) + } +} + +extension DataStreamRequest.Stream { + /// Incoming `Result` values from `Event.stream`. + public var result: Result? { + guard case let .stream(result) = event else { return nil } + + return result + } + + /// `Success` value of the instance, if any. + public var value: Success? { + guard case let .success(value) = result else { return nil } + + return value + } + + /// `Failure` value of the instance, if any. + public var error: Failure? { + guard case let .failure(error) = result else { return nil } + + return error + } + + /// `Completion` value of the instance, if any. + public var completion: DataStreamRequest.Completion? { + guard case let .complete(completion) = event else { return nil } + + return completion + } +} + +// MARK: - Serialization + +/// A type which can serialize incoming `Data`. +public protocol DataStreamSerializer { + /// Type produced from the serialized `Data`. + associatedtype SerializedObject + + /// Serializes incoming `Data` into a `SerializedObject` value. + /// + /// - Parameter data: `Data` to be serialized. + /// + /// - Throws: Any error produced during serialization. + func serialize(_ data: Data) throws -> SerializedObject +} + +/// `DataStreamSerializer` which uses the provided `DataPreprocessor` and `DataDecoder` to serialize the incoming `Data`. +public struct DecodableStreamSerializer: DataStreamSerializer { + /// `DataDecoder` used to decode incoming `Data`. + public let decoder: DataDecoder + /// `DataPreprocessor` incoming `Data` is passed through before being passed to the `DataDecoder`. + public let dataPreprocessor: DataPreprocessor + + /// Creates an instance with the provided `DataDecoder` and `DataPreprocessor`. + /// - Parameters: + /// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default. + /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the + /// `decoder`. `PassthroughPreprocessor()` by default. + public init(decoder: DataDecoder = JSONDecoder(), dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) { + self.decoder = decoder + self.dataPreprocessor = dataPreprocessor + } + + public func serialize(_ data: Data) throws -> T { + let processedData = try dataPreprocessor.preprocess(data) + do { + return try decoder.decode(T.self, from: processedData) + } catch { + throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) + } + } +} + +/// `DataStreamSerializer` which performs no serialization on incoming `Data`. +public struct PassthroughStreamSerializer: DataStreamSerializer { + /// Creates an instance. + public init() {} + + public func serialize(_ data: Data) throws -> Data { data } +} + +/// `DataStreamSerializer` which serializes incoming stream `Data` into `UTF8`-decoded `String` values. +public struct StringStreamSerializer: DataStreamSerializer { + /// Creates an instance. + public init() {} + + public func serialize(_ data: Data) throws -> String { + String(decoding: data, as: UTF8.self) + } +} + +extension DataStreamSerializer { + /// Creates a `DecodableStreamSerializer` instance with the provided `DataDecoder` and `DataPreprocessor`. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from stream data. + /// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default. + /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the + /// `decoder`. `PassthroughPreprocessor()` by default. + public static func decodable(of type: T.Type, + decoder: DataDecoder = JSONDecoder(), + dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) -> Self where Self == DecodableStreamSerializer { + DecodableStreamSerializer(decoder: decoder, dataPreprocessor: dataPreprocessor) + } +} + +extension DataStreamSerializer where Self == PassthroughStreamSerializer { + /// Provides a `PassthroughStreamSerializer` instance. + public static var passthrough: PassthroughStreamSerializer { PassthroughStreamSerializer() } +} + +extension DataStreamSerializer where Self == StringStreamSerializer { + /// Provides a `StringStreamSerializer` instance. + public static var string: StringStreamSerializer { StringStreamSerializer() } +} diff --git a/Source/Core/DownloadRequest.swift b/Source/Core/DownloadRequest.swift new file mode 100644 index 000000000..556c43eae --- /dev/null +++ b/Source/Core/DownloadRequest.swift @@ -0,0 +1,588 @@ +// +// DownloadRequest.swift +// +// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `Request` subclass which downloads `Data` to a file on disk using `URLSessionDownloadTask`. +public final class DownloadRequest: Request { + /// A set of options to be executed prior to moving a downloaded file from the temporary `URL` to the destination + /// `URL`. + public struct Options: OptionSet { + /// Specifies that intermediate directories for the destination URL should be created. + public static let createIntermediateDirectories = Options(rawValue: 1 << 0) + /// Specifies that any previous file at the destination `URL` should be removed. + public static let removePreviousFile = Options(rawValue: 1 << 1) + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + } + + // MARK: Destination + + /// A closure executed once a `DownloadRequest` has successfully completed in order to determine where to move the + /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL + /// and the `HTTPURLResponse`, and returns two values: the file URL where the temporary file should be moved and + /// the options defining how the file should be moved. + /// + /// - Note: Downloads from a local `file://` `URL`s do not use the `Destination` closure, as those downloads do not + /// return an `HTTPURLResponse`. Instead the file is merely moved within the temporary directory. + public typealias Destination = (_ temporaryURL: URL, + _ response: HTTPURLResponse) -> (destinationURL: URL, options: Options) + + /// Creates a download file destination closure which uses the default file manager to move the temporary file to a + /// file URL in the first available directory with the specified search path directory and search path domain mask. + /// + /// - Parameters: + /// - directory: The search path directory. `.documentDirectory` by default. + /// - domain: The search path domain mask. `.userDomainMask` by default. + /// - options: `DownloadRequest.Options` used when moving the downloaded file to its destination. None by + /// default. + /// - Returns: The `Destination` closure. + public class func suggestedDownloadDestination(for directory: FileManager.SearchPathDirectory = .documentDirectory, + in domain: FileManager.SearchPathDomainMask = .userDomainMask, + options: Options = []) -> Destination { + { temporaryURL, response in + let directoryURLs = FileManager.default.urls(for: directory, in: domain) + let url = directoryURLs.first?.appendingPathComponent(response.suggestedFilename!) ?? temporaryURL + + return (url, options) + } + } + + /// Default `Destination` used by Alamofire to ensure all downloads persist. This `Destination` prepends + /// `Alamofire_` to the automatically generated download name and moves it within the temporary directory. Files + /// with this destination must be additionally moved if they should survive the system reclamation of temporary + /// space. + static let defaultDestination: Destination = { url, _ in + (defaultDestinationURL(url), []) + } + + /// Default `URL` creation closure. Creates a `URL` in the temporary directory with `Alamofire_` prepended to the + /// provided file name. + static let defaultDestinationURL: (URL) -> URL = { url in + let filename = "Alamofire_\(url.lastPathComponent)" + let destination = url.deletingLastPathComponent().appendingPathComponent(filename) + + return destination + } + + // MARK: Downloadable + + /// Type describing the source used to create the underlying `URLSessionDownloadTask`. + public enum Downloadable { + /// Download should be started from the `URLRequest` produced by the associated `URLRequestConvertible` value. + case request(URLRequestConvertible) + /// Download should be started from the associated resume `Data` value. + case resumeData(Data) + } + + // MARK: Mutable State + + /// Type containing all mutable state for `DownloadRequest` instances. + private struct DownloadRequestMutableState { + /// Possible resume `Data` produced when cancelling the instance. + var resumeData: Data? + /// `URL` to which `Data` is being downloaded. + var fileURL: URL? + } + + /// Protected mutable state specific to `DownloadRequest`. + private let mutableDownloadState = Protected(DownloadRequestMutableState()) + + /// If the download is resumable and is eventually cancelled or fails, this value may be used to resume the download + /// using the `download(resumingWith data:)` API. + /// + /// - Note: For more information about `resumeData`, see [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel). + public var resumeData: Data? { + #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. + return mutableDownloadState.resumeData ?? error?.downloadResumeData + #else + return mutableDownloadState.resumeData + #endif + } + + /// If the download is successful, the `URL` where the file was downloaded. + public var fileURL: URL? { mutableDownloadState.fileURL } + + // MARK: Initial State + + /// `Downloadable` value used for this instance. + public let downloadable: Downloadable + /// The `Destination` to which the downloaded file is moved. + let destination: Destination + + /// Creates a `DownloadRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - downloadable: `Downloadable` value used to create `URLSessionDownloadTasks` for the instance. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request` + /// - destination: `Destination` closure used to move the downloaded file to its final location. + init(id: UUID = UUID(), + downloadable: Downloadable, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate, + destination: @escaping Destination) { + self.downloadable = downloadable + self.destination = destination + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func reset() { + super.reset() + + mutableDownloadState.write { + $0.resumeData = nil + $0.fileURL = nil + } + } + + /// Called when a download has finished. + /// + /// - Parameters: + /// - task: `URLSessionTask` that finished the download. + /// - result: `Result` of the automatic move to `destination`. + func didFinishDownloading(using task: URLSessionTask, with result: Result) { + eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result) + + switch result { + case let .success(url): mutableDownloadState.fileURL = url + case let .failure(error): self.error = error + } + } + + /// Updates the `downloadProgress` using the provided values. + /// + /// - Parameters: + /// - bytesWritten: Total bytes written so far. + /// - totalBytesExpectedToWrite: Total bytes expected to write. + func updateDownloadProgress(bytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + downloadProgress.totalUnitCount = totalBytesExpectedToWrite + downloadProgress.completedUnitCount += bytesWritten + + downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + session.downloadTask(with: request) + } + + /// Creates a `URLSessionTask` from the provided resume data. + /// + /// - Parameters: + /// - data: `Data` used to resume the download. + /// - session: `URLSession` used to create the `URLSessionTask`. + /// + /// - Returns: The `URLSessionTask` created. + public func task(forResumeData data: Data, using session: URLSession) -> URLSessionTask { + session.downloadTask(withResumeData: data) + } + + /// Cancels the instance. Once cancelled, a `DownloadRequest` can no longer be resumed or suspended. + /// + /// - Note: This method will NOT produce resume data. If you wish to cancel and produce resume data, use + /// `cancel(producingResumeData:)` or `cancel(byProducingResumeData:)`. + /// + /// - Returns: The instance. + @discardableResult + override public func cancel() -> Self { + cancel(producingResumeData: false) + } + + /// Cancels the instance, optionally producing resume data. Once cancelled, a `DownloadRequest` can no longer be + /// resumed or suspended. + /// + /// - Note: If `producingResumeData` is `true`, the `resumeData` property will be populated with any resume data, if + /// available. + /// + /// - Returns: The instance. + @discardableResult + public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self { + cancel(optionallyProducingResumeData: shouldProduceResumeData ? { _ in } : nil) + } + + /// Cancels the instance while producing resume data. Once cancelled, a `DownloadRequest` can no longer be resumed + /// or suspended. + /// + /// - Note: The resume data passed to the completion handler will also be available on the instance's `resumeData` + /// property. + /// + /// - Parameter completionHandler: The completion handler that is called when the download has been successfully + /// cancelled. It is not guaranteed to be called on a particular queue, so you may + /// want use an appropriate queue to perform your work. + /// + /// - Returns: The instance. + @discardableResult + public func cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void) -> Self { + cancel(optionallyProducingResumeData: completionHandler) + } + + /// Internal implementation of cancellation that optionally takes a resume data handler. If no handler is passed, + /// cancellation is performed without producing resume data. + /// + /// - Parameter completionHandler: Optional resume data handler. + /// + /// - Returns: The instance. + private func cancel(optionallyProducingResumeData completionHandler: ((_ resumeData: Data?) -> Void)?) -> Self { + mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.cancelled) else { return } + + mutableState.state = .cancelled + + underlyingQueue.async { self.didCancel() } + + guard let task = mutableState.tasks.last as? URLSessionDownloadTask, task.state != .completed else { + underlyingQueue.async { self.finish() } + return + } + + if let completionHandler { + // Resume to ensure metrics are gathered. + task.resume() + task.cancel { resumeData in + self.mutableDownloadState.resumeData = resumeData + self.underlyingQueue.async { self.didCancelTask(task) } + completionHandler(resumeData) + } + } else { + // Resume to ensure metrics are gathered. + task.resume() + task.cancel() + self.underlyingQueue.async { self.didCancelTask(task) } + } + } + + return self + } + + /// Validates the request, using the specified closure. + /// + /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter validation: `Validation` closure to validate the response. + /// + /// - Returns: The instance. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard error == nil, let response else { return } + + let result = validation(request, response, fileURL) + + if case let .failure(error) = result { + self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) + } + + eventMonitor?.request(self, + didValidateRequest: request, + response: response, + fileURL: fileURL, + withResult: result) + } + + validators.write { $0.append(validator) } + + return self + } + + // MARK: - Response Serialization + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let result = AFResult(value: self.fileURL, error: self.error) + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: 0, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + } + } + + return self + } + + private func _response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let start = ProcessInfo.processInfo.systemUptime + let result: AFResult = Result { + try responseSerializer.serializeDownload(request: self.request, + response: self.response, + fileURL: self.fileURL, + error: self.error) + }.mapError { error in + error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) + } + let end = ProcessInfo.processInfo.systemUptime + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + guard let serializerError = result.failure, let delegate = self.delegate else { + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + return + } + + delegate.retryResult(for: self, dueTo: serializerError) { retryResult in + var didComplete: (() -> Void)? + + defer { + if let didComplete { + self.responseSerializerDidComplete { queue.async { didComplete() } } + } + } + + switch retryResult { + case .doNotRetry: + didComplete = { completionHandler(response) } + + case let .doNotRetryWithError(retryError): + let result: AFResult = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError")) + + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + didComplete = { completionHandler(response) } + + case .retry, .retryWithDelay: + delegate.retryRequest(self, withDelay: retryResult.delay) + } + } + } + } + + return self + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data + /// contained in the destination `URL`. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data + /// contained in the destination `URL`. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } + + /// Adds a handler using a `URLResponseSerializer` to be called once the request is finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is called. `.main` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseURL(queue: DispatchQueue = .main, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, responseSerializer: URLResponseSerializer(), completionHandler: completionHandler) + } + + /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is called. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseData(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } + + /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseString(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } + + /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments` + /// by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") + @discardableResult + public func responseJSON(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, + options: JSONSerialization.ReadingOptions = .allowFragments, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods, + options: options), + completionHandler: completionHandler) + } + + /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseDecodable(of type: T.Type = T.self, + queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} diff --git a/Source/HTTPHeaders.swift b/Source/Core/HTTPHeaders.swift similarity index 98% rename from Source/HTTPHeaders.swift rename to Source/Core/HTTPHeaders.swift index 969fbdb88..40073ffcb 100644 --- a/Source/HTTPHeaders.swift +++ b/Source/Core/HTTPHeaders.swift @@ -402,7 +402,13 @@ extension HTTPHeader { #elseif os(tvOS) return "tvOS" #elseif os(macOS) + #if targetEnvironment(macCatalyst) + return "macOS(Catalyst)" + #else return "macOS" + #endif + #elseif swift(>=5.9.2) && os(visionOS) + return "visionOS" #elseif os(Linux) return "Linux" #elseif os(Windows) @@ -417,7 +423,7 @@ extension HTTPHeader { return "\(osName) \(versionString)" }() - let alamofireVersion = "Alamofire/\(version)" + let alamofireVersion = "Alamofire/\(AFInfo.version)" let userAgent = "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)" diff --git a/Source/HTTPMethod.swift b/Source/Core/HTTPMethod.swift similarity index 100% rename from Source/HTTPMethod.swift rename to Source/Core/HTTPMethod.swift diff --git a/Source/Notifications.swift b/Source/Core/Notifications.swift similarity index 100% rename from Source/Notifications.swift rename to Source/Core/Notifications.swift diff --git a/Source/ParameterEncoder.swift b/Source/Core/ParameterEncoder.swift similarity index 100% rename from Source/ParameterEncoder.swift rename to Source/Core/ParameterEncoder.swift diff --git a/Source/ParameterEncoding.swift b/Source/Core/ParameterEncoding.swift similarity index 100% rename from Source/ParameterEncoding.swift rename to Source/Core/ParameterEncoding.swift diff --git a/Source/Protected.swift b/Source/Core/Protected.swift similarity index 100% rename from Source/Protected.swift rename to Source/Core/Protected.swift diff --git a/Source/Core/Request.swift b/Source/Core/Request.swift new file mode 100644 index 000000000..13c0dcd7b --- /dev/null +++ b/Source/Core/Request.swift @@ -0,0 +1,1090 @@ +// +// Request.swift +// +// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `Request` is the common superclass of all Alamofire request types and provides common state, delegate, and callback +/// handling. +public class Request { + /// State of the `Request`, with managed transitions between states set when calling `resume()`, `suspend()`, or + /// `cancel()` on the `Request`. + public enum State { + /// Initial state of the `Request`. + case initialized + /// `State` set when `resume()` is called. Any tasks created for the `Request` will have `resume()` called on + /// them in this state. + case resumed + /// `State` set when `suspend()` is called. Any tasks created for the `Request` will have `suspend()` called on + /// them in this state. + case suspended + /// `State` set when `cancel()` is called. Any tasks created for the `Request` will have `cancel()` called on + /// them. Unlike `resumed` or `suspended`, once in the `cancelled` state, the `Request` can no longer transition + /// to any other state. + case cancelled + /// `State` set when all response serialization completion closures have been cleared on the `Request` and + /// enqueued on their respective queues. + case finished + + /// Determines whether `self` can be transitioned to the provided `State`. + func canTransitionTo(_ state: State) -> Bool { + switch (self, state) { + case (.initialized, _): + return true + case (_, .initialized), (.cancelled, _), (.finished, _): + return false + case (.resumed, .cancelled), (.suspended, .cancelled), (.resumed, .suspended), (.suspended, .resumed): + return true + case (.suspended, .suspended), (.resumed, .resumed): + return false + case (_, .finished): + return true + } + } + } + + // MARK: - Initial State + + /// `UUID` providing a unique identifier for the `Request`, used in the `Hashable` and `Equatable` conformances. + public let id: UUID + /// The serial queue for all internal async actions. + public let underlyingQueue: DispatchQueue + /// The queue used for all serialization actions. By default it's a serial queue that targets `underlyingQueue`. + public let serializationQueue: DispatchQueue + /// `EventMonitor` used for event callbacks. + public let eventMonitor: EventMonitor? + /// The `Request`'s interceptor. + public let interceptor: RequestInterceptor? + /// The `Request`'s delegate. + public private(set) weak var delegate: RequestDelegate? + + // MARK: - Mutable State + + /// Type encapsulating all mutable state that may need to be accessed from anything other than the `underlyingQueue`. + struct MutableState { + /// State of the `Request`. + var state: State = .initialized + /// `ProgressHandler` and `DispatchQueue` provided for upload progress callbacks. + var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? + /// `ProgressHandler` and `DispatchQueue` provided for download progress callbacks. + var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? + /// `RedirectHandler` provided for to handle request redirection. + var redirectHandler: RedirectHandler? + /// `CachedResponseHandler` provided to handle response caching. + var cachedResponseHandler: CachedResponseHandler? + /// Queue and closure called when the `Request` is able to create a cURL description of itself. + var cURLHandler: (queue: DispatchQueue, handler: (String) -> Void)? + /// Queue and closure called when the `Request` creates a `URLRequest`. + var urlRequestHandler: (queue: DispatchQueue, handler: (URLRequest) -> Void)? + /// Queue and closure called when the `Request` creates a `URLSessionTask`. + var urlSessionTaskHandler: (queue: DispatchQueue, handler: (URLSessionTask) -> Void)? + /// Response serialization closures that handle response parsing. + var responseSerializers: [() -> Void] = [] + /// Response serialization completion closures executed once all response serializers are complete. + var responseSerializerCompletions: [() -> Void] = [] + /// Whether response serializer processing is finished. + var responseSerializerProcessingFinished = false + /// `URLCredential` used for authentication challenges. + var credential: URLCredential? + /// All `URLRequest`s created by Alamofire on behalf of the `Request`. + var requests: [URLRequest] = [] + /// All `URLSessionTask`s created by Alamofire on behalf of the `Request`. + var tasks: [URLSessionTask] = [] + /// All `URLSessionTaskMetrics` values gathered by Alamofire on behalf of the `Request`. Should correspond + /// exactly the the `tasks` created. + var metrics: [URLSessionTaskMetrics] = [] + /// Number of times any retriers provided retried the `Request`. + var retryCount = 0 + /// Final `AFError` for the `Request`, whether from various internal Alamofire calls or as a result of a `task`. + var error: AFError? + /// Whether the instance has had `finish()` called and is running the serializers. Should be replaced with a + /// representation in the state machine in the future. + var isFinishing = false + /// Actions to run when requests are finished. Use for concurrency support. + var finishHandlers: [() -> Void] = [] + } + + /// Protected `MutableState` value that provides thread-safe access to state values. + let mutableState = Protected(MutableState()) + + /// `State` of the `Request`. + public var state: State { mutableState.state } + /// Returns whether `state` is `.initialized`. + public var isInitialized: Bool { state == .initialized } + /// Returns whether `state is `.resumed`. + public var isResumed: Bool { state == .resumed } + /// Returns whether `state` is `.suspended`. + public var isSuspended: Bool { state == .suspended } + /// Returns whether `state` is `.cancelled`. + public var isCancelled: Bool { state == .cancelled } + /// Returns whether `state` is `.finished`. + public var isFinished: Bool { state == .finished } + + // MARK: Progress + + /// Closure type executed when monitoring the upload or download progress of a request. + public typealias ProgressHandler = (Progress) -> Void + + /// `Progress` of the upload of the body of the executed `URLRequest`. Reset to `0` if the `Request` is retried. + public let uploadProgress = Progress(totalUnitCount: 0) + /// `Progress` of the download of any response data. Reset to `0` if the `Request` is retried. + public let downloadProgress = Progress(totalUnitCount: 0) + /// `ProgressHandler` called when `uploadProgress` is updated, on the provided `DispatchQueue`. + public internal(set) var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { + get { mutableState.uploadProgressHandler } + set { mutableState.uploadProgressHandler = newValue } + } + + /// `ProgressHandler` called when `downloadProgress` is updated, on the provided `DispatchQueue`. + public internal(set) var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { + get { mutableState.downloadProgressHandler } + set { mutableState.downloadProgressHandler = newValue } + } + + // MARK: Redirect Handling + + /// `RedirectHandler` set on the instance. + public internal(set) var redirectHandler: RedirectHandler? { + get { mutableState.redirectHandler } + set { mutableState.redirectHandler = newValue } + } + + // MARK: Cached Response Handling + + /// `CachedResponseHandler` set on the instance. + public internal(set) var cachedResponseHandler: CachedResponseHandler? { + get { mutableState.cachedResponseHandler } + set { mutableState.cachedResponseHandler = newValue } + } + + // MARK: URLCredential + + /// `URLCredential` used for authentication challenges. Created by calling one of the `authenticate` methods. + public internal(set) var credential: URLCredential? { + get { mutableState.credential } + set { mutableState.credential = newValue } + } + + // MARK: Validators + + /// `Validator` callback closures that store the validation calls enqueued. + let validators = Protected<[() -> Void]>([]) + + // MARK: URLRequests + + /// All `URLRequests` created on behalf of the `Request`, including original and adapted requests. + public var requests: [URLRequest] { mutableState.requests } + /// First `URLRequest` created on behalf of the `Request`. May not be the first one actually executed. + public var firstRequest: URLRequest? { requests.first } + /// Last `URLRequest` created on behalf of the `Request`. + public var lastRequest: URLRequest? { requests.last } + /// Current `URLRequest` created on behalf of the `Request`. + public var request: URLRequest? { lastRequest } + + /// `URLRequest`s from all of the `URLSessionTask`s executed on behalf of the `Request`. May be different from + /// `requests` due to `URLSession` manipulation. + public var performedRequests: [URLRequest] { mutableState.read { $0.tasks.compactMap(\.currentRequest) } } + + // MARK: HTTPURLResponse + + /// `HTTPURLResponse` received from the server, if any. If the `Request` was retried, this is the response of the + /// last `URLSessionTask`. + public var response: HTTPURLResponse? { lastTask?.response as? HTTPURLResponse } + + // MARK: Tasks + + /// All `URLSessionTask`s created on behalf of the `Request`. + public var tasks: [URLSessionTask] { mutableState.tasks } + /// First `URLSessionTask` created on behalf of the `Request`. + public var firstTask: URLSessionTask? { tasks.first } + /// Last `URLSessionTask` created on behalf of the `Request`. + public var lastTask: URLSessionTask? { tasks.last } + /// Current `URLSessionTask` created on behalf of the `Request`. + public var task: URLSessionTask? { lastTask } + + // MARK: Metrics + + /// All `URLSessionTaskMetrics` gathered on behalf of the `Request`. Should correspond to the `tasks` created. + public var allMetrics: [URLSessionTaskMetrics] { mutableState.metrics } + /// First `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var firstMetrics: URLSessionTaskMetrics? { allMetrics.first } + /// Last `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var lastMetrics: URLSessionTaskMetrics? { allMetrics.last } + /// Current `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var metrics: URLSessionTaskMetrics? { lastMetrics } + + // MARK: Retry Count + + /// Number of times the `Request` has been retried. + public var retryCount: Int { mutableState.retryCount } + + // MARK: Error + + /// `Error` returned from Alamofire internally, from the network request directly, or any validators executed. + public internal(set) var error: AFError? { + get { mutableState.error } + set { mutableState.error = newValue } + } + + /// Default initializer for the `Request` superclass. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + init(id: UUID = UUID(), + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.id = id + self.underlyingQueue = underlyingQueue + self.serializationQueue = serializationQueue + self.eventMonitor = eventMonitor + self.interceptor = interceptor + self.delegate = delegate + } + + // MARK: - Internal Event API + + // All API must be called from underlyingQueue. + + /// Called when an initial `URLRequest` has been created on behalf of the instance. If a `RequestAdapter` is active, + /// the `URLRequest` will be adapted before being issued. + /// + /// - Parameter request: The `URLRequest` created. + func didCreateInitialURLRequest(_ request: URLRequest) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + mutableState.write { $0.requests.append(request) } + + eventMonitor?.request(self, didCreateInitialURLRequest: request) + } + + /// Called when initial `URLRequest` creation has failed, typically through a `URLRequestConvertible`. + /// + /// - Note: Triggers retry. + /// + /// - Parameter error: `AFError` thrown from the failed creation. + func didFailToCreateURLRequest(with error: AFError) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = error + + eventMonitor?.request(self, didFailToCreateURLRequestWithError: error) + + callCURLHandlerIfNecessary() + + retryOrFinish(error: error) + } + + /// Called when a `RequestAdapter` has successfully adapted a `URLRequest`. + /// + /// - Parameters: + /// - initialRequest: The `URLRequest` that was adapted. + /// - adaptedRequest: The `URLRequest` returned by the `RequestAdapter`. + func didAdaptInitialRequest(_ initialRequest: URLRequest, to adaptedRequest: URLRequest) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + mutableState.write { $0.requests.append(adaptedRequest) } + + eventMonitor?.request(self, didAdaptInitialRequest: initialRequest, to: adaptedRequest) + } + + /// Called when a `RequestAdapter` fails to adapt a `URLRequest`. + /// + /// - Note: Triggers retry. + /// + /// - Parameters: + /// - request: The `URLRequest` the adapter was called with. + /// - error: The `AFError` returned by the `RequestAdapter`. + func didFailToAdaptURLRequest(_ request: URLRequest, withError error: AFError) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = error + + eventMonitor?.request(self, didFailToAdaptURLRequest: request, withError: error) + + callCURLHandlerIfNecessary() + + retryOrFinish(error: error) + } + + /// Final `URLRequest` has been created for the instance. + /// + /// - Parameter request: The `URLRequest` created. + func didCreateURLRequest(_ request: URLRequest) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + mutableState.read { state in + state.urlRequestHandler?.queue.async { state.urlRequestHandler?.handler(request) } + } + + eventMonitor?.request(self, didCreateURLRequest: request) + + callCURLHandlerIfNecessary() + } + + /// Asynchronously calls any stored `cURLHandler` and then removes it from `mutableState`. + private func callCURLHandlerIfNecessary() { + mutableState.write { mutableState in + guard let cURLHandler = mutableState.cURLHandler else { return } + + cURLHandler.queue.async { cURLHandler.handler(self.cURLDescription()) } + + mutableState.cURLHandler = nil + } + } + + /// Called when a `URLSessionTask` is created on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` created. + func didCreateTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + mutableState.write { state in + state.tasks.append(task) + + guard let urlSessionTaskHandler = state.urlSessionTaskHandler else { return } + + urlSessionTaskHandler.queue.async { urlSessionTaskHandler.handler(task) } + } + + eventMonitor?.request(self, didCreateTask: task) + } + + /// Called when resumption is completed. + func didResume() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.requestDidResume(self) + } + + /// Called when a `URLSessionTask` is resumed on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` resumed. + func didResumeTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.request(self, didResumeTask: task) + } + + /// Called when suspension is completed. + func didSuspend() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.requestDidSuspend(self) + } + + /// Called when a `URLSessionTask` is suspended on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` suspended. + func didSuspendTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.request(self, didSuspendTask: task) + } + + /// Called when cancellation is completed, sets `error` to `AFError.explicitlyCancelled`. + func didCancel() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + mutableState.write { mutableState in + mutableState.error = mutableState.error ?? AFError.explicitlyCancelled + } + + eventMonitor?.requestDidCancel(self) + } + + /// Called when a `URLSessionTask` is cancelled on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` cancelled. + func didCancelTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.request(self, didCancelTask: task) + } + + /// Called when a `URLSessionTaskMetrics` value is gathered on behalf of the instance. + /// + /// - Parameter metrics: The `URLSessionTaskMetrics` gathered. + func didGatherMetrics(_ metrics: URLSessionTaskMetrics) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + mutableState.write { $0.metrics.append(metrics) } + + eventMonitor?.request(self, didGatherMetrics: metrics) + } + + /// Called when a `URLSessionTask` fails before it is finished, typically during certificate pinning. + /// + /// - Parameters: + /// - task: The `URLSessionTask` which failed. + /// - error: The early failure `AFError`. + func didFailTask(_ task: URLSessionTask, earlyWithError error: AFError) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = error + + // Task will still complete, so didCompleteTask(_:with:) will handle retry. + eventMonitor?.request(self, didFailTask: task, earlyWithError: error) + } + + /// Called when a `URLSessionTask` completes. All tasks will eventually call this method. + /// + /// - Note: Response validation is synchronously triggered in this step. + /// + /// - Parameters: + /// - task: The `URLSessionTask` which completed. + /// - error: The `AFError` `task` may have completed with. If `error` has already been set on the instance, this + /// value is ignored. + func didCompleteTask(_ task: URLSessionTask, with error: AFError?) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = self.error ?? error + + let validators = validators.read { $0 } + validators.forEach { $0() } + + eventMonitor?.request(self, didCompleteTask: task, with: error) + + retryOrFinish(error: self.error) + } + + /// Called when the `RequestDelegate` is going to retry this `Request`. Calls `reset()`. + func prepareForRetry() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + mutableState.write { $0.retryCount += 1 } + + reset() + + eventMonitor?.requestIsRetrying(self) + } + + /// Called to determine whether retry will be triggered for the particular error, or whether the instance should + /// call `finish()`. + /// + /// - Parameter error: The possible `AFError` which may trigger retry. + func retryOrFinish(error: AFError?) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + guard !isCancelled, let error, let delegate else { finish(); return } + + delegate.retryResult(for: self, dueTo: error) { retryResult in + switch retryResult { + case .doNotRetry: + self.finish() + case let .doNotRetryWithError(retryError): + self.finish(error: retryError.asAFError(orFailWith: "Received retryError was not already AFError")) + case .retry, .retryWithDelay: + delegate.retryRequest(self, withDelay: retryResult.delay) + } + } + } + + /// Finishes this `Request` and starts the response serializers. + /// + /// - Parameter error: The possible `Error` with which the instance will finish. + func finish(error: AFError? = nil) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + guard !mutableState.isFinishing else { return } + + mutableState.isFinishing = true + + if let error { self.error = error } + + // Start response handlers + processNextResponseSerializer() + + eventMonitor?.requestDidFinish(self) + } + + /// Appends the response serialization closure to the instance. + /// + /// - Note: This method will also `resume` the instance if `delegate.startImmediately` returns `true`. + /// + /// - Parameter closure: The closure containing the response serialization call. + func appendResponseSerializer(_ closure: @escaping () -> Void) { + mutableState.write { mutableState in + mutableState.responseSerializers.append(closure) + + if mutableState.state == .finished { + mutableState.state = .resumed + } + + if mutableState.responseSerializerProcessingFinished { + underlyingQueue.async { self.processNextResponseSerializer() } + } + + if mutableState.state.canTransitionTo(.resumed) { + underlyingQueue.async { if self.delegate?.startImmediately == true { self.resume() } } + } + } + } + + /// Returns the next response serializer closure to execute if there's one left. + /// + /// - Returns: The next response serialization closure, if there is one. + func nextResponseSerializer() -> (() -> Void)? { + var responseSerializer: (() -> Void)? + + mutableState.write { mutableState in + let responseSerializerIndex = mutableState.responseSerializerCompletions.count + + if responseSerializerIndex < mutableState.responseSerializers.count { + responseSerializer = mutableState.responseSerializers[responseSerializerIndex] + } + } + + return responseSerializer + } + + /// Processes the next response serializer and calls all completions if response serialization is complete. + func processNextResponseSerializer() { + guard let responseSerializer = nextResponseSerializer() else { + // Execute all response serializer completions and clear them + var completions: [() -> Void] = [] + + mutableState.write { mutableState in + completions = mutableState.responseSerializerCompletions + + // Clear out all response serializers and response serializer completions in mutable state since the + // request is complete. It's important to do this prior to calling the completion closures in case + // the completions call back into the request triggering a re-processing of the response serializers. + // An example of how this can happen is by calling cancel inside a response completion closure. + mutableState.responseSerializers.removeAll() + mutableState.responseSerializerCompletions.removeAll() + + if mutableState.state.canTransitionTo(.finished) { + mutableState.state = .finished + } + + mutableState.responseSerializerProcessingFinished = true + mutableState.isFinishing = false + } + + completions.forEach { $0() } + + // Cleanup the request + cleanup() + + return + } + + serializationQueue.async { responseSerializer() } + } + + /// Notifies the `Request` that the response serializer is complete. + /// + /// - Parameter completion: The completion handler provided with the response serializer, called when all serializers + /// are complete. + func responseSerializerDidComplete(completion: @escaping () -> Void) { + mutableState.write { $0.responseSerializerCompletions.append(completion) } + processNextResponseSerializer() + } + + /// Resets all task and response serializer related state for retry. + func reset() { + error = nil + + uploadProgress.totalUnitCount = 0 + uploadProgress.completedUnitCount = 0 + downloadProgress.totalUnitCount = 0 + downloadProgress.completedUnitCount = 0 + + mutableState.write { state in + state.isFinishing = false + state.responseSerializerCompletions = [] + } + } + + /// Called when updating the upload progress. + /// + /// - Parameters: + /// - totalBytesSent: Total bytes sent so far. + /// - totalBytesExpectedToSend: Total bytes expected to send. + func updateUploadProgress(totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + uploadProgress.totalUnitCount = totalBytesExpectedToSend + uploadProgress.completedUnitCount = totalBytesSent + + uploadProgressHandler?.queue.async { self.uploadProgressHandler?.handler(self.uploadProgress) } + } + + /// Perform a closure on the current `state` while locked. + /// + /// - Parameter perform: The closure to perform. + func withState(perform: (State) -> Void) { + mutableState.withState(perform: perform) + } + + // MARK: Task Creation + + /// Called when creating a `URLSessionTask` for this `Request`. Subclasses must override. + /// + /// - Parameters: + /// - request: `URLRequest` to use to create the `URLSessionTask`. + /// - session: `URLSession` which creates the `URLSessionTask`. + /// + /// - Returns: The `URLSessionTask` created. + func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + fatalError("Subclasses must override.") + } + + // MARK: - Public API + + // These APIs are callable from any queue. + + // MARK: State + + /// Cancels the instance. Once cancelled, a `Request` can no longer be resumed or suspended. + /// + /// - Returns: The instance. + @discardableResult + public func cancel() -> Self { + mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.cancelled) else { return } + + mutableState.state = .cancelled + + underlyingQueue.async { self.didCancel() } + + guard let task = mutableState.tasks.last, task.state != .completed else { + underlyingQueue.async { self.finish() } + return + } + + // Resume to ensure metrics are gathered. + task.resume() + task.cancel() + underlyingQueue.async { self.didCancelTask(task) } + } + + return self + } + + /// Suspends the instance. + /// + /// - Returns: The instance. + @discardableResult + public func suspend() -> Self { + mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.suspended) else { return } + + mutableState.state = .suspended + + underlyingQueue.async { self.didSuspend() } + + guard let task = mutableState.tasks.last, task.state != .completed else { return } + + task.suspend() + underlyingQueue.async { self.didSuspendTask(task) } + } + + return self + } + + /// Resumes the instance. + /// + /// - Returns: The instance. + @discardableResult + public func resume() -> Self { + mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.resumed) else { return } + + mutableState.state = .resumed + + underlyingQueue.async { self.didResume() } + + guard let task = mutableState.tasks.last, task.state != .completed else { return } + + task.resume() + underlyingQueue.async { self.didResumeTask(task) } + } + + return self + } + + // MARK: - Closure API + + /// Associates a credential using the provided values with the instance. + /// + /// - Parameters: + /// - username: The username. + /// - password: The password. + /// - persistence: The `URLCredential.Persistence` for the created `URLCredential`. `.forSession` by default. + /// + /// - Returns: The instance. + @discardableResult + public func authenticate(username: String, password: String, persistence: URLCredential.Persistence = .forSession) -> Self { + let credential = URLCredential(user: username, password: password, persistence: persistence) + + return authenticate(with: credential) + } + + /// Associates the provided credential with the instance. + /// + /// - Parameter credential: The `URLCredential`. + /// + /// - Returns: The instance. + @discardableResult + public func authenticate(with credential: URLCredential) -> Self { + mutableState.credential = credential + + return self + } + + /// Sets a closure to be called periodically during the lifecycle of the instance as data is read from the server. + /// + /// - Note: Only the last closure provided is used. + /// + /// - Parameters: + /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. + /// - closure: The closure to be executed periodically as data is read from the server. + /// + /// - Returns: The instance. + @discardableResult + public func downloadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { + mutableState.downloadProgressHandler = (handler: closure, queue: queue) + + return self + } + + /// Sets a closure to be called periodically during the lifecycle of the instance as data is sent to the server. + /// + /// - Note: Only the last closure provided is used. + /// + /// - Parameters: + /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. + /// - closure: The closure to be executed periodically as data is sent to the server. + /// + /// - Returns: The instance. + @discardableResult + public func uploadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { + mutableState.uploadProgressHandler = (handler: closure, queue: queue) + + return self + } + + // MARK: Redirects + + /// Sets the redirect handler for the instance which will be used if a redirect response is encountered. + /// + /// - Note: Attempting to set the redirect handler more than once is a logic error and will crash. + /// + /// - Parameter handler: The `RedirectHandler`. + /// + /// - Returns: The instance. + @discardableResult + public func redirect(using handler: RedirectHandler) -> Self { + mutableState.write { mutableState in + precondition(mutableState.redirectHandler == nil, "Redirect handler has already been set.") + mutableState.redirectHandler = handler + } + + return self + } + + // MARK: Cached Responses + + /// Sets the cached response handler for the `Request` which will be used when attempting to cache a response. + /// + /// - Note: Attempting to set the cache handler more than once is a logic error and will crash. + /// + /// - Parameter handler: The `CachedResponseHandler`. + /// + /// - Returns: The instance. + @discardableResult + public func cacheResponse(using handler: CachedResponseHandler) -> Self { + mutableState.write { mutableState in + precondition(mutableState.cachedResponseHandler == nil, "Cached response handler has already been set.") + mutableState.cachedResponseHandler = handler + } + + return self + } + + // MARK: - Lifetime APIs + + /// Sets a handler to be called when the cURL description of the request is available. + /// + /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which `handler` will be called. + /// - handler: Closure to be called when the cURL description is available. + /// + /// - Returns: The instance. + @discardableResult + public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping (String) -> Void) -> Self { + mutableState.write { mutableState in + if mutableState.requests.last != nil { + queue.async { handler(self.cURLDescription()) } + } else { + mutableState.cURLHandler = (queue, handler) + } + } + + return self + } + + /// Sets a handler to be called when the cURL description of the request is available. + /// + /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. + /// + /// - Parameter handler: Closure to be called when the cURL description is available. Called on the instance's + /// `underlyingQueue` by default. + /// + /// - Returns: The instance. + @discardableResult + public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self { + cURLDescription(on: underlyingQueue, calling: handler) + + return self + } + + /// Sets a closure to called whenever Alamofire creates a `URLRequest` for this instance. + /// + /// - Note: This closure will be called multiple times if the instance adapts incoming `URLRequest`s or is retried. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. + /// - handler: Closure to be called when a `URLRequest` is available. + /// + /// - Returns: The instance. + @discardableResult + public func onURLRequestCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLRequest) -> Void) -> Self { + mutableState.write { state in + if let request = state.requests.last { + queue.async { handler(request) } + } + + state.urlRequestHandler = (queue, handler) + } + + return self + } + + /// Sets a closure to be called whenever the instance creates a `URLSessionTask`. + /// + /// - Note: This API should only be used to provide `URLSessionTask`s to existing API, like `NSFileProvider`. It + /// **SHOULD NOT** be used to interact with tasks directly, as that may be break Alamofire features. + /// Additionally, this closure may be called multiple times if the instance is retried. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. + /// - handler: Closure to be called when the `URLSessionTask` is available. + /// + /// - Returns: The instance. + @discardableResult + public func onURLSessionTaskCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLSessionTask) -> Void) -> Self { + mutableState.write { state in + if let task = state.tasks.last { + queue.async { handler(task) } + } + + state.urlSessionTaskHandler = (queue, handler) + } + + return self + } + + // MARK: Cleanup + + /// Adds a `finishHandler` closure to be called when the request completes. + /// + /// - Parameter closure: Closure to be called when the request finishes. + func onFinish(perform finishHandler: @escaping () -> Void) { + guard !isFinished else { finishHandler(); return } + + mutableState.write { state in + state.finishHandlers.append(finishHandler) + } + } + + /// Final cleanup step executed when the instance finishes response serialization. + func cleanup() { + let handlers = mutableState.finishHandlers + handlers.forEach { $0() } + mutableState.write { state in + state.finishHandlers.removeAll() + } + + delegate?.cleanup(after: self) + } +} + +extension Request { + /// Type indicating how a `DataRequest` or `DataStreamRequest` should proceed after receiving an `HTTPURLResponse`. + public enum ResponseDisposition { + /// Allow the request to continue normally. + case allow + /// Cancel the request, similar to calling `cancel()`. + case cancel + + var sessionDisposition: URLSession.ResponseDisposition { + switch self { + case .allow: return .allow + case .cancel: return .cancel + } + } + } +} + +// MARK: - Protocol Conformances + +extension Request: Equatable { + public static func ==(lhs: Request, rhs: Request) -> Bool { + lhs.id == rhs.id + } +} + +extension Request: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension Request: CustomStringConvertible { + /// A textual representation of this instance, including the `HTTPMethod` and `URL` if the `URLRequest` has been + /// created, as well as the response status code, if a response has been received. + public var description: String { + guard let request = performedRequests.last ?? lastRequest, + let url = request.url, + let method = request.httpMethod else { return "No request created yet." } + + let requestDescription = "\(method) \(url.absoluteString)" + + return response.map { "\(requestDescription) (\($0.statusCode))" } ?? requestDescription + } +} + +extension Request { + /// cURL representation of the instance. + /// + /// - Returns: The cURL equivalent of the instance. + public func cURLDescription() -> String { + guard + let request = lastRequest, + let url = request.url, + let host = url.host, + let method = request.httpMethod else { return "$ curl command could not be created" } + + var components = ["$ curl -v"] + + components.append("-X \(method)") + + if let credentialStorage = delegate?.sessionConfiguration.urlCredentialStorage { + let protectionSpace = URLProtectionSpace(host: host, + port: url.port ?? 0, + protocol: url.scheme, + realm: host, + authenticationMethod: NSURLAuthenticationMethodHTTPBasic) + + if let credentials = credentialStorage.credentials(for: protectionSpace)?.values { + for credential in credentials { + guard let user = credential.user, let password = credential.password else { continue } + components.append("-u \(user):\(password)") + } + } else { + if let credential, let user = credential.user, let password = credential.password { + components.append("-u \(user):\(password)") + } + } + } + + if let configuration = delegate?.sessionConfiguration, configuration.httpShouldSetCookies { + if + let cookieStorage = configuration.httpCookieStorage, + let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty { + let allCookies = cookies.map { "\($0.name)=\($0.value)" }.joined(separator: ";") + + components.append("-b \"\(allCookies)\"") + } + } + + var headers = HTTPHeaders() + + if let sessionHeaders = delegate?.sessionConfiguration.headers { + for header in sessionHeaders where header.name != "Cookie" { + headers[header.name] = header.value + } + } + + for header in request.headers where header.name != "Cookie" { + headers[header.name] = header.value + } + + for header in headers { + let escapedValue = header.value.replacingOccurrences(of: "\"", with: "\\\"") + components.append("-H \"\(header.name): \(escapedValue)\"") + } + + if let httpBodyData = request.httpBody { + let httpBody = String(decoding: httpBodyData, as: UTF8.self) + var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"") + escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"") + + components.append("-d \"\(escapedBody)\"") + } + + components.append("\"\(url.absoluteString)\"") + + return components.joined(separator: " \\\n\t") + } +} + +/// Protocol abstraction for `Request`'s communication back to the `SessionDelegate`. +public protocol RequestDelegate: AnyObject { + /// `URLSessionConfiguration` used to create the underlying `URLSessionTask`s. + var sessionConfiguration: URLSessionConfiguration { get } + + /// Determines whether the `Request` should automatically call `resume()` when adding the first response handler. + var startImmediately: Bool { get } + + /// Notifies the delegate the `Request` has reached a point where it needs cleanup. + /// + /// - Parameter request: The `Request` to cleanup after. + func cleanup(after request: Request) + + /// Asynchronously ask the delegate whether a `Request` will be retried. + /// + /// - Parameters: + /// - request: `Request` which failed. + /// - error: `Error` which produced the failure. + /// - completion: Closure taking the `RetryResult` for evaluation. + func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) + + /// Asynchronously retry the `Request`. + /// + /// - Parameters: + /// - request: `Request` which will be retried. + /// - timeDelay: `TimeInterval` after which the retry will be triggered. + func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) +} diff --git a/Source/RequestTaskMap.swift b/Source/Core/RequestTaskMap.swift similarity index 100% rename from Source/RequestTaskMap.swift rename to Source/Core/RequestTaskMap.swift diff --git a/Source/Response.swift b/Source/Core/Response.swift similarity index 100% rename from Source/Response.swift rename to Source/Core/Response.swift diff --git a/Source/Session.swift b/Source/Core/Session.swift similarity index 100% rename from Source/Session.swift rename to Source/Core/Session.swift diff --git a/Source/SessionDelegate.swift b/Source/Core/SessionDelegate.swift similarity index 100% rename from Source/SessionDelegate.swift rename to Source/Core/SessionDelegate.swift diff --git a/Source/URLConvertible+URLRequestConvertible.swift b/Source/Core/URLConvertible+URLRequestConvertible.swift similarity index 100% rename from Source/URLConvertible+URLRequestConvertible.swift rename to Source/Core/URLConvertible+URLRequestConvertible.swift diff --git a/Source/Core/UploadRequest.swift b/Source/Core/UploadRequest.swift new file mode 100644 index 000000000..854373b52 --- /dev/null +++ b/Source/Core/UploadRequest.swift @@ -0,0 +1,174 @@ +// +// UploadRequest.swift +// +// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `DataRequest` subclass which handles `Data` upload from memory, file, or stream using `URLSessionUploadTask`. +public final class UploadRequest: DataRequest { + /// Type describing the origin of the upload, whether `Data`, file, or stream. + public enum Uploadable { + /// Upload from the provided `Data` value. + case data(Data) + /// Upload from the provided file `URL`, as well as a `Bool` determining whether the source file should be + /// automatically removed once uploaded. + case file(URL, shouldRemove: Bool) + /// Upload from the provided `InputStream`. + case stream(InputStream) + } + + // MARK: Initial State + + /// The `UploadableConvertible` value used to produce the `Uploadable` value for this instance. + public let upload: UploadableConvertible + + /// `FileManager` used to perform cleanup tasks, including the removal of multipart form encoded payloads written + /// to disk. + public let fileManager: FileManager + + // MARK: Mutable State + + /// `Uploadable` value used by the instance. + public var uploadable: Uploadable? + + /// Creates an `UploadRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - convertible: `UploadConvertible` value used to determine the type of upload to be performed. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - fileManager: `FileManager` used to perform cleanup tasks, including the removal of multipart form + /// encoded payloads written to disk. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + init(id: UUID = UUID(), + convertible: UploadConvertible, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + fileManager: FileManager, + delegate: RequestDelegate) { + upload = convertible + self.fileManager = fileManager + + super.init(id: id, + convertible: convertible, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + /// Called when the `Uploadable` value has been created from the `UploadConvertible`. + /// + /// - Parameter uploadable: The `Uploadable` that was created. + func didCreateUploadable(_ uploadable: Uploadable) { + self.uploadable = uploadable + + eventMonitor?.request(self, didCreateUploadable: uploadable) + } + + /// Called when the `Uploadable` value could not be created. + /// + /// - Parameter error: `AFError` produced by the failure. + func didFailToCreateUploadable(with error: AFError) { + self.error = error + + eventMonitor?.request(self, didFailToCreateUploadableWithError: error) + + retryOrFinish(error: error) + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + guard let uploadable else { + fatalError("Attempting to create a URLSessionUploadTask when Uploadable value doesn't exist.") + } + + switch uploadable { + case let .data(data): return session.uploadTask(with: request, from: data) + case let .file(url, _): return session.uploadTask(with: request, fromFile: url) + case .stream: return session.uploadTask(withStreamedRequest: request) + } + } + + override func reset() { + // Uploadable must be recreated on every retry. + uploadable = nil + + super.reset() + } + + /// Produces the `InputStream` from `uploadable`, if it can. + /// + /// - Note: Calling this method with a non-`.stream` `Uploadable` is a logic error and will crash. + /// + /// - Returns: The `InputStream`. + func inputStream() -> InputStream { + guard let uploadable else { + fatalError("Attempting to access the input stream but the uploadable doesn't exist.") + } + + guard case let .stream(stream) = uploadable else { + fatalError("Attempted to access the stream of an UploadRequest that wasn't created with one.") + } + + eventMonitor?.request(self, didProvideInputStream: stream) + + return stream + } + + override public func cleanup() { + defer { super.cleanup() } + + guard + let uploadable, + case let .file(url, shouldRemove) = uploadable, + shouldRemove + else { return } + + try? fileManager.removeItem(at: url) + } +} + +/// A type that can produce an `UploadRequest.Uploadable` value. +public protocol UploadableConvertible { + /// Produces an `UploadRequest.Uploadable` value from the instance. + /// + /// - Returns: The `UploadRequest.Uploadable`. + /// - Throws: Any `Error` produced during creation. + func createUploadable() throws -> UploadRequest.Uploadable +} + +extension UploadRequest.Uploadable: UploadableConvertible { + public func createUploadable() throws -> UploadRequest.Uploadable { + self + } +} + +/// A type that can be converted to an upload, whether from an `UploadRequest.Uploadable` or `URLRequestConvertible`. +public protocol UploadConvertible: UploadableConvertible & URLRequestConvertible {} diff --git a/Source/Core/WebSocketRequest.swift b/Source/Core/WebSocketRequest.swift new file mode 100644 index 000000000..17761d945 --- /dev/null +++ b/Source/Core/WebSocketRequest.swift @@ -0,0 +1,560 @@ +// +// WebSocketRequest.swift +// +// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if canImport(Darwin) && !canImport(FoundationNetworking) // Only Apple platforms support URLSessionWebSocketTask. + +import Foundation + +/// `Request` subclass which manages a WebSocket connection using `URLSessionWebSocketTask`. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@_spi(WebSocket) public final class WebSocketRequest: Request { + enum IncomingEvent { + case connected(protocol: String?) + case receivedMessage(URLSessionWebSocketTask.Message) + case disconnected(closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) + case completed(Completion) + } + + public struct Event { + public enum Kind { + case connected(protocol: String?) + case receivedMessage(Success) + case serializerFailed(Failure) + // Only received if the server disconnects or we cancel with code, not if we do a simple cancel or error. + case disconnected(closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) + case completed(Completion) + } + + weak var socket: WebSocketRequest? + + public let kind: Kind + public var message: Success? { + guard case let .receivedMessage(message) = kind else { return nil } + + return message + } + + init(socket: WebSocketRequest, kind: Kind) { + self.socket = socket + self.kind = kind + } + + public func close(sending closeCode: URLSessionWebSocketTask.CloseCode, reason: Data? = nil) { + socket?.close(sending: closeCode, reason: reason) + } + + public func cancel() { + socket?.cancel() + } + + public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping (PingResponse) -> Void) { + socket?.sendPing(respondingOn: queue, onResponse: onResponse) + } + } + + public struct Completion { + /// Last `URLRequest` issued by the instance. + public let request: URLRequest? + /// Last `HTTPURLResponse` received by the instance. + public let response: HTTPURLResponse? + /// Last `URLSessionTaskMetrics` produced for the instance. + public let metrics: URLSessionTaskMetrics? + /// `AFError` produced for the instance, if any. + public let error: AFError? + } + + public struct Configuration { + public static var `default`: Self { Self() } + + public static func `protocol`(_ protocol: String) -> Self { + Self(protocol: `protocol`) + } + + public static func maximumMessageSize(_ maximumMessageSize: Int) -> Self { + Self(maximumMessageSize: maximumMessageSize) + } + + public static func pingInterval(_ pingInterval: TimeInterval) -> Self { + Self(pingInterval: pingInterval) + } + + public let `protocol`: String? + public let maximumMessageSize: Int + public let pingInterval: TimeInterval? + + init(protocol: String? = nil, maximumMessageSize: Int = 1_048_576, pingInterval: TimeInterval? = nil) { + self.protocol = `protocol` + self.maximumMessageSize = maximumMessageSize + self.pingInterval = pingInterval + } + } + + /// Response to a sent ping. + public enum PingResponse { + public struct Pong { + let start: Date + let end: Date + let latency: TimeInterval + } + + /// Received a pong with the associated state. + case pong(Pong) + /// Received an error. + case error(Error) + /// Did not send the ping, the request is cancelled or suspended. + case unsent + } + + struct SocketMutableState { + var enqueuedSends: [(message: URLSessionWebSocketTask.Message, + queue: DispatchQueue, + completionHandler: (Result) -> Void)] = [] + var handlers: [(queue: DispatchQueue, handler: (_ event: IncomingEvent) -> Void)] = [] + var pingTimerItem: DispatchWorkItem? + } + + let socketMutableState = Protected(SocketMutableState()) + + var socket: URLSessionWebSocketTask? { + task as? URLSessionWebSocketTask + } + + public let convertible: URLRequestConvertible + public let configuration: Configuration + + init(id: UUID = UUID(), + convertible: URLRequestConvertible, + configuration: Configuration, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.convertible = convertible + self.configuration = configuration + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + var copiedRequest = request + let task: URLSessionWebSocketTask + if let `protocol` = configuration.protocol { + copiedRequest.headers.update(.websocketProtocol(`protocol`)) + task = session.webSocketTask(with: copiedRequest) + } else { + task = session.webSocketTask(with: copiedRequest) + } + task.maximumMessageSize = configuration.maximumMessageSize + + return task + } + + override func didCreateTask(_ task: URLSessionTask) { + super.didCreateTask(task) + + guard let webSocketTask = task as? URLSessionWebSocketTask else { + fatalError("Invalid task of type \(task.self) created for WebSocketRequest.") + } + // TODO: What about the any old tasks? Reset their receive? + listen(to: webSocketTask) + + // Empty pending messages. + socketMutableState.write { state in + guard !state.enqueuedSends.isEmpty else { return } + + let sends = state.enqueuedSends + self.underlyingQueue.async { + for send in sends { + webSocketTask.send(send.message) { error in + send.queue.async { + send.completionHandler(Result(value: (), error: error)) + } + } + } + } + + state.enqueuedSends = [] + } + } + + func didClose() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + mutableState.write { mutableState in + // Check whether error is cancellation or other websocket closing error. + // If so, remove it. + // Otherwise keep it. + if case let .sessionTaskFailed(error) = mutableState.error, (error as? URLError)?.code == .cancelled { + mutableState.error = nil + } + } + + // TODO: Still issue this event? + eventMonitor?.requestDidCancel(self) + } + + @discardableResult + public func close(sending closeCode: URLSessionWebSocketTask.CloseCode, reason: Data? = nil) -> Self { + cancelAutomaticPing() + + mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.cancelled) else { return } + + mutableState.state = .cancelled + + underlyingQueue.async { self.didClose() } + + guard let task = mutableState.tasks.last, task.state != .completed else { + underlyingQueue.async { self.finish() } + return + } + + // Resume to ensure metrics are gathered. + task.resume() + // Cast from state directly, not the property, otherwise the lock is recursive. + (mutableState.tasks.last as? URLSessionWebSocketTask)?.cancel(with: closeCode, reason: reason) + underlyingQueue.async { self.didCancelTask(task) } + } + + return self + } + + @discardableResult + override public func cancel() -> Self { + cancelAutomaticPing() + + return super.cancel() + } + + func didConnect(protocol: String?) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + socketMutableState.read { state in + // TODO: Capture HTTPURLResponse here too? + for handler in state.handlers { + // Saved handler calls out to serializationQueue immediately, then to handler's queue. + handler.handler(.connected(protocol: `protocol`)) + } + } + + if let pingInterval = configuration.pingInterval { + startAutomaticPing(every: pingInterval) + } + } + + public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping (PingResponse) -> Void) { + guard isResumed else { + queue.async { onResponse(.unsent) } + return + } + + let start = Date() + let startTimestamp = ProcessInfo.processInfo.systemUptime + socket?.sendPing { error in + // Calls back on delegate queue / rootQueue / underlyingQueue + if let error { + queue.async { + onResponse(.error(error)) + } + // TODO: What to do with failed ping? Configure for failure, auto retry, or stop pinging? + } else { + let end = Date() + let endTimestamp = ProcessInfo.processInfo.systemUptime + let pong = PingResponse.Pong(start: start, end: end, latency: endTimestamp - startTimestamp) + + queue.async { + onResponse(.pong(pong)) + } + } + } + } + + func startAutomaticPing(every pingInterval: TimeInterval) { + socketMutableState.write { mutableState in + guard isResumed else { + // Defer out of lock. + defer { cancelAutomaticPing() } + return + } + + let item = DispatchWorkItem { [weak self] in + guard let self, self.isResumed else { return } + + self.sendPing(respondingOn: self.underlyingQueue) { response in + guard case .pong = response else { return } + + self.startAutomaticPing(every: pingInterval) + } + } + + mutableState.pingTimerItem = item + underlyingQueue.asyncAfter(deadline: .now() + pingInterval, execute: item) + } + } + + #if swift(>=5.8) + @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) + func startAutomaticPing(every duration: Duration) { + let interval = TimeInterval(duration.components.seconds) + (Double(duration.components.attoseconds) / 1e18) + startAutomaticPing(every: interval) + } + #endif + + func cancelAutomaticPing() { + socketMutableState.write { mutableState in + mutableState.pingTimerItem?.cancel() + mutableState.pingTimerItem = nil + } + } + + func didDisconnect(closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + cancelAutomaticPing() + socketMutableState.read { state in + for handler in state.handlers { + // Saved handler calls out to serializationQueue immediately, then to handler's queue. + handler.handler(.disconnected(closeCode: closeCode, reason: reason)) + } + } + } + + private func listen(to task: URLSessionWebSocketTask) { + // TODO: Do we care about the cycle while receiving? + task.receive { result in + switch result { + case let .success(message): + self.socketMutableState.read { state in + for handler in state.handlers { + // Saved handler calls out to serializationQueue immediately, then to handler's queue. + handler.handler(.receivedMessage(message)) + } + } + + self.listen(to: task) + case .failure: + // It doesn't seem like any relevant errors are received here, just incorrect garbage, like errors when + // the socket disconnects. + break + } + } + } + + @discardableResult + public func streamSerializer( + _ serializer: Serializer, + on queue: DispatchQueue = .main, + handler: @escaping (_ event: Event) -> Void + ) -> Self where Serializer: WebSocketMessageSerializer, Serializer.Failure == Error { + forIncomingEvent(on: queue) { incomingEvent in + let event: Event + switch incomingEvent { + case let .connected(`protocol`): + event = .init(socket: self, kind: .connected(protocol: `protocol`)) + case let .receivedMessage(message): + do { + let serializedMessage = try serializer.decode(message) + event = .init(socket: self, kind: .receivedMessage(serializedMessage)) + } catch { + event = .init(socket: self, kind: .serializerFailed(error)) + } + case let .disconnected(closeCode, reason): + event = .init(socket: self, kind: .disconnected(closeCode: closeCode, reason: reason)) + case let .completed(completion): + event = .init(socket: self, kind: .completed(completion)) + } + + queue.async { handler(event) } + } + } + + @discardableResult + public func streamDecodableEvents( + _ type: Value.Type = Value.self, + on queue: DispatchQueue = .main, + using decoder: DataDecoder = JSONDecoder(), + handler: @escaping (_ event: Event) -> Void + ) -> Self where Value: Decodable { + streamSerializer(DecodableWebSocketMessageDecoder(decoder: decoder), on: queue, handler: handler) + } + + @discardableResult + public func streamDecodable( + _ type: Value.Type = Value.self, + on queue: DispatchQueue = .main, + using decoder: DataDecoder = JSONDecoder(), + handler: @escaping (_ value: Value) -> Void + ) -> Self where Value: Decodable { + streamDecodableEvents(Value.self, on: queue) { event in + event.message.map(handler) + } + } + + @discardableResult + public func streamMessageEvents( + on queue: DispatchQueue = .main, + handler: @escaping (_ event: Event) -> Void + ) -> Self { + forIncomingEvent(on: queue) { incomingEvent in + let event: Event + switch incomingEvent { + case let .connected(`protocol`): + event = .init(socket: self, kind: .connected(protocol: `protocol`)) + case let .receivedMessage(message): + event = .init(socket: self, kind: .receivedMessage(message)) + case let .disconnected(closeCode, reason): + event = .init(socket: self, kind: .disconnected(closeCode: closeCode, reason: reason)) + case let .completed(completion): + event = .init(socket: self, kind: .completed(completion)) + } + + queue.async { handler(event) } + } + } + + @discardableResult + public func streamMessages( + on queue: DispatchQueue = .main, + handler: @escaping (_ message: URLSessionWebSocketTask.Message) -> Void + ) -> Self { + streamMessageEvents(on: queue) { event in + event.message.map(handler) + } + } + + func forIncomingEvent(on queue: DispatchQueue, handler: @escaping (IncomingEvent) -> Void) -> Self { + socketMutableState.write { state in + state.handlers.append((queue: queue, handler: { incomingEvent in + self.serializationQueue.async { + handler(incomingEvent) + } + })) + } + + appendResponseSerializer { + self.responseSerializerDidComplete { + self.serializationQueue.async { + handler(.completed(.init(request: self.request, + response: self.response, + metrics: self.metrics, + error: self.error))) + } + } + } + + return self + } + + public func send(_ message: URLSessionWebSocketTask.Message, + queue: DispatchQueue = .main, + completionHandler: @escaping (Result) -> Void) { + guard !(isCancelled || isFinished) else { return } + + guard let socket else { + // URLSessionWebSocketTask note created yet, enqueue the send. + socketMutableState.write { mutableState in + mutableState.enqueuedSends.append((message, queue, completionHandler)) + } + + return + } + + socket.send(message) { error in + queue.async { + completionHandler(Result(value: (), error: error)) + } + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public protocol WebSocketMessageSerializer { + associatedtype Output + associatedtype Failure: Error = Error + + func decode(_ message: URLSessionWebSocketTask.Message) throws -> Output +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension WebSocketMessageSerializer { + public static func json( + decoding _: Value.Type = Value.self, + using decoder: JSONDecoder = JSONDecoder() + ) -> DecodableWebSocketMessageDecoder where Self == DecodableWebSocketMessageDecoder { + Self(decoder: decoder) + } + + static var passthrough: PassthroughWebSocketMessageDecoder { + PassthroughWebSocketMessageDecoder() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +struct PassthroughWebSocketMessageDecoder: WebSocketMessageSerializer { + public typealias Failure = Never + + public func decode(_ message: URLSessionWebSocketTask.Message) -> URLSessionWebSocketTask.Message { + message + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DecodableWebSocketMessageDecoder: WebSocketMessageSerializer { + public enum Error: Swift.Error { + case decoding(Swift.Error) + case unknownMessage(description: String) + } + + public let decoder: DataDecoder + + public init(decoder: DataDecoder) { + self.decoder = decoder + } + + public func decode(_ message: URLSessionWebSocketTask.Message) throws -> Value { + let data: Data + switch message { + case let .data(messageData): + data = messageData + case let .string(string): + data = Data(string.utf8) + @unknown default: + throw Error.unknownMessage(description: String(describing: message)) + } + + do { + return try decoder.decode(Value.self, from: data) + } catch { + throw Error.decoding(error) + } + } +} + +#endif diff --git a/Source/DispatchQueue+Alamofire.swift b/Source/Extensions/DispatchQueue+Alamofire.swift similarity index 100% rename from Source/DispatchQueue+Alamofire.swift rename to Source/Extensions/DispatchQueue+Alamofire.swift diff --git a/Source/OperationQueue+Alamofire.swift b/Source/Extensions/OperationQueue+Alamofire.swift similarity index 100% rename from Source/OperationQueue+Alamofire.swift rename to Source/Extensions/OperationQueue+Alamofire.swift diff --git a/Source/Result+Alamofire.swift b/Source/Extensions/Result+Alamofire.swift similarity index 100% rename from Source/Result+Alamofire.swift rename to Source/Extensions/Result+Alamofire.swift diff --git a/Source/StringEncoding+Alamofire.swift b/Source/Extensions/StringEncoding+Alamofire.swift similarity index 100% rename from Source/StringEncoding+Alamofire.swift rename to Source/Extensions/StringEncoding+Alamofire.swift diff --git a/Source/URLRequest+Alamofire.swift b/Source/Extensions/URLRequest+Alamofire.swift similarity index 100% rename from Source/URLRequest+Alamofire.swift rename to Source/Extensions/URLRequest+Alamofire.swift diff --git a/Source/URLSessionConfiguration+Alamofire.swift b/Source/Extensions/URLSessionConfiguration+Alamofire.swift similarity index 100% rename from Source/URLSessionConfiguration+Alamofire.swift rename to Source/Extensions/URLSessionConfiguration+Alamofire.swift diff --git a/Source/AlamofireExtended.swift b/Source/Features/AlamofireExtended.swift similarity index 100% rename from Source/AlamofireExtended.swift rename to Source/Features/AlamofireExtended.swift diff --git a/Source/AuthenticationInterceptor.swift b/Source/Features/AuthenticationInterceptor.swift similarity index 100% rename from Source/AuthenticationInterceptor.swift rename to Source/Features/AuthenticationInterceptor.swift diff --git a/Source/CachedResponseHandler.swift b/Source/Features/CachedResponseHandler.swift similarity index 100% rename from Source/CachedResponseHandler.swift rename to Source/Features/CachedResponseHandler.swift diff --git a/Source/Combine.swift b/Source/Features/Combine.swift similarity index 100% rename from Source/Combine.swift rename to Source/Features/Combine.swift diff --git a/Source/Concurrency.swift b/Source/Features/Concurrency.swift similarity index 100% rename from Source/Concurrency.swift rename to Source/Features/Concurrency.swift diff --git a/Source/EventMonitor.swift b/Source/Features/EventMonitor.swift similarity index 100% rename from Source/EventMonitor.swift rename to Source/Features/EventMonitor.swift diff --git a/Source/MultipartFormData.swift b/Source/Features/MultipartFormData.swift similarity index 100% rename from Source/MultipartFormData.swift rename to Source/Features/MultipartFormData.swift diff --git a/Source/MultipartUpload.swift b/Source/Features/MultipartUpload.swift similarity index 100% rename from Source/MultipartUpload.swift rename to Source/Features/MultipartUpload.swift diff --git a/Source/NetworkReachabilityManager.swift b/Source/Features/NetworkReachabilityManager.swift similarity index 100% rename from Source/NetworkReachabilityManager.swift rename to Source/Features/NetworkReachabilityManager.swift diff --git a/Source/RedirectHandler.swift b/Source/Features/RedirectHandler.swift similarity index 100% rename from Source/RedirectHandler.swift rename to Source/Features/RedirectHandler.swift diff --git a/Source/RequestCompression.swift b/Source/Features/RequestCompression.swift similarity index 100% rename from Source/RequestCompression.swift rename to Source/Features/RequestCompression.swift diff --git a/Source/RequestInterceptor.swift b/Source/Features/RequestInterceptor.swift similarity index 100% rename from Source/RequestInterceptor.swift rename to Source/Features/RequestInterceptor.swift diff --git a/Source/Features/ResponseSerialization.swift b/Source/Features/ResponseSerialization.swift new file mode 100644 index 000000000..a61b98e94 --- /dev/null +++ b/Source/Features/ResponseSerialization.swift @@ -0,0 +1,525 @@ +// +// ResponseSerialization.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// The type to which all data response serializers must conform in order to serialize a response. +public protocol DataResponseSerializerProtocol { + /// The type of serialized object to be created. + associatedtype SerializedObject + + /// Serialize the response `Data` into the provided type. + /// + /// - Parameters: + /// - request: `URLRequest` which was used to perform the request, if any. + /// - response: `HTTPURLResponse` received from the server, if any. + /// - data: `Data` returned from the server, if any. + /// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request. + /// + /// - Returns: The `SerializedObject`. + /// - Throws: Any `Error` produced during serialization. + func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject +} + +/// The type to which all download response serializers must conform in order to serialize a response. +public protocol DownloadResponseSerializerProtocol { + /// The type of serialized object to be created. + associatedtype SerializedObject + + /// Serialize the downloaded response `Data` from disk into the provided type. + /// + /// - Parameters: + /// - request: `URLRequest` which was used to perform the request, if any. + /// - response: `HTTPURLResponse` received from the server, if any. + /// - fileURL: File `URL` to which the response data was downloaded. + /// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request. + /// + /// - Returns: The `SerializedObject`. + /// - Throws: Any `Error` produced during serialization. + func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> SerializedObject +} + +/// A serializer that can handle both data and download responses. +public protocol ResponseSerializer: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol { + /// `DataPreprocessor` used to prepare incoming `Data` for serialization. + var dataPreprocessor: DataPreprocessor { get } + /// `HTTPMethod`s for which empty response bodies are considered appropriate. + var emptyRequestMethods: Set { get } + /// HTTP response codes for which empty response bodies are considered appropriate. + var emptyResponseCodes: Set { get } +} + +/// Type used to preprocess `Data` before it handled by a serializer. +public protocol DataPreprocessor { + /// Process `Data` before it's handled by a serializer. + /// - Parameter data: The raw `Data` to process. + func preprocess(_ data: Data) throws -> Data +} + +/// `DataPreprocessor` that returns passed `Data` without any transform. +public struct PassthroughPreprocessor: DataPreprocessor { + /// Creates an instance. + public init() {} + + public func preprocess(_ data: Data) throws -> Data { data } +} + +/// `DataPreprocessor` that trims Google's typical `)]}',\n` XSSI JSON header. +public struct GoogleXSSIPreprocessor: DataPreprocessor { + /// Creates an instance. + public init() {} + + public func preprocess(_ data: Data) throws -> Data { + (data.prefix(6) == Data(")]}',\n".utf8)) ? data.dropFirst(6) : data + } +} + +extension DataPreprocessor where Self == PassthroughPreprocessor { + /// Provides a `PassthroughPreprocessor` instance. + public static var passthrough: PassthroughPreprocessor { PassthroughPreprocessor() } +} + +extension DataPreprocessor where Self == GoogleXSSIPreprocessor { + /// Provides a `GoogleXSSIPreprocessor` instance. + public static var googleXSSI: GoogleXSSIPreprocessor { GoogleXSSIPreprocessor() } +} + +extension ResponseSerializer { + /// Default `DataPreprocessor`. `PassthroughPreprocessor` by default. + public static var defaultDataPreprocessor: DataPreprocessor { PassthroughPreprocessor() } + /// Default `HTTPMethod`s for which empty response bodies are always considered appropriate. `[.head]` by default. + public static var defaultEmptyRequestMethods: Set { [.head] } + /// HTTP response codes for which empty response bodies are always considered appropriate. `[204, 205]` by default. + public static var defaultEmptyResponseCodes: Set { [204, 205] } + + public var dataPreprocessor: DataPreprocessor { Self.defaultDataPreprocessor } + public var emptyRequestMethods: Set { Self.defaultEmptyRequestMethods } + public var emptyResponseCodes: Set { Self.defaultEmptyResponseCodes } + + /// Determines whether the `request` allows empty response bodies, if `request` exists. + /// + /// - Parameter request: `URLRequest` to evaluate. + /// + /// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `request` was `nil`. + public func requestAllowsEmptyResponseData(_ request: URLRequest?) -> Bool? { + request.flatMap(\.httpMethod) + .flatMap(HTTPMethod.init) + .map { emptyRequestMethods.contains($0) } + } + + /// Determines whether the `response` allows empty response bodies, if `response` exists`. + /// + /// - Parameter response: `HTTPURLResponse` to evaluate. + /// + /// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `response` was `nil`. + public func responseAllowsEmptyResponseData(_ response: HTTPURLResponse?) -> Bool? { + response.map(\.statusCode) + .map { emptyResponseCodes.contains($0) } + } + + /// Determines whether `request` and `response` allow empty response bodies. + /// + /// - Parameters: + /// - request: `URLRequest` to evaluate. + /// - response: `HTTPURLResponse` to evaluate. + /// + /// - Returns: `true` if `request` or `response` allow empty bodies, `false` otherwise. + public func emptyResponseAllowed(forRequest request: URLRequest?, response: HTTPURLResponse?) -> Bool { + (requestAllowsEmptyResponseData(request) == true) || (responseAllowsEmptyResponseData(response) == true) + } +} + +/// By default, any serializer declared to conform to both types will get file serialization for free, as it just feeds +/// the data read from disk into the data response serializer. +extension DownloadResponseSerializerProtocol where Self: DataResponseSerializerProtocol { + public func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> Self.SerializedObject { + guard error == nil else { throw error! } + + guard let fileURL else { + throw AFError.responseSerializationFailed(reason: .inputFileNil) + } + + let data: Data + do { + data = try Data(contentsOf: fileURL) + } catch { + throw AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)) + } + + do { + return try serialize(request: request, response: response, data: data, error: error) + } catch { + throw error + } + } +} + +// MARK: - URL + +/// A `DownloadResponseSerializerProtocol` that performs only `Error` checking and ensures that a downloaded `fileURL` +/// is present. +public struct URLResponseSerializer: DownloadResponseSerializerProtocol { + /// Creates an instance. + public init() {} + + public func serializeDownload(request: URLRequest?, + response: HTTPURLResponse?, + fileURL: URL?, + error: Error?) throws -> URL { + guard error == nil else { throw error! } + + guard let url = fileURL else { + throw AFError.responseSerializationFailed(reason: .inputFileNil) + } + + return url + } +} + +extension DownloadResponseSerializerProtocol where Self == URLResponseSerializer { + /// Provides a `URLResponseSerializer` instance. + public static var url: URLResponseSerializer { URLResponseSerializer() } +} + +// MARK: - Data + +/// A `ResponseSerializer` that performs minimal response checking and returns any response `Data` as-is. By default, a +/// request returning `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the +/// response has an HTTP status code valid for empty responses, then an empty `Data` value is returned. +public final class DataResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + + /// Creates a `DataResponseSerializer` using the provided parameters. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + public init(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) { + self.dataPreprocessor = dataPreprocessor + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Data { + guard error == nil else { throw error! } + + guard var data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return Data() + } + + data = try dataPreprocessor.preprocess(data) + + return data + } +} + +extension ResponseSerializer where Self == DataResponseSerializer { + /// Provides a default `DataResponseSerializer` instance. + public static var data: DataResponseSerializer { DataResponseSerializer() } + + /// Creates a `DataResponseSerializer` using the provided parameters. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// + /// - Returns: The `DataResponseSerializer`. + public static func data(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponseSerializer { + DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods) + } +} + +// MARK: - String + +/// A `ResponseSerializer` that decodes the response data as a `String`. By default, a request returning `nil` or no +/// data is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code +/// valid for empty responses, then an empty `String` is returned. +public final class StringResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + /// Optional string encoding used to validate the response. + public let encoding: String.Encoding? + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + + /// Creates an instance with the provided values. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + public init(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) { + self.dataPreprocessor = dataPreprocessor + self.encoding = encoding + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String { + guard error == nil else { throw error! } + + guard var data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return "" + } + + data = try dataPreprocessor.preprocess(data) + + var convertedEncoding = encoding + + if let encodingName = response?.textEncodingName, convertedEncoding == nil { + convertedEncoding = String.Encoding(ianaCharsetName: encodingName) + } + + let actualEncoding = convertedEncoding ?? .isoLatin1 + + guard let string = String(data: data, encoding: actualEncoding) else { + throw AFError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding)) + } + + return string + } +} + +extension ResponseSerializer where Self == StringResponseSerializer { + /// Provides a default `StringResponseSerializer` instance. + public static var string: StringResponseSerializer { StringResponseSerializer() } + + /// Creates a `StringResponseSerializer` with the provided values. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// + /// - Returns: The `StringResponseSerializer`. + public static func string(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> StringResponseSerializer { + StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods) + } +} + +// MARK: - JSON + +/// A `ResponseSerializer` that decodes the response data using `JSONSerialization`. By default, a request returning +/// `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the response has an +/// HTTP status code valid for empty responses, then an `NSNull` value is returned. +/// +/// - Note: This serializer is deprecated and should not be used. Instead, create concrete types conforming to +/// `Decodable` and use a `DecodableResponseSerializer`. +@available(*, deprecated, message: "JSONResponseSerializer deprecated and will be removed in Alamofire 6. Use DecodableResponseSerializer instead.") +public final class JSONResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + /// `JSONSerialization.ReadingOptions` used when serializing a response. + public let options: JSONSerialization.ReadingOptions + + /// Creates an instance with the provided values. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// - options: The options to use. `.allowFragments` by default. + public init(dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, + options: JSONSerialization.ReadingOptions = .allowFragments) { + self.dataPreprocessor = dataPreprocessor + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + self.options = options + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Any { + guard error == nil else { throw error! } + + guard var data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return NSNull() + } + + data = try dataPreprocessor.preprocess(data) + + do { + return try JSONSerialization.jsonObject(with: data, options: options) + } catch { + throw AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)) + } + } +} + +// MARK: - Empty + +/// Protocol representing an empty response. Use `T.emptyValue()` to get an instance. +public protocol EmptyResponse { + /// Empty value for the conforming type. + /// + /// - Returns: Value of `Self` to use for empty values. + static func emptyValue() -> Self +} + +/// Type representing an empty value. Use `Empty.value` to get the static instance. +public struct Empty: Codable, Sendable { + /// Static `Empty` instance used for all `Empty` responses. + public static let value = Empty() +} + +extension Empty: EmptyResponse { + public static func emptyValue() -> Empty { + value + } +} + +// MARK: - DataDecoder Protocol + +/// Any type which can decode `Data` into a `Decodable` type. +public protocol DataDecoder { + /// Decode `Data` into the provided type. + /// + /// - Parameters: + /// - type: The `Type` to be decoded. + /// - data: The `Data` to be decoded. + /// + /// - Returns: The decoded value of type `D`. + /// - Throws: Any error that occurs during decode. + func decode(_ type: D.Type, from data: Data) throws -> D +} + +/// `JSONDecoder` automatically conforms to `DataDecoder`. +extension JSONDecoder: DataDecoder {} +/// `PropertyListDecoder` automatically conforms to `DataDecoder`. +extension PropertyListDecoder: DataDecoder {} + +// MARK: - Decodable + +/// A `ResponseSerializer` that decodes the response data as a generic value using any type that conforms to +/// `DataDecoder`. By default, this is an instance of `JSONDecoder`. Additionally, a request returning `nil` or no data +/// is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code valid +/// for empty responses then an empty value will be returned. If the decoded type conforms to `EmptyResponse`, the +/// type's `emptyValue()` will be returned. If the decoded type is `Empty`, the `.value` instance is returned. If the +/// decoded type *does not* conform to `EmptyResponse` and isn't `Empty`, an error will be produced. +public final class DecodableResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + /// The `DataDecoder` instance used to decode responses. + public let decoder: DataDecoder + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + + /// Creates an instance using the values provided. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - decoder: The `DataDecoder`. `JSONDecoder()` by default. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + public init(dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) { + self.dataPreprocessor = dataPreprocessor + self.decoder = decoder + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T { + guard error == nil else { throw error! } + + guard var data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + guard let emptyResponseType = T.self as? EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else { + throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)")) + } + + return emptyValue + } + + data = try dataPreprocessor.preprocess(data) + + do { + return try decoder.decode(T.self, from: data) + } catch { + throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) + } + } +} + +extension ResponseSerializer { + /// Creates a `DecodableResponseSerializer` using the values provided. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - decoder: The `DataDecoder`. `JSONDecoder()` by default. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// + /// - Returns: The `DecodableResponseSerializer`. + public static func decodable(of type: T.Type, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DecodableResponseSerializer where Self == DecodableResponseSerializer { + DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods) + } +} diff --git a/Source/RetryPolicy.swift b/Source/Features/RetryPolicy.swift similarity index 100% rename from Source/RetryPolicy.swift rename to Source/Features/RetryPolicy.swift diff --git a/Source/ServerTrustEvaluation.swift b/Source/Features/ServerTrustEvaluation.swift similarity index 100% rename from Source/ServerTrustEvaluation.swift rename to Source/Features/ServerTrustEvaluation.swift diff --git a/Source/URLEncodedFormEncoder.swift b/Source/Features/URLEncodedFormEncoder.swift similarity index 100% rename from Source/URLEncodedFormEncoder.swift rename to Source/Features/URLEncodedFormEncoder.swift diff --git a/Source/Validation.swift b/Source/Features/Validation.swift similarity index 100% rename from Source/Validation.swift rename to Source/Features/Validation.swift diff --git a/Source/Request.swift b/Source/Request.swift deleted file mode 100644 index 7d823d2ce..000000000 --- a/Source/Request.swift +++ /dev/null @@ -1,2603 +0,0 @@ -// -// Request.swift -// -// Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -/// `Request` is the common superclass of all Alamofire request types and provides common state, delegate, and callback -/// handling. -public class Request { - /// State of the `Request`, with managed transitions between states set when calling `resume()`, `suspend()`, or - /// `cancel()` on the `Request`. - public enum State { - /// Initial state of the `Request`. - case initialized - /// `State` set when `resume()` is called. Any tasks created for the `Request` will have `resume()` called on - /// them in this state. - case resumed - /// `State` set when `suspend()` is called. Any tasks created for the `Request` will have `suspend()` called on - /// them in this state. - case suspended - /// `State` set when `cancel()` is called. Any tasks created for the `Request` will have `cancel()` called on - /// them. Unlike `resumed` or `suspended`, once in the `cancelled` state, the `Request` can no longer transition - /// to any other state. - case cancelled - /// `State` set when all response serialization completion closures have been cleared on the `Request` and - /// enqueued on their respective queues. - case finished - - /// Determines whether `self` can be transitioned to the provided `State`. - func canTransitionTo(_ state: State) -> Bool { - switch (self, state) { - case (.initialized, _): - return true - case (_, .initialized), (.cancelled, _), (.finished, _): - return false - case (.resumed, .cancelled), (.suspended, .cancelled), (.resumed, .suspended), (.suspended, .resumed): - return true - case (.suspended, .suspended), (.resumed, .resumed): - return false - case (_, .finished): - return true - } - } - } - - // MARK: - Initial State - - /// `UUID` providing a unique identifier for the `Request`, used in the `Hashable` and `Equatable` conformances. - public let id: UUID - /// The serial queue for all internal async actions. - public let underlyingQueue: DispatchQueue - /// The queue used for all serialization actions. By default it's a serial queue that targets `underlyingQueue`. - public let serializationQueue: DispatchQueue - /// `EventMonitor` used for event callbacks. - public let eventMonitor: EventMonitor? - /// The `Request`'s interceptor. - public let interceptor: RequestInterceptor? - /// The `Request`'s delegate. - public private(set) weak var delegate: RequestDelegate? - - // MARK: - Mutable State - - /// Type encapsulating all mutable state that may need to be accessed from anything other than the `underlyingQueue`. - struct MutableState { - /// State of the `Request`. - var state: State = .initialized - /// `ProgressHandler` and `DispatchQueue` provided for upload progress callbacks. - var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? - /// `ProgressHandler` and `DispatchQueue` provided for download progress callbacks. - var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? - /// `RedirectHandler` provided for to handle request redirection. - var redirectHandler: RedirectHandler? - /// `CachedResponseHandler` provided to handle response caching. - var cachedResponseHandler: CachedResponseHandler? - /// Queue and closure called when the `Request` is able to create a cURL description of itself. - var cURLHandler: (queue: DispatchQueue, handler: (String) -> Void)? - /// Queue and closure called when the `Request` creates a `URLRequest`. - var urlRequestHandler: (queue: DispatchQueue, handler: (URLRequest) -> Void)? - /// Queue and closure called when the `Request` creates a `URLSessionTask`. - var urlSessionTaskHandler: (queue: DispatchQueue, handler: (URLSessionTask) -> Void)? - /// Response serialization closures that handle response parsing. - var responseSerializers: [() -> Void] = [] - /// Response serialization completion closures executed once all response serializers are complete. - var responseSerializerCompletions: [() -> Void] = [] - /// Whether response serializer processing is finished. - var responseSerializerProcessingFinished = false - /// `URLCredential` used for authentication challenges. - var credential: URLCredential? - /// All `URLRequest`s created by Alamofire on behalf of the `Request`. - var requests: [URLRequest] = [] - /// All `URLSessionTask`s created by Alamofire on behalf of the `Request`. - var tasks: [URLSessionTask] = [] - /// All `URLSessionTaskMetrics` values gathered by Alamofire on behalf of the `Request`. Should correspond - /// exactly the the `tasks` created. - var metrics: [URLSessionTaskMetrics] = [] - /// Number of times any retriers provided retried the `Request`. - var retryCount = 0 - /// Final `AFError` for the `Request`, whether from various internal Alamofire calls or as a result of a `task`. - var error: AFError? - /// Whether the instance has had `finish()` called and is running the serializers. Should be replaced with a - /// representation in the state machine in the future. - var isFinishing = false - /// Actions to run when requests are finished. Use for concurrency support. - var finishHandlers: [() -> Void] = [] - } - - /// Protected `MutableState` value that provides thread-safe access to state values. - fileprivate let mutableState = Protected(MutableState()) - - /// `State` of the `Request`. - public var state: State { mutableState.state } - /// Returns whether `state` is `.initialized`. - public var isInitialized: Bool { state == .initialized } - /// Returns whether `state is `.resumed`. - public var isResumed: Bool { state == .resumed } - /// Returns whether `state` is `.suspended`. - public var isSuspended: Bool { state == .suspended } - /// Returns whether `state` is `.cancelled`. - public var isCancelled: Bool { state == .cancelled } - /// Returns whether `state` is `.finished`. - public var isFinished: Bool { state == .finished } - - // MARK: Progress - - /// Closure type executed when monitoring the upload or download progress of a request. - public typealias ProgressHandler = (Progress) -> Void - - /// `Progress` of the upload of the body of the executed `URLRequest`. Reset to `0` if the `Request` is retried. - public let uploadProgress = Progress(totalUnitCount: 0) - /// `Progress` of the download of any response data. Reset to `0` if the `Request` is retried. - public let downloadProgress = Progress(totalUnitCount: 0) - /// `ProgressHandler` called when `uploadProgress` is updated, on the provided `DispatchQueue`. - private var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { - get { mutableState.uploadProgressHandler } - set { mutableState.uploadProgressHandler = newValue } - } - - /// `ProgressHandler` called when `downloadProgress` is updated, on the provided `DispatchQueue`. - fileprivate var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { - get { mutableState.downloadProgressHandler } - set { mutableState.downloadProgressHandler = newValue } - } - - // MARK: Redirect Handling - - /// `RedirectHandler` set on the instance. - public private(set) var redirectHandler: RedirectHandler? { - get { mutableState.redirectHandler } - set { mutableState.redirectHandler = newValue } - } - - // MARK: Cached Response Handling - - /// `CachedResponseHandler` set on the instance. - public private(set) var cachedResponseHandler: CachedResponseHandler? { - get { mutableState.cachedResponseHandler } - set { mutableState.cachedResponseHandler = newValue } - } - - // MARK: URLCredential - - /// `URLCredential` used for authentication challenges. Created by calling one of the `authenticate` methods. - public private(set) var credential: URLCredential? { - get { mutableState.credential } - set { mutableState.credential = newValue } - } - - // MARK: Validators - - /// `Validator` callback closures that store the validation calls enqueued. - fileprivate let validators = Protected<[() -> Void]>([]) - - // MARK: URLRequests - - /// All `URLRequests` created on behalf of the `Request`, including original and adapted requests. - public var requests: [URLRequest] { mutableState.requests } - /// First `URLRequest` created on behalf of the `Request`. May not be the first one actually executed. - public var firstRequest: URLRequest? { requests.first } - /// Last `URLRequest` created on behalf of the `Request`. - public var lastRequest: URLRequest? { requests.last } - /// Current `URLRequest` created on behalf of the `Request`. - public var request: URLRequest? { lastRequest } - - /// `URLRequest`s from all of the `URLSessionTask`s executed on behalf of the `Request`. May be different from - /// `requests` due to `URLSession` manipulation. - public var performedRequests: [URLRequest] { mutableState.read { $0.tasks.compactMap(\.currentRequest) } } - - // MARK: HTTPURLResponse - - /// `HTTPURLResponse` received from the server, if any. If the `Request` was retried, this is the response of the - /// last `URLSessionTask`. - public var response: HTTPURLResponse? { lastTask?.response as? HTTPURLResponse } - - // MARK: Tasks - - /// All `URLSessionTask`s created on behalf of the `Request`. - public var tasks: [URLSessionTask] { mutableState.tasks } - /// First `URLSessionTask` created on behalf of the `Request`. - public var firstTask: URLSessionTask? { tasks.first } - /// Last `URLSessionTask` created on behalf of the `Request`. - public var lastTask: URLSessionTask? { tasks.last } - /// Current `URLSessionTask` created on behalf of the `Request`. - public var task: URLSessionTask? { lastTask } - - // MARK: Metrics - - /// All `URLSessionTaskMetrics` gathered on behalf of the `Request`. Should correspond to the `tasks` created. - public var allMetrics: [URLSessionTaskMetrics] { mutableState.metrics } - /// First `URLSessionTaskMetrics` gathered on behalf of the `Request`. - public var firstMetrics: URLSessionTaskMetrics? { allMetrics.first } - /// Last `URLSessionTaskMetrics` gathered on behalf of the `Request`. - public var lastMetrics: URLSessionTaskMetrics? { allMetrics.last } - /// Current `URLSessionTaskMetrics` gathered on behalf of the `Request`. - public var metrics: URLSessionTaskMetrics? { lastMetrics } - - // MARK: Retry Count - - /// Number of times the `Request` has been retried. - public var retryCount: Int { mutableState.retryCount } - - // MARK: Error - - /// `Error` returned from Alamofire internally, from the network request directly, or any validators executed. - public fileprivate(set) var error: AFError? { - get { mutableState.error } - set { mutableState.error = newValue } - } - - /// Default initializer for the `Request` superclass. - /// - /// - Parameters: - /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. - /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. - /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets - /// `underlyingQueue`, but can be passed another queue from a `Session`. - /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. - /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. - /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. - init(id: UUID = UUID(), - underlyingQueue: DispatchQueue, - serializationQueue: DispatchQueue, - eventMonitor: EventMonitor?, - interceptor: RequestInterceptor?, - delegate: RequestDelegate) { - self.id = id - self.underlyingQueue = underlyingQueue - self.serializationQueue = serializationQueue - self.eventMonitor = eventMonitor - self.interceptor = interceptor - self.delegate = delegate - } - - // MARK: - Internal Event API - - // All API must be called from underlyingQueue. - - /// Called when an initial `URLRequest` has been created on behalf of the instance. If a `RequestAdapter` is active, - /// the `URLRequest` will be adapted before being issued. - /// - /// - Parameter request: The `URLRequest` created. - func didCreateInitialURLRequest(_ request: URLRequest) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - mutableState.write { $0.requests.append(request) } - - eventMonitor?.request(self, didCreateInitialURLRequest: request) - } - - /// Called when initial `URLRequest` creation has failed, typically through a `URLRequestConvertible`. - /// - /// - Note: Triggers retry. - /// - /// - Parameter error: `AFError` thrown from the failed creation. - func didFailToCreateURLRequest(with error: AFError) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - self.error = error - - eventMonitor?.request(self, didFailToCreateURLRequestWithError: error) - - callCURLHandlerIfNecessary() - - retryOrFinish(error: error) - } - - /// Called when a `RequestAdapter` has successfully adapted a `URLRequest`. - /// - /// - Parameters: - /// - initialRequest: The `URLRequest` that was adapted. - /// - adaptedRequest: The `URLRequest` returned by the `RequestAdapter`. - func didAdaptInitialRequest(_ initialRequest: URLRequest, to adaptedRequest: URLRequest) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - mutableState.write { $0.requests.append(adaptedRequest) } - - eventMonitor?.request(self, didAdaptInitialRequest: initialRequest, to: adaptedRequest) - } - - /// Called when a `RequestAdapter` fails to adapt a `URLRequest`. - /// - /// - Note: Triggers retry. - /// - /// - Parameters: - /// - request: The `URLRequest` the adapter was called with. - /// - error: The `AFError` returned by the `RequestAdapter`. - func didFailToAdaptURLRequest(_ request: URLRequest, withError error: AFError) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - self.error = error - - eventMonitor?.request(self, didFailToAdaptURLRequest: request, withError: error) - - callCURLHandlerIfNecessary() - - retryOrFinish(error: error) - } - - /// Final `URLRequest` has been created for the instance. - /// - /// - Parameter request: The `URLRequest` created. - func didCreateURLRequest(_ request: URLRequest) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - mutableState.read { state in - state.urlRequestHandler?.queue.async { state.urlRequestHandler?.handler(request) } - } - - eventMonitor?.request(self, didCreateURLRequest: request) - - callCURLHandlerIfNecessary() - } - - /// Asynchronously calls any stored `cURLHandler` and then removes it from `mutableState`. - private func callCURLHandlerIfNecessary() { - mutableState.write { mutableState in - guard let cURLHandler = mutableState.cURLHandler else { return } - - cURLHandler.queue.async { cURLHandler.handler(self.cURLDescription()) } - - mutableState.cURLHandler = nil - } - } - - /// Called when a `URLSessionTask` is created on behalf of the instance. - /// - /// - Parameter task: The `URLSessionTask` created. - func didCreateTask(_ task: URLSessionTask) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - mutableState.write { state in - state.tasks.append(task) - - guard let urlSessionTaskHandler = state.urlSessionTaskHandler else { return } - - urlSessionTaskHandler.queue.async { urlSessionTaskHandler.handler(task) } - } - - eventMonitor?.request(self, didCreateTask: task) - } - - /// Called when resumption is completed. - func didResume() { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - eventMonitor?.requestDidResume(self) - } - - /// Called when a `URLSessionTask` is resumed on behalf of the instance. - /// - /// - Parameter task: The `URLSessionTask` resumed. - func didResumeTask(_ task: URLSessionTask) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - eventMonitor?.request(self, didResumeTask: task) - } - - /// Called when suspension is completed. - func didSuspend() { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - eventMonitor?.requestDidSuspend(self) - } - - /// Called when a `URLSessionTask` is suspended on behalf of the instance. - /// - /// - Parameter task: The `URLSessionTask` suspended. - func didSuspendTask(_ task: URLSessionTask) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - eventMonitor?.request(self, didSuspendTask: task) - } - - /// Called when cancellation is completed, sets `error` to `AFError.explicitlyCancelled`. - func didCancel() { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - mutableState.write { mutableState in - mutableState.error = mutableState.error ?? AFError.explicitlyCancelled - } - - eventMonitor?.requestDidCancel(self) - } - - /// Called when a `URLSessionTask` is cancelled on behalf of the instance. - /// - /// - Parameter task: The `URLSessionTask` cancelled. - func didCancelTask(_ task: URLSessionTask) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - eventMonitor?.request(self, didCancelTask: task) - } - - /// Called when a `URLSessionTaskMetrics` value is gathered on behalf of the instance. - /// - /// - Parameter metrics: The `URLSessionTaskMetrics` gathered. - func didGatherMetrics(_ metrics: URLSessionTaskMetrics) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - mutableState.write { $0.metrics.append(metrics) } - - eventMonitor?.request(self, didGatherMetrics: metrics) - } - - /// Called when a `URLSessionTask` fails before it is finished, typically during certificate pinning. - /// - /// - Parameters: - /// - task: The `URLSessionTask` which failed. - /// - error: The early failure `AFError`. - func didFailTask(_ task: URLSessionTask, earlyWithError error: AFError) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - self.error = error - - // Task will still complete, so didCompleteTask(_:with:) will handle retry. - eventMonitor?.request(self, didFailTask: task, earlyWithError: error) - } - - /// Called when a `URLSessionTask` completes. All tasks will eventually call this method. - /// - /// - Note: Response validation is synchronously triggered in this step. - /// - /// - Parameters: - /// - task: The `URLSessionTask` which completed. - /// - error: The `AFError` `task` may have completed with. If `error` has already been set on the instance, this - /// value is ignored. - func didCompleteTask(_ task: URLSessionTask, with error: AFError?) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - self.error = self.error ?? error - - let validators = validators.read { $0 } - validators.forEach { $0() } - - eventMonitor?.request(self, didCompleteTask: task, with: error) - - retryOrFinish(error: self.error) - } - - /// Called when the `RequestDelegate` is going to retry this `Request`. Calls `reset()`. - func prepareForRetry() { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - mutableState.write { $0.retryCount += 1 } - - reset() - - eventMonitor?.requestIsRetrying(self) - } - - /// Called to determine whether retry will be triggered for the particular error, or whether the instance should - /// call `finish()`. - /// - /// - Parameter error: The possible `AFError` which may trigger retry. - func retryOrFinish(error: AFError?) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - guard !isCancelled, let error, let delegate else { finish(); return } - - delegate.retryResult(for: self, dueTo: error) { retryResult in - switch retryResult { - case .doNotRetry: - self.finish() - case let .doNotRetryWithError(retryError): - self.finish(error: retryError.asAFError(orFailWith: "Received retryError was not already AFError")) - case .retry, .retryWithDelay: - delegate.retryRequest(self, withDelay: retryResult.delay) - } - } - } - - /// Finishes this `Request` and starts the response serializers. - /// - /// - Parameter error: The possible `Error` with which the instance will finish. - func finish(error: AFError? = nil) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - guard !mutableState.isFinishing else { return } - - mutableState.isFinishing = true - - if let error { self.error = error } - - // Start response handlers - processNextResponseSerializer() - - eventMonitor?.requestDidFinish(self) - } - - /// Appends the response serialization closure to the instance. - /// - /// - Note: This method will also `resume` the instance if `delegate.startImmediately` returns `true`. - /// - /// - Parameter closure: The closure containing the response serialization call. - func appendResponseSerializer(_ closure: @escaping () -> Void) { - mutableState.write { mutableState in - mutableState.responseSerializers.append(closure) - - if mutableState.state == .finished { - mutableState.state = .resumed - } - - if mutableState.responseSerializerProcessingFinished { - underlyingQueue.async { self.processNextResponseSerializer() } - } - - if mutableState.state.canTransitionTo(.resumed) { - underlyingQueue.async { if self.delegate?.startImmediately == true { self.resume() } } - } - } - } - - /// Returns the next response serializer closure to execute if there's one left. - /// - /// - Returns: The next response serialization closure, if there is one. - func nextResponseSerializer() -> (() -> Void)? { - var responseSerializer: (() -> Void)? - - mutableState.write { mutableState in - let responseSerializerIndex = mutableState.responseSerializerCompletions.count - - if responseSerializerIndex < mutableState.responseSerializers.count { - responseSerializer = mutableState.responseSerializers[responseSerializerIndex] - } - } - - return responseSerializer - } - - /// Processes the next response serializer and calls all completions if response serialization is complete. - func processNextResponseSerializer() { - guard let responseSerializer = nextResponseSerializer() else { - // Execute all response serializer completions and clear them - var completions: [() -> Void] = [] - - mutableState.write { mutableState in - completions = mutableState.responseSerializerCompletions - - // Clear out all response serializers and response serializer completions in mutable state since the - // request is complete. It's important to do this prior to calling the completion closures in case - // the completions call back into the request triggering a re-processing of the response serializers. - // An example of how this can happen is by calling cancel inside a response completion closure. - mutableState.responseSerializers.removeAll() - mutableState.responseSerializerCompletions.removeAll() - - if mutableState.state.canTransitionTo(.finished) { - mutableState.state = .finished - } - - mutableState.responseSerializerProcessingFinished = true - mutableState.isFinishing = false - } - - completions.forEach { $0() } - - // Cleanup the request - cleanup() - - return - } - - serializationQueue.async { responseSerializer() } - } - - /// Notifies the `Request` that the response serializer is complete. - /// - /// - Parameter completion: The completion handler provided with the response serializer, called when all serializers - /// are complete. - func responseSerializerDidComplete(completion: @escaping () -> Void) { - mutableState.write { $0.responseSerializerCompletions.append(completion) } - processNextResponseSerializer() - } - - /// Resets all task and response serializer related state for retry. - func reset() { - error = nil - - uploadProgress.totalUnitCount = 0 - uploadProgress.completedUnitCount = 0 - downloadProgress.totalUnitCount = 0 - downloadProgress.completedUnitCount = 0 - - mutableState.write { state in - state.isFinishing = false - state.responseSerializerCompletions = [] - } - } - - /// Called when updating the upload progress. - /// - /// - Parameters: - /// - totalBytesSent: Total bytes sent so far. - /// - totalBytesExpectedToSend: Total bytes expected to send. - func updateUploadProgress(totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - uploadProgress.totalUnitCount = totalBytesExpectedToSend - uploadProgress.completedUnitCount = totalBytesSent - - uploadProgressHandler?.queue.async { self.uploadProgressHandler?.handler(self.uploadProgress) } - } - - /// Perform a closure on the current `state` while locked. - /// - /// - Parameter perform: The closure to perform. - func withState(perform: (State) -> Void) { - mutableState.withState(perform: perform) - } - - // MARK: Task Creation - - /// Called when creating a `URLSessionTask` for this `Request`. Subclasses must override. - /// - /// - Parameters: - /// - request: `URLRequest` to use to create the `URLSessionTask`. - /// - session: `URLSession` which creates the `URLSessionTask`. - /// - /// - Returns: The `URLSessionTask` created. - func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { - fatalError("Subclasses must override.") - } - - // MARK: - Public API - - // These APIs are callable from any queue. - - // MARK: State - - /// Cancels the instance. Once cancelled, a `Request` can no longer be resumed or suspended. - /// - /// - Returns: The instance. - @discardableResult - public func cancel() -> Self { - mutableState.write { mutableState in - guard mutableState.state.canTransitionTo(.cancelled) else { return } - - mutableState.state = .cancelled - - underlyingQueue.async { self.didCancel() } - - guard let task = mutableState.tasks.last, task.state != .completed else { - underlyingQueue.async { self.finish() } - return - } - - // Resume to ensure metrics are gathered. - task.resume() - task.cancel() - underlyingQueue.async { self.didCancelTask(task) } - } - - return self - } - - /// Suspends the instance. - /// - /// - Returns: The instance. - @discardableResult - public func suspend() -> Self { - mutableState.write { mutableState in - guard mutableState.state.canTransitionTo(.suspended) else { return } - - mutableState.state = .suspended - - underlyingQueue.async { self.didSuspend() } - - guard let task = mutableState.tasks.last, task.state != .completed else { return } - - task.suspend() - underlyingQueue.async { self.didSuspendTask(task) } - } - - return self - } - - /// Resumes the instance. - /// - /// - Returns: The instance. - @discardableResult - public func resume() -> Self { - mutableState.write { mutableState in - guard mutableState.state.canTransitionTo(.resumed) else { return } - - mutableState.state = .resumed - - underlyingQueue.async { self.didResume() } - - guard let task = mutableState.tasks.last, task.state != .completed else { return } - - task.resume() - underlyingQueue.async { self.didResumeTask(task) } - } - - return self - } - - // MARK: - Closure API - - /// Associates a credential using the provided values with the instance. - /// - /// - Parameters: - /// - username: The username. - /// - password: The password. - /// - persistence: The `URLCredential.Persistence` for the created `URLCredential`. `.forSession` by default. - /// - /// - Returns: The instance. - @discardableResult - public func authenticate(username: String, password: String, persistence: URLCredential.Persistence = .forSession) -> Self { - let credential = URLCredential(user: username, password: password, persistence: persistence) - - return authenticate(with: credential) - } - - /// Associates the provided credential with the instance. - /// - /// - Parameter credential: The `URLCredential`. - /// - /// - Returns: The instance. - @discardableResult - public func authenticate(with credential: URLCredential) -> Self { - mutableState.credential = credential - - return self - } - - /// Sets a closure to be called periodically during the lifecycle of the instance as data is read from the server. - /// - /// - Note: Only the last closure provided is used. - /// - /// - Parameters: - /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. - /// - closure: The closure to be executed periodically as data is read from the server. - /// - /// - Returns: The instance. - @discardableResult - public func downloadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { - mutableState.downloadProgressHandler = (handler: closure, queue: queue) - - return self - } - - /// Sets a closure to be called periodically during the lifecycle of the instance as data is sent to the server. - /// - /// - Note: Only the last closure provided is used. - /// - /// - Parameters: - /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. - /// - closure: The closure to be executed periodically as data is sent to the server. - /// - /// - Returns: The instance. - @discardableResult - public func uploadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { - mutableState.uploadProgressHandler = (handler: closure, queue: queue) - - return self - } - - // MARK: Redirects - - /// Sets the redirect handler for the instance which will be used if a redirect response is encountered. - /// - /// - Note: Attempting to set the redirect handler more than once is a logic error and will crash. - /// - /// - Parameter handler: The `RedirectHandler`. - /// - /// - Returns: The instance. - @discardableResult - public func redirect(using handler: RedirectHandler) -> Self { - mutableState.write { mutableState in - precondition(mutableState.redirectHandler == nil, "Redirect handler has already been set.") - mutableState.redirectHandler = handler - } - - return self - } - - // MARK: Cached Responses - - /// Sets the cached response handler for the `Request` which will be used when attempting to cache a response. - /// - /// - Note: Attempting to set the cache handler more than once is a logic error and will crash. - /// - /// - Parameter handler: The `CachedResponseHandler`. - /// - /// - Returns: The instance. - @discardableResult - public func cacheResponse(using handler: CachedResponseHandler) -> Self { - mutableState.write { mutableState in - precondition(mutableState.cachedResponseHandler == nil, "Cached response handler has already been set.") - mutableState.cachedResponseHandler = handler - } - - return self - } - - // MARK: - Lifetime APIs - - /// Sets a handler to be called when the cURL description of the request is available. - /// - /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. - /// - /// - Parameters: - /// - queue: `DispatchQueue` on which `handler` will be called. - /// - handler: Closure to be called when the cURL description is available. - /// - /// - Returns: The instance. - @discardableResult - public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping (String) -> Void) -> Self { - mutableState.write { mutableState in - if mutableState.requests.last != nil { - queue.async { handler(self.cURLDescription()) } - } else { - mutableState.cURLHandler = (queue, handler) - } - } - - return self - } - - /// Sets a handler to be called when the cURL description of the request is available. - /// - /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. - /// - /// - Parameter handler: Closure to be called when the cURL description is available. Called on the instance's - /// `underlyingQueue` by default. - /// - /// - Returns: The instance. - @discardableResult - public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self { - cURLDescription(on: underlyingQueue, calling: handler) - - return self - } - - /// Sets a closure to called whenever Alamofire creates a `URLRequest` for this instance. - /// - /// - Note: This closure will be called multiple times if the instance adapts incoming `URLRequest`s or is retried. - /// - /// - Parameters: - /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. - /// - handler: Closure to be called when a `URLRequest` is available. - /// - /// - Returns: The instance. - @discardableResult - public func onURLRequestCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLRequest) -> Void) -> Self { - mutableState.write { state in - if let request = state.requests.last { - queue.async { handler(request) } - } - - state.urlRequestHandler = (queue, handler) - } - - return self - } - - /// Sets a closure to be called whenever the instance creates a `URLSessionTask`. - /// - /// - Note: This API should only be used to provide `URLSessionTask`s to existing API, like `NSFileProvider`. It - /// **SHOULD NOT** be used to interact with tasks directly, as that may be break Alamofire features. - /// Additionally, this closure may be called multiple times if the instance is retried. - /// - /// - Parameters: - /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. - /// - handler: Closure to be called when the `URLSessionTask` is available. - /// - /// - Returns: The instance. - @discardableResult - public func onURLSessionTaskCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLSessionTask) -> Void) -> Self { - mutableState.write { state in - if let task = state.tasks.last { - queue.async { handler(task) } - } - - state.urlSessionTaskHandler = (queue, handler) - } - - return self - } - - // MARK: Cleanup - - /// Adds a `finishHandler` closure to be called when the request completes. - /// - /// - Parameter closure: Closure to be called when the request finishes. - func onFinish(perform finishHandler: @escaping () -> Void) { - guard !isFinished else { finishHandler(); return } - - mutableState.write { state in - state.finishHandlers.append(finishHandler) - } - } - - /// Final cleanup step executed when the instance finishes response serialization. - func cleanup() { - let handlers = mutableState.finishHandlers - handlers.forEach { $0() } - mutableState.write { state in - state.finishHandlers.removeAll() - } - - delegate?.cleanup(after: self) - } -} - -extension Request { - /// Type indicating how a `DataRequest` or `DataStreamRequest` should proceed after receiving an `HTTPURLResponse`. - public enum ResponseDisposition { - /// Allow the request to continue normally. - case allow - /// Cancel the request, similar to calling `cancel()`. - case cancel - - var sessionDisposition: URLSession.ResponseDisposition { - switch self { - case .allow: return .allow - case .cancel: return .cancel - } - } - } -} - -// MARK: - Protocol Conformances - -extension Request: Equatable { - public static func ==(lhs: Request, rhs: Request) -> Bool { - lhs.id == rhs.id - } -} - -extension Request: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - -extension Request: CustomStringConvertible { - /// A textual representation of this instance, including the `HTTPMethod` and `URL` if the `URLRequest` has been - /// created, as well as the response status code, if a response has been received. - public var description: String { - guard let request = performedRequests.last ?? lastRequest, - let url = request.url, - let method = request.httpMethod else { return "No request created yet." } - - let requestDescription = "\(method) \(url.absoluteString)" - - return response.map { "\(requestDescription) (\($0.statusCode))" } ?? requestDescription - } -} - -extension Request { - /// cURL representation of the instance. - /// - /// - Returns: The cURL equivalent of the instance. - public func cURLDescription() -> String { - guard - let request = lastRequest, - let url = request.url, - let host = url.host, - let method = request.httpMethod else { return "$ curl command could not be created" } - - var components = ["$ curl -v"] - - components.append("-X \(method)") - - if let credentialStorage = delegate?.sessionConfiguration.urlCredentialStorage { - let protectionSpace = URLProtectionSpace(host: host, - port: url.port ?? 0, - protocol: url.scheme, - realm: host, - authenticationMethod: NSURLAuthenticationMethodHTTPBasic) - - if let credentials = credentialStorage.credentials(for: protectionSpace)?.values { - for credential in credentials { - guard let user = credential.user, let password = credential.password else { continue } - components.append("-u \(user):\(password)") - } - } else { - if let credential, let user = credential.user, let password = credential.password { - components.append("-u \(user):\(password)") - } - } - } - - if let configuration = delegate?.sessionConfiguration, configuration.httpShouldSetCookies { - if - let cookieStorage = configuration.httpCookieStorage, - let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty { - let allCookies = cookies.map { "\($0.name)=\($0.value)" }.joined(separator: ";") - - components.append("-b \"\(allCookies)\"") - } - } - - var headers = HTTPHeaders() - - if let sessionHeaders = delegate?.sessionConfiguration.headers { - for header in sessionHeaders where header.name != "Cookie" { - headers[header.name] = header.value - } - } - - for header in request.headers where header.name != "Cookie" { - headers[header.name] = header.value - } - - for header in headers { - let escapedValue = header.value.replacingOccurrences(of: "\"", with: "\\\"") - components.append("-H \"\(header.name): \(escapedValue)\"") - } - - if let httpBodyData = request.httpBody { - let httpBody = String(decoding: httpBodyData, as: UTF8.self) - var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"") - escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"") - - components.append("-d \"\(escapedBody)\"") - } - - components.append("\"\(url.absoluteString)\"") - - return components.joined(separator: " \\\n\t") - } -} - -/// Protocol abstraction for `Request`'s communication back to the `SessionDelegate`. -public protocol RequestDelegate: AnyObject { - /// `URLSessionConfiguration` used to create the underlying `URLSessionTask`s. - var sessionConfiguration: URLSessionConfiguration { get } - - /// Determines whether the `Request` should automatically call `resume()` when adding the first response handler. - var startImmediately: Bool { get } - - /// Notifies the delegate the `Request` has reached a point where it needs cleanup. - /// - /// - Parameter request: The `Request` to cleanup after. - func cleanup(after request: Request) - - /// Asynchronously ask the delegate whether a `Request` will be retried. - /// - /// - Parameters: - /// - request: `Request` which failed. - /// - error: `Error` which produced the failure. - /// - completion: Closure taking the `RetryResult` for evaluation. - func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) - - /// Asynchronously retry the `Request`. - /// - /// - Parameters: - /// - request: `Request` which will be retried. - /// - timeDelay: `TimeInterval` after which the retry will be triggered. - func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) -} - -// MARK: - Subclasses - -// MARK: - DataRequest - -/// `Request` subclass which handles in-memory `Data` download using `URLSessionDataTask`. -public class DataRequest: Request { - /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. - public let convertible: URLRequestConvertible - /// `Data` read from the server so far. - public var data: Data? { dataMutableState.data } - - private struct DataMutableState { - var data: Data? - var httpResponseHandler: (queue: DispatchQueue, - handler: (_ response: HTTPURLResponse, - _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void)? - } - - private let dataMutableState = Protected(DataMutableState()) - - /// Creates a `DataRequest` using the provided parameters. - /// - /// - Parameters: - /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. - /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this instance. - /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. - /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets - /// `underlyingQueue`, but can be passed another queue from a `Session`. - /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. - /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. - /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. - init(id: UUID = UUID(), - convertible: URLRequestConvertible, - underlyingQueue: DispatchQueue, - serializationQueue: DispatchQueue, - eventMonitor: EventMonitor?, - interceptor: RequestInterceptor?, - delegate: RequestDelegate) { - self.convertible = convertible - - super.init(id: id, - underlyingQueue: underlyingQueue, - serializationQueue: serializationQueue, - eventMonitor: eventMonitor, - interceptor: interceptor, - delegate: delegate) - } - - override func reset() { - super.reset() - - dataMutableState.write { mutableState in - mutableState.data = nil - } - } - - /// Called when `Data` is received by this instance. - /// - /// - Note: Also calls `updateDownloadProgress`. - /// - /// - Parameter data: The `Data` received. - func didReceive(data: Data) { - dataMutableState.write { mutableState in - if mutableState.data == nil { - mutableState.data = data - } else { - mutableState.data?.append(data) - } - } - - updateDownloadProgress() - } - - func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - dataMutableState.read { dataMutableState in - guard let httpResponseHandler = dataMutableState.httpResponseHandler else { - underlyingQueue.async { completionHandler(.allow) } - return - } - - httpResponseHandler.queue.async { - httpResponseHandler.handler(response) { disposition in - if disposition == .cancel { - self.mutableState.write { mutableState in - mutableState.state = .cancelled - mutableState.error = mutableState.error ?? AFError.explicitlyCancelled - } - } - - self.underlyingQueue.async { - completionHandler(disposition.sessionDisposition) - } - } - } - } - } - - override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { - let copiedRequest = request - return session.dataTask(with: copiedRequest) - } - - /// Called to update the `downloadProgress` of the instance. - func updateDownloadProgress() { - let totalBytesReceived = Int64(data?.count ?? 0) - let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown - - downloadProgress.totalUnitCount = totalBytesExpected - downloadProgress.completedUnitCount = totalBytesReceived - - downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } - } - - /// Validates the request, using the specified closure. - /// - /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. - /// - /// - Parameter validation: `Validation` closure used to validate the response. - /// - /// - Returns: The instance. - @discardableResult - public func validate(_ validation: @escaping Validation) -> Self { - let validator: () -> Void = { [unowned self] in - guard error == nil, let response else { return } - - let result = validation(request, response, data) - - if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) } - - eventMonitor?.request(self, - didValidateRequest: request, - response: response, - data: data, - withResult: result) - } - - validators.write { $0.append(validator) } - - return self - } - - /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse` and providing a completion - /// handler to return a `ResponseDisposition` value. - /// - /// - Parameters: - /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. - /// - handler: Closure called when the instance produces an `HTTPURLResponse`. The `completionHandler` provided - /// MUST be called, otherwise the request will never complete. - /// - /// - Returns: The instance. - @_disfavoredOverload - @discardableResult - public func onHTTPResponse( - on queue: DispatchQueue = .main, - perform handler: @escaping (_ response: HTTPURLResponse, - _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void - ) -> Self { - dataMutableState.write { mutableState in - mutableState.httpResponseHandler = (queue, handler) - } - - return self - } - - /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse`. - /// - /// - Parameters: - /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. - /// - handler: Closure called when the instance produces an `HTTPURLResponse`. - /// - /// - Returns: The instance. - @discardableResult - public func onHTTPResponse(on queue: DispatchQueue = .main, - perform handler: @escaping (HTTPURLResponse) -> Void) -> Self { - onHTTPResponse(on: queue) { response, completionHandler in - handler(response) - completionHandler(.allow) - } - - return self - } -} - -// MARK: - DataStreamRequest - -/// `Request` subclass which streams HTTP response `Data` through a `Handler` closure. -public final class DataStreamRequest: Request { - /// Closure type handling `DataStreamRequest.Stream` values. - public typealias Handler = (Stream) throws -> Void - - /// Type encapsulating an `Event` as it flows through the stream, as well as a `CancellationToken` which can be used - /// to stop the stream at any time. - public struct Stream { - /// Latest `Event` from the stream. - public let event: Event - /// Token used to cancel the stream. - public let token: CancellationToken - - /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. - public func cancel() { - token.cancel() - } - } - - /// Type representing an event flowing through the stream. Contains either the `Result` of processing streamed - /// `Data` or the completion of the stream. - public enum Event { - /// Output produced every time the instance receives additional `Data`. The associated value contains the - /// `Result` of processing the incoming `Data`. - case stream(Result) - /// Output produced when the instance has completed, whether due to stream end, cancellation, or an error. - /// Associated `Completion` value contains the final state. - case complete(Completion) - } - - /// Value containing the state of a `DataStreamRequest` when the stream was completed. - public struct Completion { - /// Last `URLRequest` issued by the instance. - public let request: URLRequest? - /// Last `HTTPURLResponse` received by the instance. - public let response: HTTPURLResponse? - /// Last `URLSessionTaskMetrics` produced for the instance. - public let metrics: URLSessionTaskMetrics? - /// `AFError` produced for the instance, if any. - public let error: AFError? - } - - /// Type used to cancel an ongoing stream. - public struct CancellationToken { - weak var request: DataStreamRequest? - - init(_ request: DataStreamRequest) { - self.request = request - } - - /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. - public func cancel() { - request?.cancel() - } - } - - /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. - public let convertible: URLRequestConvertible - /// Whether or not the instance will be cancelled if stream parsing encounters an error. - public let automaticallyCancelOnStreamError: Bool - - /// Internal mutable state specific to this type. - struct StreamMutableState { - /// `OutputStream` bound to the `InputStream` produced by `asInputStream`, if it has been called. - var outputStream: OutputStream? - /// Stream closures called as `Data` is received. - var streams: [(_ data: Data) -> Void] = [] - /// Number of currently executing streams. Used to ensure completions are only fired after all streams are - /// enqueued. - var numberOfExecutingStreams = 0 - /// Completion calls enqueued while streams are still executing. - var enqueuedCompletionEvents: [() -> Void] = [] - /// Handler for any `HTTPURLResponse`s received. - var httpResponseHandler: (queue: DispatchQueue, - handler: (_ response: HTTPURLResponse, - _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void)? - } - - let streamMutableState = Protected(StreamMutableState()) - - /// Creates a `DataStreamRequest` using the provided parameters. - /// - /// - Parameters: - /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` - /// by default. - /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this - /// instance. - /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance will be cancelled when an `Error` - /// is thrown while serializing stream `Data`. - /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. - /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default - /// targets - /// `underlyingQueue`, but can be passed another queue from a `Session`. - /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. - /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. - /// - delegate: `RequestDelegate` that provides an interface to actions not performed by - /// the `Request`. - init(id: UUID = UUID(), - convertible: URLRequestConvertible, - automaticallyCancelOnStreamError: Bool, - underlyingQueue: DispatchQueue, - serializationQueue: DispatchQueue, - eventMonitor: EventMonitor?, - interceptor: RequestInterceptor?, - delegate: RequestDelegate) { - self.convertible = convertible - self.automaticallyCancelOnStreamError = automaticallyCancelOnStreamError - - super.init(id: id, - underlyingQueue: underlyingQueue, - serializationQueue: serializationQueue, - eventMonitor: eventMonitor, - interceptor: interceptor, - delegate: delegate) - } - - override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { - let copiedRequest = request - return session.dataTask(with: copiedRequest) - } - - override func finish(error: AFError? = nil) { - streamMutableState.write { state in - state.outputStream?.close() - } - - super.finish(error: error) - } - - func didReceive(data: Data) { - streamMutableState.write { state in - #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. - if let stream = state.outputStream { - underlyingQueue.async { - var bytes = Array(data) - stream.write(&bytes, maxLength: bytes.count) - } - } - #endif - state.numberOfExecutingStreams += state.streams.count - let localState = state - underlyingQueue.async { localState.streams.forEach { $0(data) } } - } - } - - func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - streamMutableState.read { dataMutableState in - guard let httpResponseHandler = dataMutableState.httpResponseHandler else { - underlyingQueue.async { completionHandler(.allow) } - return - } - - httpResponseHandler.queue.async { - httpResponseHandler.handler(response) { disposition in - if disposition == .cancel { - self.mutableState.write { mutableState in - mutableState.state = .cancelled - mutableState.error = mutableState.error ?? AFError.explicitlyCancelled - } - } - - self.underlyingQueue.async { - completionHandler(disposition.sessionDisposition) - } - } - } - } - } - - /// Validates the `URLRequest` and `HTTPURLResponse` received for the instance using the provided `Validation` closure. - /// - /// - Parameter validation: `Validation` closure used to validate the request and response. - /// - /// - Returns: The `DataStreamRequest`. - @discardableResult - public func validate(_ validation: @escaping Validation) -> Self { - let validator: () -> Void = { [unowned self] in - guard error == nil, let response else { return } - - let result = validation(request, response) - - if case let .failure(error) = result { - self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) - } - - eventMonitor?.request(self, - didValidateRequest: request, - response: response, - withResult: result) - } - - validators.write { $0.append(validator) } - - return self - } - - #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. - /// Produces an `InputStream` that receives the `Data` received by the instance. - /// - /// - Note: The `InputStream` produced by this method must have `open()` called before being able to read `Data`. - /// Additionally, this method will automatically call `resume()` on the instance, regardless of whether or - /// not the creating session has `startRequestsImmediately` set to `true`. - /// - /// - Parameter bufferSize: Size, in bytes, of the buffer between the `OutputStream` and `InputStream`. - /// - /// - Returns: The `InputStream` bound to the internal `OutboundStream`. - public func asInputStream(bufferSize: Int = 1024) -> InputStream? { - defer { resume() } - - var inputStream: InputStream? - streamMutableState.write { state in - Foundation.Stream.getBoundStreams(withBufferSize: bufferSize, - inputStream: &inputStream, - outputStream: &state.outputStream) - state.outputStream?.open() - } - - return inputStream - } - #endif - - /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse` and providing a completion - /// handler to return a `ResponseDisposition` value. - /// - /// - Parameters: - /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. - /// - handler: Closure called when the instance produces an `HTTPURLResponse`. The `completionHandler` provided - /// MUST be called, otherwise the request will never complete. - /// - /// - Returns: The instance. - @_disfavoredOverload - @discardableResult - public func onHTTPResponse( - on queue: DispatchQueue = .main, - perform handler: @escaping (_ response: HTTPURLResponse, - _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void - ) -> Self { - streamMutableState.write { mutableState in - mutableState.httpResponseHandler = (queue, handler) - } - - return self - } - - /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse`. - /// - /// - Parameters: - /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. - /// - handler: Closure called when the instance produces an `HTTPURLResponse`. - /// - /// - Returns: The instance. - @discardableResult - public func onHTTPResponse(on queue: DispatchQueue = .main, - perform handler: @escaping (HTTPURLResponse) -> Void) -> Self { - onHTTPResponse(on: queue) { response, completionHandler in - handler(response) - completionHandler(.allow) - } - - return self - } - - func capturingError(from closure: () throws -> Void) { - do { - try closure() - } catch { - self.error = error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) - cancel() - } - } - - func appendStreamCompletion(on queue: DispatchQueue, - stream: @escaping Handler) { - appendResponseSerializer { - self.underlyingQueue.async { - self.responseSerializerDidComplete { - self.streamMutableState.write { state in - guard state.numberOfExecutingStreams == 0 else { - state.enqueuedCompletionEvents.append { - self.enqueueCompletion(on: queue, stream: stream) - } - - return - } - - self.enqueueCompletion(on: queue, stream: stream) - } - } - } - } - } - - func enqueueCompletion(on queue: DispatchQueue, - stream: @escaping Handler) { - queue.async { - do { - let completion = Completion(request: self.request, - response: self.response, - metrics: self.metrics, - error: self.error) - try stream(.init(event: .complete(completion), token: .init(self))) - } catch { - // Ignore error, as errors on Completion can't be handled anyway. - } - } - } -} - -extension DataStreamRequest.Stream { - /// Incoming `Result` values from `Event.stream`. - public var result: Result? { - guard case let .stream(result) = event else { return nil } - - return result - } - - /// `Success` value of the instance, if any. - public var value: Success? { - guard case let .success(value) = result else { return nil } - - return value - } - - /// `Failure` value of the instance, if any. - public var error: Failure? { - guard case let .failure(error) = result else { return nil } - - return error - } - - /// `Completion` value of the instance, if any. - public var completion: DataStreamRequest.Completion? { - guard case let .complete(completion) = event else { return nil } - - return completion - } -} - -// MARK: - WebSocketRequest - -#if canImport(Darwin) && !canImport(FoundationNetworking) // Only Apple platforms support URLSessionWebSocketTask. - -/// `Request` subclass which manages a WebSocket connection using `URLSessionWebSocketTask`. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@_spi(WebSocket) public final class WebSocketRequest: Request { - enum IncomingEvent { - case connected(protocol: String?) - case receivedMessage(URLSessionWebSocketTask.Message) - case disconnected(closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) - case completed(Completion) - } - - public struct Event { - public enum Kind { - case connected(protocol: String?) - case receivedMessage(Success) - case serializerFailed(Failure) - // Only received if the server disconnects or we cancel with code, not if we do a simple cancel or error. - case disconnected(closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) - case completed(Completion) - } - - weak var socket: WebSocketRequest? - - public let kind: Kind - public var message: Success? { - guard case let .receivedMessage(message) = kind else { return nil } - - return message - } - - init(socket: WebSocketRequest, kind: Kind) { - self.socket = socket - self.kind = kind - } - - public func close(sending closeCode: URLSessionWebSocketTask.CloseCode, reason: Data? = nil) { - socket?.close(sending: closeCode, reason: reason) - } - - public func cancel() { - socket?.cancel() - } - - public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping (PingResponse) -> Void) { - socket?.sendPing(respondingOn: queue, onResponse: onResponse) - } - } - - public struct Completion { - /// Last `URLRequest` issued by the instance. - public let request: URLRequest? - /// Last `HTTPURLResponse` received by the instance. - public let response: HTTPURLResponse? - /// Last `URLSessionTaskMetrics` produced for the instance. - public let metrics: URLSessionTaskMetrics? - /// `AFError` produced for the instance, if any. - public let error: AFError? - } - - public struct Configuration { - public static var `default`: Self { Self() } - - public static func `protocol`(_ protocol: String) -> Self { - Self(protocol: `protocol`) - } - - public static func maximumMessageSize(_ maximumMessageSize: Int) -> Self { - Self(maximumMessageSize: maximumMessageSize) - } - - public static func pingInterval(_ pingInterval: TimeInterval) -> Self { - Self(pingInterval: pingInterval) - } - - public let `protocol`: String? - public let maximumMessageSize: Int - public let pingInterval: TimeInterval? - - init(protocol: String? = nil, maximumMessageSize: Int = 1_048_576, pingInterval: TimeInterval? = nil) { - self.protocol = `protocol` - self.maximumMessageSize = maximumMessageSize - self.pingInterval = pingInterval - } - } - - /// Response to a sent ping. - public enum PingResponse { - public struct Pong { - let start: Date - let end: Date - let latency: TimeInterval - } - - /// Received a pong with the associated state. - case pong(Pong) - /// Received an error. - case error(Error) - /// Did not send the ping, the request is cancelled or suspended. - case unsent - } - - struct SocketMutableState { - var enqueuedSends: [(message: URLSessionWebSocketTask.Message, - queue: DispatchQueue, - completionHandler: (Result) -> Void)] = [] - var handlers: [(queue: DispatchQueue, handler: (_ event: IncomingEvent) -> Void)] = [] - var pingTimerItem: DispatchWorkItem? - } - - let socketMutableState = Protected(SocketMutableState()) - - var socket: URLSessionWebSocketTask? { - task as? URLSessionWebSocketTask - } - - public let convertible: URLRequestConvertible - public let configuration: Configuration - - init(id: UUID = UUID(), - convertible: URLRequestConvertible, - configuration: Configuration, - underlyingQueue: DispatchQueue, - serializationQueue: DispatchQueue, - eventMonitor: EventMonitor?, - interceptor: RequestInterceptor?, - delegate: RequestDelegate) { - self.convertible = convertible - self.configuration = configuration - - super.init(id: id, - underlyingQueue: underlyingQueue, - serializationQueue: serializationQueue, - eventMonitor: eventMonitor, - interceptor: interceptor, - delegate: delegate) - } - - override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { - var copiedRequest = request - let task: URLSessionWebSocketTask - if let `protocol` = configuration.protocol { - copiedRequest.headers.update(.websocketProtocol(`protocol`)) - task = session.webSocketTask(with: copiedRequest) - } else { - task = session.webSocketTask(with: copiedRequest) - } - task.maximumMessageSize = configuration.maximumMessageSize - - return task - } - - override func didCreateTask(_ task: URLSessionTask) { - super.didCreateTask(task) - - guard let webSocketTask = task as? URLSessionWebSocketTask else { - fatalError("Invalid task of type \(task.self) created for WebSocketRequest.") - } - // TODO: What about the any old tasks? Reset their receive? - listen(to: webSocketTask) - - // Empty pending messages. - socketMutableState.write { state in - guard !state.enqueuedSends.isEmpty else { return } - - let sends = state.enqueuedSends - self.underlyingQueue.async { - for send in sends { - webSocketTask.send(send.message) { error in - send.queue.async { - send.completionHandler(Result(value: (), error: error)) - } - } - } - } - - state.enqueuedSends = [] - } - } - - func didClose() { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - mutableState.write { mutableState in - // Check whether error is cancellation or other websocket closing error. - // If so, remove it. - // Otherwise keep it. - if case let .sessionTaskFailed(error) = mutableState.error, (error as? URLError)?.code == .cancelled { - mutableState.error = nil - } - } - - // TODO: Still issue this event? - eventMonitor?.requestDidCancel(self) - } - - @discardableResult - public func close(sending closeCode: URLSessionWebSocketTask.CloseCode, reason: Data? = nil) -> Self { - cancelAutomaticPing() - - mutableState.write { mutableState in - guard mutableState.state.canTransitionTo(.cancelled) else { return } - - mutableState.state = .cancelled - - underlyingQueue.async { self.didClose() } - - guard let task = mutableState.tasks.last, task.state != .completed else { - underlyingQueue.async { self.finish() } - return - } - - // Resume to ensure metrics are gathered. - task.resume() - // Cast from state directly, not the property, otherwise the lock is recursive. - (mutableState.tasks.last as? URLSessionWebSocketTask)?.cancel(with: closeCode, reason: reason) - underlyingQueue.async { self.didCancelTask(task) } - } - - return self - } - - @discardableResult - override public func cancel() -> Self { - cancelAutomaticPing() - - return super.cancel() - } - - func didConnect(protocol: String?) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - socketMutableState.read { state in - // TODO: Capture HTTPURLResponse here too? - for handler in state.handlers { - // Saved handler calls out to serializationQueue immediately, then to handler's queue. - handler.handler(.connected(protocol: `protocol`)) - } - } - - if let pingInterval = configuration.pingInterval { - startAutomaticPing(every: pingInterval) - } - } - - public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping (PingResponse) -> Void) { - guard isResumed else { - queue.async { onResponse(.unsent) } - return - } - - let start = Date() - let startTimestamp = ProcessInfo.processInfo.systemUptime - socket?.sendPing { error in - // Calls back on delegate queue / rootQueue / underlyingQueue - if let error { - queue.async { - onResponse(.error(error)) - } - // TODO: What to do with failed ping? Configure for failure, auto retry, or stop pinging? - } else { - let end = Date() - let endTimestamp = ProcessInfo.processInfo.systemUptime - let pong = PingResponse.Pong(start: start, end: end, latency: endTimestamp - startTimestamp) - - queue.async { - onResponse(.pong(pong)) - } - } - } - } - - func startAutomaticPing(every pingInterval: TimeInterval) { - socketMutableState.write { mutableState in - guard isResumed else { - // Defer out of lock. - defer { cancelAutomaticPing() } - return - } - - let item = DispatchWorkItem { [weak self] in - guard let self, self.isResumed else { return } - - self.sendPing(respondingOn: self.underlyingQueue) { response in - guard case .pong = response else { return } - - self.startAutomaticPing(every: pingInterval) - } - } - - mutableState.pingTimerItem = item - underlyingQueue.asyncAfter(deadline: .now() + pingInterval, execute: item) - } - } - - #if swift(>=5.8) - @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) - func startAutomaticPing(every duration: Duration) { - let interval = TimeInterval(duration.components.seconds) + (Double(duration.components.attoseconds) / 1e18) - startAutomaticPing(every: interval) - } - #endif - - func cancelAutomaticPing() { - socketMutableState.write { mutableState in - mutableState.pingTimerItem?.cancel() - mutableState.pingTimerItem = nil - } - } - - func didDisconnect(closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { - dispatchPrecondition(condition: .onQueue(underlyingQueue)) - - cancelAutomaticPing() - socketMutableState.read { state in - for handler in state.handlers { - // Saved handler calls out to serializationQueue immediately, then to handler's queue. - handler.handler(.disconnected(closeCode: closeCode, reason: reason)) - } - } - } - - private func listen(to task: URLSessionWebSocketTask) { - // TODO: Do we care about the cycle while receiving? - task.receive { result in - switch result { - case let .success(message): - self.socketMutableState.read { state in - for handler in state.handlers { - // Saved handler calls out to serializationQueue immediately, then to handler's queue. - handler.handler(.receivedMessage(message)) - } - } - - self.listen(to: task) - case .failure: - // It doesn't seem like any relevant errors are received here, just incorrect garbage, like errors when - // the socket disconnects. - break - } - } - } - - @discardableResult - public func streamSerializer( - _ serializer: Serializer, - on queue: DispatchQueue = .main, - handler: @escaping (_ event: Event) -> Void - ) -> Self where Serializer: WebSocketMessageSerializer, Serializer.Failure == Error { - forIncomingEvent(on: queue) { incomingEvent in - let event: Event - switch incomingEvent { - case let .connected(`protocol`): - event = .init(socket: self, kind: .connected(protocol: `protocol`)) - case let .receivedMessage(message): - do { - let serializedMessage = try serializer.decode(message) - event = .init(socket: self, kind: .receivedMessage(serializedMessage)) - } catch { - event = .init(socket: self, kind: .serializerFailed(error)) - } - case let .disconnected(closeCode, reason): - event = .init(socket: self, kind: .disconnected(closeCode: closeCode, reason: reason)) - case let .completed(completion): - event = .init(socket: self, kind: .completed(completion)) - } - - queue.async { handler(event) } - } - } - - @discardableResult - public func streamDecodableEvents( - _ type: Value.Type = Value.self, - on queue: DispatchQueue = .main, - using decoder: DataDecoder = JSONDecoder(), - handler: @escaping (_ event: Event) -> Void - ) -> Self where Value: Decodable { - streamSerializer(DecodableWebSocketMessageDecoder(decoder: decoder), on: queue, handler: handler) - } - - @discardableResult - public func streamDecodable( - _ type: Value.Type = Value.self, - on queue: DispatchQueue = .main, - using decoder: DataDecoder = JSONDecoder(), - handler: @escaping (_ value: Value) -> Void - ) -> Self where Value: Decodable { - streamDecodableEvents(Value.self, on: queue) { event in - event.message.map(handler) - } - } - - @discardableResult - public func streamMessageEvents( - on queue: DispatchQueue = .main, - handler: @escaping (_ event: Event) -> Void - ) -> Self { - forIncomingEvent(on: queue) { incomingEvent in - let event: Event - switch incomingEvent { - case let .connected(`protocol`): - event = .init(socket: self, kind: .connected(protocol: `protocol`)) - case let .receivedMessage(message): - event = .init(socket: self, kind: .receivedMessage(message)) - case let .disconnected(closeCode, reason): - event = .init(socket: self, kind: .disconnected(closeCode: closeCode, reason: reason)) - case let .completed(completion): - event = .init(socket: self, kind: .completed(completion)) - } - - queue.async { handler(event) } - } - } - - @discardableResult - public func streamMessages( - on queue: DispatchQueue = .main, - handler: @escaping (_ message: URLSessionWebSocketTask.Message) -> Void - ) -> Self { - streamMessageEvents(on: queue) { event in - event.message.map(handler) - } - } - - func forIncomingEvent(on queue: DispatchQueue, handler: @escaping (IncomingEvent) -> Void) -> Self { - socketMutableState.write { state in - state.handlers.append((queue: queue, handler: { incomingEvent in - self.serializationQueue.async { - handler(incomingEvent) - } - })) - } - - appendResponseSerializer { - self.responseSerializerDidComplete { - self.serializationQueue.async { - handler(.completed(.init(request: self.request, - response: self.response, - metrics: self.metrics, - error: self.error))) - } - } - } - - return self - } - - public func send(_ message: URLSessionWebSocketTask.Message, - queue: DispatchQueue = .main, - completionHandler: @escaping (Result) -> Void) { - guard !(isCancelled || isFinished) else { return } - - guard let socket else { - // URLSessionWebSocketTask note created yet, enqueue the send. - socketMutableState.write { mutableState in - mutableState.enqueuedSends.append((message, queue, completionHandler)) - } - - return - } - - socket.send(message) { error in - queue.async { - completionHandler(Result(value: (), error: error)) - } - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol WebSocketMessageSerializer { - associatedtype Output - associatedtype Failure: Error = Error - - func decode(_ message: URLSessionWebSocketTask.Message) throws -> Output -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension WebSocketMessageSerializer { - public static func json( - decoding _: Value.Type = Value.self, - using decoder: JSONDecoder = JSONDecoder() - ) -> DecodableWebSocketMessageDecoder where Self == DecodableWebSocketMessageDecoder { - Self(decoder: decoder) - } - - static var passthrough: PassthroughWebSocketMessageDecoder { - PassthroughWebSocketMessageDecoder() - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct PassthroughWebSocketMessageDecoder: WebSocketMessageSerializer { - public typealias Failure = Never - - public func decode(_ message: URLSessionWebSocketTask.Message) -> URLSessionWebSocketTask.Message { - message - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct DecodableWebSocketMessageDecoder: WebSocketMessageSerializer { - public enum Error: Swift.Error { - case decoding(Swift.Error) - case unknownMessage(description: String) - } - - public let decoder: DataDecoder - - public init(decoder: DataDecoder) { - self.decoder = decoder - } - - public func decode(_ message: URLSessionWebSocketTask.Message) throws -> Value { - let data: Data - switch message { - case let .data(messageData): - data = messageData - case let .string(string): - data = Data(string.utf8) - @unknown default: - throw Error.unknownMessage(description: String(describing: message)) - } - - do { - return try decoder.decode(Value.self, from: data) - } catch { - throw Error.decoding(error) - } - } -} - -#endif - -// MARK: - DownloadRequest - -/// `Request` subclass which downloads `Data` to a file on disk using `URLSessionDownloadTask`. -public class DownloadRequest: Request { - /// A set of options to be executed prior to moving a downloaded file from the temporary `URL` to the destination - /// `URL`. - public struct Options: OptionSet { - /// Specifies that intermediate directories for the destination URL should be created. - public static let createIntermediateDirectories = Options(rawValue: 1 << 0) - /// Specifies that any previous file at the destination `URL` should be removed. - public static let removePreviousFile = Options(rawValue: 1 << 1) - - public let rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - } - - // MARK: Destination - - /// A closure executed once a `DownloadRequest` has successfully completed in order to determine where to move the - /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL - /// and the `HTTPURLResponse`, and returns two values: the file URL where the temporary file should be moved and - /// the options defining how the file should be moved. - /// - /// - Note: Downloads from a local `file://` `URL`s do not use the `Destination` closure, as those downloads do not - /// return an `HTTPURLResponse`. Instead the file is merely moved within the temporary directory. - public typealias Destination = (_ temporaryURL: URL, - _ response: HTTPURLResponse) -> (destinationURL: URL, options: Options) - - /// Creates a download file destination closure which uses the default file manager to move the temporary file to a - /// file URL in the first available directory with the specified search path directory and search path domain mask. - /// - /// - Parameters: - /// - directory: The search path directory. `.documentDirectory` by default. - /// - domain: The search path domain mask. `.userDomainMask` by default. - /// - options: `DownloadRequest.Options` used when moving the downloaded file to its destination. None by - /// default. - /// - Returns: The `Destination` closure. - public class func suggestedDownloadDestination(for directory: FileManager.SearchPathDirectory = .documentDirectory, - in domain: FileManager.SearchPathDomainMask = .userDomainMask, - options: Options = []) -> Destination { - { temporaryURL, response in - let directoryURLs = FileManager.default.urls(for: directory, in: domain) - let url = directoryURLs.first?.appendingPathComponent(response.suggestedFilename!) ?? temporaryURL - - return (url, options) - } - } - - /// Default `Destination` used by Alamofire to ensure all downloads persist. This `Destination` prepends - /// `Alamofire_` to the automatically generated download name and moves it within the temporary directory. Files - /// with this destination must be additionally moved if they should survive the system reclamation of temporary - /// space. - static let defaultDestination: Destination = { url, _ in - (defaultDestinationURL(url), []) - } - - /// Default `URL` creation closure. Creates a `URL` in the temporary directory with `Alamofire_` prepended to the - /// provided file name. - static let defaultDestinationURL: (URL) -> URL = { url in - let filename = "Alamofire_\(url.lastPathComponent)" - let destination = url.deletingLastPathComponent().appendingPathComponent(filename) - - return destination - } - - // MARK: Downloadable - - /// Type describing the source used to create the underlying `URLSessionDownloadTask`. - public enum Downloadable { - /// Download should be started from the `URLRequest` produced by the associated `URLRequestConvertible` value. - case request(URLRequestConvertible) - /// Download should be started from the associated resume `Data` value. - case resumeData(Data) - } - - // MARK: Mutable State - - /// Type containing all mutable state for `DownloadRequest` instances. - private struct DownloadRequestMutableState { - /// Possible resume `Data` produced when cancelling the instance. - var resumeData: Data? - /// `URL` to which `Data` is being downloaded. - var fileURL: URL? - } - - /// Protected mutable state specific to `DownloadRequest`. - private let mutableDownloadState = Protected(DownloadRequestMutableState()) - - /// If the download is resumable and is eventually cancelled or fails, this value may be used to resume the download - /// using the `download(resumingWith data:)` API. - /// - /// - Note: For more information about `resumeData`, see [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel). - public var resumeData: Data? { - #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. - return mutableDownloadState.resumeData ?? error?.downloadResumeData - #else - return mutableDownloadState.resumeData - #endif - } - - /// If the download is successful, the `URL` where the file was downloaded. - public var fileURL: URL? { mutableDownloadState.fileURL } - - // MARK: Initial State - - /// `Downloadable` value used for this instance. - public let downloadable: Downloadable - /// The `Destination` to which the downloaded file is moved. - let destination: Destination - - /// Creates a `DownloadRequest` using the provided parameters. - /// - /// - Parameters: - /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. - /// - downloadable: `Downloadable` value used to create `URLSessionDownloadTasks` for the instance. - /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. - /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets - /// `underlyingQueue`, but can be passed another queue from a `Session`. - /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. - /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. - /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request` - /// - destination: `Destination` closure used to move the downloaded file to its final location. - init(id: UUID = UUID(), - downloadable: Downloadable, - underlyingQueue: DispatchQueue, - serializationQueue: DispatchQueue, - eventMonitor: EventMonitor?, - interceptor: RequestInterceptor?, - delegate: RequestDelegate, - destination: @escaping Destination) { - self.downloadable = downloadable - self.destination = destination - - super.init(id: id, - underlyingQueue: underlyingQueue, - serializationQueue: serializationQueue, - eventMonitor: eventMonitor, - interceptor: interceptor, - delegate: delegate) - } - - override func reset() { - super.reset() - - mutableDownloadState.write { - $0.resumeData = nil - $0.fileURL = nil - } - } - - /// Called when a download has finished. - /// - /// - Parameters: - /// - task: `URLSessionTask` that finished the download. - /// - result: `Result` of the automatic move to `destination`. - func didFinishDownloading(using task: URLSessionTask, with result: Result) { - eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result) - - switch result { - case let .success(url): mutableDownloadState.fileURL = url - case let .failure(error): self.error = error - } - } - - /// Updates the `downloadProgress` using the provided values. - /// - /// - Parameters: - /// - bytesWritten: Total bytes written so far. - /// - totalBytesExpectedToWrite: Total bytes expected to write. - func updateDownloadProgress(bytesWritten: Int64, totalBytesExpectedToWrite: Int64) { - downloadProgress.totalUnitCount = totalBytesExpectedToWrite - downloadProgress.completedUnitCount += bytesWritten - - downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } - } - - override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { - session.downloadTask(with: request) - } - - /// Creates a `URLSessionTask` from the provided resume data. - /// - /// - Parameters: - /// - data: `Data` used to resume the download. - /// - session: `URLSession` used to create the `URLSessionTask`. - /// - /// - Returns: The `URLSessionTask` created. - public func task(forResumeData data: Data, using session: URLSession) -> URLSessionTask { - session.downloadTask(withResumeData: data) - } - - /// Cancels the instance. Once cancelled, a `DownloadRequest` can no longer be resumed or suspended. - /// - /// - Note: This method will NOT produce resume data. If you wish to cancel and produce resume data, use - /// `cancel(producingResumeData:)` or `cancel(byProducingResumeData:)`. - /// - /// - Returns: The instance. - @discardableResult - override public func cancel() -> Self { - cancel(producingResumeData: false) - } - - /// Cancels the instance, optionally producing resume data. Once cancelled, a `DownloadRequest` can no longer be - /// resumed or suspended. - /// - /// - Note: If `producingResumeData` is `true`, the `resumeData` property will be populated with any resume data, if - /// available. - /// - /// - Returns: The instance. - @discardableResult - public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self { - cancel(optionallyProducingResumeData: shouldProduceResumeData ? { _ in } : nil) - } - - /// Cancels the instance while producing resume data. Once cancelled, a `DownloadRequest` can no longer be resumed - /// or suspended. - /// - /// - Note: The resume data passed to the completion handler will also be available on the instance's `resumeData` - /// property. - /// - /// - Parameter completionHandler: The completion handler that is called when the download has been successfully - /// cancelled. It is not guaranteed to be called on a particular queue, so you may - /// want use an appropriate queue to perform your work. - /// - /// - Returns: The instance. - @discardableResult - public func cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void) -> Self { - cancel(optionallyProducingResumeData: completionHandler) - } - - /// Internal implementation of cancellation that optionally takes a resume data handler. If no handler is passed, - /// cancellation is performed without producing resume data. - /// - /// - Parameter completionHandler: Optional resume data handler. - /// - /// - Returns: The instance. - private func cancel(optionallyProducingResumeData completionHandler: ((_ resumeData: Data?) -> Void)?) -> Self { - mutableState.write { mutableState in - guard mutableState.state.canTransitionTo(.cancelled) else { return } - - mutableState.state = .cancelled - - underlyingQueue.async { self.didCancel() } - - guard let task = mutableState.tasks.last as? URLSessionDownloadTask, task.state != .completed else { - underlyingQueue.async { self.finish() } - return - } - - if let completionHandler { - // Resume to ensure metrics are gathered. - task.resume() - task.cancel { resumeData in - self.mutableDownloadState.resumeData = resumeData - self.underlyingQueue.async { self.didCancelTask(task) } - completionHandler(resumeData) - } - } else { - // Resume to ensure metrics are gathered. - task.resume() - task.cancel() - self.underlyingQueue.async { self.didCancelTask(task) } - } - } - - return self - } - - /// Validates the request, using the specified closure. - /// - /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. - /// - /// - Parameter validation: `Validation` closure to validate the response. - /// - /// - Returns: The instance. - @discardableResult - public func validate(_ validation: @escaping Validation) -> Self { - let validator: () -> Void = { [unowned self] in - guard error == nil, let response else { return } - - let result = validation(request, response, fileURL) - - if case let .failure(error) = result { - self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) - } - - eventMonitor?.request(self, - didValidateRequest: request, - response: response, - fileURL: fileURL, - withResult: result) - } - - validators.write { $0.append(validator) } - - return self - } -} - -// MARK: - UploadRequest - -/// `DataRequest` subclass which handles `Data` upload from memory, file, or stream using `URLSessionUploadTask`. -public class UploadRequest: DataRequest { - /// Type describing the origin of the upload, whether `Data`, file, or stream. - public enum Uploadable { - /// Upload from the provided `Data` value. - case data(Data) - /// Upload from the provided file `URL`, as well as a `Bool` determining whether the source file should be - /// automatically removed once uploaded. - case file(URL, shouldRemove: Bool) - /// Upload from the provided `InputStream`. - case stream(InputStream) - } - - // MARK: Initial State - - /// The `UploadableConvertible` value used to produce the `Uploadable` value for this instance. - public let upload: UploadableConvertible - - /// `FileManager` used to perform cleanup tasks, including the removal of multipart form encoded payloads written - /// to disk. - public let fileManager: FileManager - - // MARK: Mutable State - - /// `Uploadable` value used by the instance. - public var uploadable: Uploadable? - - /// Creates an `UploadRequest` using the provided parameters. - /// - /// - Parameters: - /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. - /// - convertible: `UploadConvertible` value used to determine the type of upload to be performed. - /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. - /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets - /// `underlyingQueue`, but can be passed another queue from a `Session`. - /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. - /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. - /// - fileManager: `FileManager` used to perform cleanup tasks, including the removal of multipart form - /// encoded payloads written to disk. - /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. - init(id: UUID = UUID(), - convertible: UploadConvertible, - underlyingQueue: DispatchQueue, - serializationQueue: DispatchQueue, - eventMonitor: EventMonitor?, - interceptor: RequestInterceptor?, - fileManager: FileManager, - delegate: RequestDelegate) { - upload = convertible - self.fileManager = fileManager - - super.init(id: id, - convertible: convertible, - underlyingQueue: underlyingQueue, - serializationQueue: serializationQueue, - eventMonitor: eventMonitor, - interceptor: interceptor, - delegate: delegate) - } - - /// Called when the `Uploadable` value has been created from the `UploadConvertible`. - /// - /// - Parameter uploadable: The `Uploadable` that was created. - func didCreateUploadable(_ uploadable: Uploadable) { - self.uploadable = uploadable - - eventMonitor?.request(self, didCreateUploadable: uploadable) - } - - /// Called when the `Uploadable` value could not be created. - /// - /// - Parameter error: `AFError` produced by the failure. - func didFailToCreateUploadable(with error: AFError) { - self.error = error - - eventMonitor?.request(self, didFailToCreateUploadableWithError: error) - - retryOrFinish(error: error) - } - - override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { - guard let uploadable else { - fatalError("Attempting to create a URLSessionUploadTask when Uploadable value doesn't exist.") - } - - switch uploadable { - case let .data(data): return session.uploadTask(with: request, from: data) - case let .file(url, _): return session.uploadTask(with: request, fromFile: url) - case .stream: return session.uploadTask(withStreamedRequest: request) - } - } - - override func reset() { - // Uploadable must be recreated on every retry. - uploadable = nil - - super.reset() - } - - /// Produces the `InputStream` from `uploadable`, if it can. - /// - /// - Note: Calling this method with a non-`.stream` `Uploadable` is a logic error and will crash. - /// - /// - Returns: The `InputStream`. - func inputStream() -> InputStream { - guard let uploadable else { - fatalError("Attempting to access the input stream but the uploadable doesn't exist.") - } - - guard case let .stream(stream) = uploadable else { - fatalError("Attempted to access the stream of an UploadRequest that wasn't created with one.") - } - - eventMonitor?.request(self, didProvideInputStream: stream) - - return stream - } - - override public func cleanup() { - defer { super.cleanup() } - - guard - let uploadable, - case let .file(url, shouldRemove) = uploadable, - shouldRemove - else { return } - - try? fileManager.removeItem(at: url) - } -} - -/// A type that can produce an `UploadRequest.Uploadable` value. -public protocol UploadableConvertible { - /// Produces an `UploadRequest.Uploadable` value from the instance. - /// - /// - Returns: The `UploadRequest.Uploadable`. - /// - Throws: Any `Error` produced during creation. - func createUploadable() throws -> UploadRequest.Uploadable -} - -extension UploadRequest.Uploadable: UploadableConvertible { - public func createUploadable() throws -> UploadRequest.Uploadable { - self - } -} - -/// A type that can be converted to an upload, whether from an `UploadRequest.Uploadable` or `URLRequestConvertible`. -public protocol UploadConvertible: UploadableConvertible & URLRequestConvertible {} diff --git a/Source/ResponseSerialization.swift b/Source/ResponseSerialization.swift deleted file mode 100644 index e4b9920bc..000000000 --- a/Source/ResponseSerialization.swift +++ /dev/null @@ -1,1270 +0,0 @@ -// -// ResponseSerialization.swift -// -// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -// MARK: Protocols - -/// The type to which all data response serializers must conform in order to serialize a response. -public protocol DataResponseSerializerProtocol { - /// The type of serialized object to be created. - associatedtype SerializedObject - - /// Serialize the response `Data` into the provided type. - /// - /// - Parameters: - /// - request: `URLRequest` which was used to perform the request, if any. - /// - response: `HTTPURLResponse` received from the server, if any. - /// - data: `Data` returned from the server, if any. - /// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request. - /// - /// - Returns: The `SerializedObject`. - /// - Throws: Any `Error` produced during serialization. - func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject -} - -/// The type to which all download response serializers must conform in order to serialize a response. -public protocol DownloadResponseSerializerProtocol { - /// The type of serialized object to be created. - associatedtype SerializedObject - - /// Serialize the downloaded response `Data` from disk into the provided type. - /// - /// - Parameters: - /// - request: `URLRequest` which was used to perform the request, if any. - /// - response: `HTTPURLResponse` received from the server, if any. - /// - fileURL: File `URL` to which the response data was downloaded. - /// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request. - /// - /// - Returns: The `SerializedObject`. - /// - Throws: Any `Error` produced during serialization. - func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> SerializedObject -} - -/// A serializer that can handle both data and download responses. -public protocol ResponseSerializer: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol { - /// `DataPreprocessor` used to prepare incoming `Data` for serialization. - var dataPreprocessor: DataPreprocessor { get } - /// `HTTPMethod`s for which empty response bodies are considered appropriate. - var emptyRequestMethods: Set { get } - /// HTTP response codes for which empty response bodies are considered appropriate. - var emptyResponseCodes: Set { get } -} - -/// Type used to preprocess `Data` before it handled by a serializer. -public protocol DataPreprocessor { - /// Process `Data` before it's handled by a serializer. - /// - Parameter data: The raw `Data` to process. - func preprocess(_ data: Data) throws -> Data -} - -/// `DataPreprocessor` that returns passed `Data` without any transform. -public struct PassthroughPreprocessor: DataPreprocessor { - public init() {} - - public func preprocess(_ data: Data) throws -> Data { data } -} - -/// `DataPreprocessor` that trims Google's typical `)]}',\n` XSSI JSON header. -public struct GoogleXSSIPreprocessor: DataPreprocessor { - public init() {} - - public func preprocess(_ data: Data) throws -> Data { - (data.prefix(6) == Data(")]}',\n".utf8)) ? data.dropFirst(6) : data - } -} - -extension DataPreprocessor where Self == PassthroughPreprocessor { - /// Provides a `PassthroughPreprocessor` instance. - public static var passthrough: PassthroughPreprocessor { PassthroughPreprocessor() } -} - -extension DataPreprocessor where Self == GoogleXSSIPreprocessor { - /// Provides a `GoogleXSSIPreprocessor` instance. - public static var googleXSSI: GoogleXSSIPreprocessor { GoogleXSSIPreprocessor() } -} - -extension ResponseSerializer { - /// Default `DataPreprocessor`. `PassthroughPreprocessor` by default. - public static var defaultDataPreprocessor: DataPreprocessor { PassthroughPreprocessor() } - /// Default `HTTPMethod`s for which empty response bodies are considered appropriate. `[.head]` by default. - public static var defaultEmptyRequestMethods: Set { [.head] } - /// HTTP response codes for which empty response bodies are considered appropriate. `[204, 205]` by default. - public static var defaultEmptyResponseCodes: Set { [204, 205] } - - public var dataPreprocessor: DataPreprocessor { Self.defaultDataPreprocessor } - public var emptyRequestMethods: Set { Self.defaultEmptyRequestMethods } - public var emptyResponseCodes: Set { Self.defaultEmptyResponseCodes } - - /// Determines whether the `request` allows empty response bodies, if `request` exists. - /// - /// - Parameter request: `URLRequest` to evaluate. - /// - /// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `request` was `nil`. - public func requestAllowsEmptyResponseData(_ request: URLRequest?) -> Bool? { - request.flatMap(\.httpMethod) - .flatMap(HTTPMethod.init) - .map { emptyRequestMethods.contains($0) } - } - - /// Determines whether the `response` allows empty response bodies, if `response` exists`. - /// - /// - Parameter response: `HTTPURLResponse` to evaluate. - /// - /// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `response` was `nil`. - public func responseAllowsEmptyResponseData(_ response: HTTPURLResponse?) -> Bool? { - response.map(\.statusCode) - .map { emptyResponseCodes.contains($0) } - } - - /// Determines whether `request` and `response` allow empty response bodies. - /// - /// - Parameters: - /// - request: `URLRequest` to evaluate. - /// - response: `HTTPURLResponse` to evaluate. - /// - /// - Returns: `true` if `request` or `response` allow empty bodies, `false` otherwise. - public func emptyResponseAllowed(forRequest request: URLRequest?, response: HTTPURLResponse?) -> Bool { - (requestAllowsEmptyResponseData(request) == true) || (responseAllowsEmptyResponseData(response) == true) - } -} - -/// By default, any serializer declared to conform to both types will get file serialization for free, as it just feeds -/// the data read from disk into the data response serializer. -extension DownloadResponseSerializerProtocol where Self: DataResponseSerializerProtocol { - public func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> Self.SerializedObject { - guard error == nil else { throw error! } - - guard let fileURL else { - throw AFError.responseSerializationFailed(reason: .inputFileNil) - } - - let data: Data - do { - data = try Data(contentsOf: fileURL) - } catch { - throw AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)) - } - - do { - return try serialize(request: request, response: response, data: data, error: error) - } catch { - throw error - } - } -} - -// MARK: - Default - -extension DataRequest { - /// Adds a handler to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - completionHandler: The code to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse) -> Void) -> Self { - appendResponseSerializer { - // Start work that should be on the serialization queue. - let result = AFResult(value: self.data, error: self.error) - // End work that should be on the serialization queue. - - self.underlyingQueue.async { - let response = DataResponse(request: self.request, - response: self.response, - data: self.data, - metrics: self.metrics, - serializationDuration: 0, - result: result) - - self.eventMonitor?.request(self, didParseResponse: response) - - self.responseSerializerDidComplete { queue.async { completionHandler(response) } } - } - } - - return self - } - - private func _response(queue: DispatchQueue = .main, - responseSerializer: Serializer, - completionHandler: @escaping (AFDataResponse) -> Void) - -> Self { - appendResponseSerializer { - // Start work that should be on the serialization queue. - let start = ProcessInfo.processInfo.systemUptime - let result: AFResult = Result { - try responseSerializer.serialize(request: self.request, - response: self.response, - data: self.data, - error: self.error) - }.mapError { error in - error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) - } - - let end = ProcessInfo.processInfo.systemUptime - // End work that should be on the serialization queue. - - self.underlyingQueue.async { - let response = DataResponse(request: self.request, - response: self.response, - data: self.data, - metrics: self.metrics, - serializationDuration: end - start, - result: result) - - self.eventMonitor?.request(self, didParseResponse: response) - - guard !self.isCancelled, let serializerError = result.failure, let delegate = self.delegate else { - self.responseSerializerDidComplete { queue.async { completionHandler(response) } } - return - } - - delegate.retryResult(for: self, dueTo: serializerError) { retryResult in - var didComplete: (() -> Void)? - - defer { - if let didComplete { - self.responseSerializerDidComplete { queue.async { didComplete() } } - } - } - - switch retryResult { - case .doNotRetry: - didComplete = { completionHandler(response) } - - case let .doNotRetryWithError(retryError): - let result: AFResult = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError")) - - let response = DataResponse(request: self.request, - response: self.response, - data: self.data, - metrics: self.metrics, - serializationDuration: end - start, - result: result) - - didComplete = { completionHandler(response) } - - case .retry, .retryWithDelay: - delegate.retryRequest(self, withDelay: retryResult.delay) - } - } - } - } - - return self - } - - /// Adds a handler to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default - /// - responseSerializer: The response serializer responsible for serializing the request, response, and data. - /// - completionHandler: The code to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func response(queue: DispatchQueue = .main, - responseSerializer: Serializer, - completionHandler: @escaping (AFDataResponse) -> Void) - -> Self { - _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) - } - - /// Adds a handler to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default - /// - responseSerializer: The response serializer responsible for serializing the request, response, and data. - /// - completionHandler: The code to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func response(queue: DispatchQueue = .main, - responseSerializer: Serializer, - completionHandler: @escaping (AFDataResponse) -> Void) - -> Self { - _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) - } -} - -extension DownloadRequest { - /// Adds a handler to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - completionHandler: The code to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func response(queue: DispatchQueue = .main, - completionHandler: @escaping (AFDownloadResponse) -> Void) - -> Self { - appendResponseSerializer { - // Start work that should be on the serialization queue. - let result = AFResult(value: self.fileURL, error: self.error) - // End work that should be on the serialization queue. - - self.underlyingQueue.async { - let response = DownloadResponse(request: self.request, - response: self.response, - fileURL: self.fileURL, - resumeData: self.resumeData, - metrics: self.metrics, - serializationDuration: 0, - result: result) - - self.eventMonitor?.request(self, didParseResponse: response) - - self.responseSerializerDidComplete { queue.async { completionHandler(response) } } - } - } - - return self - } - - private func _response(queue: DispatchQueue = .main, - responseSerializer: Serializer, - completionHandler: @escaping (AFDownloadResponse) -> Void) - -> Self { - appendResponseSerializer { - // Start work that should be on the serialization queue. - let start = ProcessInfo.processInfo.systemUptime - let result: AFResult = Result { - try responseSerializer.serializeDownload(request: self.request, - response: self.response, - fileURL: self.fileURL, - error: self.error) - }.mapError { error in - error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) - } - let end = ProcessInfo.processInfo.systemUptime - // End work that should be on the serialization queue. - - self.underlyingQueue.async { - let response = DownloadResponse(request: self.request, - response: self.response, - fileURL: self.fileURL, - resumeData: self.resumeData, - metrics: self.metrics, - serializationDuration: end - start, - result: result) - - self.eventMonitor?.request(self, didParseResponse: response) - - guard let serializerError = result.failure, let delegate = self.delegate else { - self.responseSerializerDidComplete { queue.async { completionHandler(response) } } - return - } - - delegate.retryResult(for: self, dueTo: serializerError) { retryResult in - var didComplete: (() -> Void)? - - defer { - if let didComplete { - self.responseSerializerDidComplete { queue.async { didComplete() } } - } - } - - switch retryResult { - case .doNotRetry: - didComplete = { completionHandler(response) } - - case let .doNotRetryWithError(retryError): - let result: AFResult = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError")) - - let response = DownloadResponse(request: self.request, - response: self.response, - fileURL: self.fileURL, - resumeData: self.resumeData, - metrics: self.metrics, - serializationDuration: end - start, - result: result) - - didComplete = { completionHandler(response) } - - case .retry, .retryWithDelay: - delegate.retryRequest(self, withDelay: retryResult.delay) - } - } - } - } - - return self - } - - /// Adds a handler to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - responseSerializer: The response serializer responsible for serializing the request, response, and data - /// contained in the destination `URL`. - /// - completionHandler: The code to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func response(queue: DispatchQueue = .main, - responseSerializer: Serializer, - completionHandler: @escaping (AFDownloadResponse) -> Void) - -> Self { - _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) - } - - /// Adds a handler to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - responseSerializer: The response serializer responsible for serializing the request, response, and data - /// contained in the destination `URL`. - /// - completionHandler: The code to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func response(queue: DispatchQueue = .main, - responseSerializer: Serializer, - completionHandler: @escaping (AFDownloadResponse) -> Void) - -> Self { - _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) - } -} - -// MARK: - URL - -/// A `DownloadResponseSerializerProtocol` that performs only `Error` checking and ensures that a downloaded `fileURL` -/// is present. -public struct URLResponseSerializer: DownloadResponseSerializerProtocol { - /// Creates an instance. - public init() {} - - public func serializeDownload(request: URLRequest?, - response: HTTPURLResponse?, - fileURL: URL?, - error: Error?) throws -> URL { - guard error == nil else { throw error! } - - guard let url = fileURL else { - throw AFError.responseSerializationFailed(reason: .inputFileNil) - } - - return url - } -} - -extension DownloadResponseSerializerProtocol where Self == URLResponseSerializer { - /// Provides a `URLResponseSerializer` instance. - public static var url: URLResponseSerializer { URLResponseSerializer() } -} - -extension DownloadRequest { - /// Adds a handler using a `URLResponseSerializer` to be called once the request is finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is called. `.main` by default. - /// - completionHandler: A closure to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func responseURL(queue: DispatchQueue = .main, - completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { - response(queue: queue, responseSerializer: URLResponseSerializer(), completionHandler: completionHandler) - } -} - -// MARK: - Data - -/// A `ResponseSerializer` that performs minimal response checking and returns any response `Data` as-is. By default, a -/// request returning `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the -/// response has an HTTP status code valid for empty responses, then an empty `Data` value is returned. -public final class DataResponseSerializer: ResponseSerializer { - public let dataPreprocessor: DataPreprocessor - public let emptyResponseCodes: Set - public let emptyRequestMethods: Set - - /// Creates a `DataResponseSerializer` using the provided parameters. - /// - /// - Parameters: - /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. - /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. - /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. - public init(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, - emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) { - self.dataPreprocessor = dataPreprocessor - self.emptyResponseCodes = emptyResponseCodes - self.emptyRequestMethods = emptyRequestMethods - } - - public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Data { - guard error == nil else { throw error! } - - guard var data, !data.isEmpty else { - guard emptyResponseAllowed(forRequest: request, response: response) else { - throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) - } - - return Data() - } - - data = try dataPreprocessor.preprocess(data) - - return data - } -} - -extension ResponseSerializer where Self == DataResponseSerializer { - /// Provides a default `DataResponseSerializer` instance. - public static var data: DataResponseSerializer { DataResponseSerializer() } - - /// Creates a `DataResponseSerializer` using the provided parameters. - /// - /// - Parameters: - /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. - /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. - /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. - /// - /// - Returns: The `DataResponseSerializer`. - public static func data(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, - emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponseSerializer { - DataResponseSerializer(dataPreprocessor: dataPreprocessor, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods) - } -} - -extension DataRequest { - /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is called. `.main` by default. - /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the - /// `completionHandler`. `PassthroughPreprocessor()` by default. - /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. - /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. - /// - completionHandler: A closure to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func responseData(queue: DispatchQueue = .main, - dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, - emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods, - completionHandler: @escaping (AFDataResponse) -> Void) -> Self { - response(queue: queue, - responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods), - completionHandler: completionHandler) - } -} - -extension DownloadRequest { - /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is called. `.main` by default. - /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the - /// `completionHandler`. `PassthroughPreprocessor()` by default. - /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. - /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. - /// - completionHandler: A closure to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func responseData(queue: DispatchQueue = .main, - dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, - emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods, - completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { - response(queue: queue, - responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods), - completionHandler: completionHandler) - } -} - -// MARK: - String - -/// A `ResponseSerializer` that decodes the response data as a `String`. By default, a request returning `nil` or no -/// data is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code -/// valid for empty responses, then an empty `String` is returned. -public final class StringResponseSerializer: ResponseSerializer { - public let dataPreprocessor: DataPreprocessor - /// Optional string encoding used to validate the response. - public let encoding: String.Encoding? - public let emptyResponseCodes: Set - public let emptyRequestMethods: Set - - /// Creates an instance with the provided values. - /// - /// - Parameters: - /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. - /// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined - /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. - /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. - /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. - public init(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, - encoding: String.Encoding? = nil, - emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) { - self.dataPreprocessor = dataPreprocessor - self.encoding = encoding - self.emptyResponseCodes = emptyResponseCodes - self.emptyRequestMethods = emptyRequestMethods - } - - public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String { - guard error == nil else { throw error! } - - guard var data, !data.isEmpty else { - guard emptyResponseAllowed(forRequest: request, response: response) else { - throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) - } - - return "" - } - - data = try dataPreprocessor.preprocess(data) - - var convertedEncoding = encoding - - if let encodingName = response?.textEncodingName, convertedEncoding == nil { - convertedEncoding = String.Encoding(ianaCharsetName: encodingName) - } - - let actualEncoding = convertedEncoding ?? .isoLatin1 - - guard let string = String(data: data, encoding: actualEncoding) else { - throw AFError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding)) - } - - return string - } -} - -extension ResponseSerializer where Self == StringResponseSerializer { - /// Provides a default `StringResponseSerializer` instance. - public static var string: StringResponseSerializer { StringResponseSerializer() } - - /// Creates a `StringResponseSerializer` with the provided values. - /// - /// - Parameters: - /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. - /// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined - /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. - /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. - /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. - /// - /// - Returns: The `StringResponseSerializer`. - public static func string(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, - encoding: String.Encoding? = nil, - emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> StringResponseSerializer { - StringResponseSerializer(dataPreprocessor: dataPreprocessor, - encoding: encoding, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods) - } -} - -extension DataRequest { - /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the - /// `completionHandler`. `PassthroughPreprocessor()` by default. - /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined - /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. - /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. - /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. - /// - completionHandler: A closure to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func responseString(queue: DispatchQueue = .main, - dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, - encoding: String.Encoding? = nil, - emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods, - completionHandler: @escaping (AFDataResponse) -> Void) -> Self { - response(queue: queue, - responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, - encoding: encoding, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods), - completionHandler: completionHandler) - } -} - -extension DownloadRequest { - /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the - /// `completionHandler`. `PassthroughPreprocessor()` by default. - /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined - /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. - /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. - /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. - /// - completionHandler: A closure to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func responseString(queue: DispatchQueue = .main, - dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, - encoding: String.Encoding? = nil, - emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods, - completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { - response(queue: queue, - responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, - encoding: encoding, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods), - completionHandler: completionHandler) - } -} - -// MARK: - JSON - -/// A `ResponseSerializer` that decodes the response data using `JSONSerialization`. By default, a request returning -/// `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the response has an -/// HTTP status code valid for empty responses, then an `NSNull` value is returned. -@available(*, deprecated, message: "JSONResponseSerializer deprecated and will be removed in Alamofire 6. Use DecodableResponseSerializer instead.") -public final class JSONResponseSerializer: ResponseSerializer { - public let dataPreprocessor: DataPreprocessor - public let emptyResponseCodes: Set - public let emptyRequestMethods: Set - /// `JSONSerialization.ReadingOptions` used when serializing a response. - public let options: JSONSerialization.ReadingOptions - - /// Creates an instance with the provided values. - /// - /// - Parameters: - /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. - /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. - /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. - /// - options: The options to use. `.allowFragments` by default. - public init(dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, - emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, - options: JSONSerialization.ReadingOptions = .allowFragments) { - self.dataPreprocessor = dataPreprocessor - self.emptyResponseCodes = emptyResponseCodes - self.emptyRequestMethods = emptyRequestMethods - self.options = options - } - - public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Any { - guard error == nil else { throw error! } - - guard var data, !data.isEmpty else { - guard emptyResponseAllowed(forRequest: request, response: response) else { - throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) - } - - return NSNull() - } - - data = try dataPreprocessor.preprocess(data) - - do { - return try JSONSerialization.jsonObject(with: data, options: options) - } catch { - throw AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)) - } - } -} - -extension DataRequest { - /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the - /// `completionHandler`. `PassthroughPreprocessor()` by default. - /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. - /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. - /// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments` - /// by default. - /// - completionHandler: A closure to be executed once the request has finished. - /// - /// - Returns: The request. - @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") - @discardableResult - public func responseJSON(queue: DispatchQueue = .main, - dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, - emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, - options: JSONSerialization.ReadingOptions = .allowFragments, - completionHandler: @escaping (AFDataResponse) -> Void) -> Self { - response(queue: queue, - responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods, - options: options), - completionHandler: completionHandler) - } -} - -extension DownloadRequest { - /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. - /// - /// - Parameters: - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the - /// `completionHandler`. `PassthroughPreprocessor()` by default. - /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. - /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. - /// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments` - /// by default. - /// - completionHandler: A closure to be executed once the request has finished. - /// - /// - Returns: The request. - @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") - @discardableResult - public func responseJSON(queue: DispatchQueue = .main, - dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, - emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, - options: JSONSerialization.ReadingOptions = .allowFragments, - completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { - response(queue: queue, - responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods, - options: options), - completionHandler: completionHandler) - } -} - -// MARK: - Empty - -/// Protocol representing an empty response. Use `T.emptyValue()` to get an instance. -public protocol EmptyResponse { - /// Empty value for the conforming type. - /// - /// - Returns: Value of `Self` to use for empty values. - static func emptyValue() -> Self -} - -/// Type representing an empty value. Use `Empty.value` to get the static instance. -public struct Empty: Codable, Sendable { - /// Static `Empty` instance used for all `Empty` responses. - public static let value = Empty() -} - -extension Empty: EmptyResponse { - public static func emptyValue() -> Empty { - value - } -} - -// MARK: - DataDecoder Protocol - -/// Any type which can decode `Data` into a `Decodable` type. -public protocol DataDecoder { - /// Decode `Data` into the provided type. - /// - /// - Parameters: - /// - type: The `Type` to be decoded. - /// - data: The `Data` to be decoded. - /// - /// - Returns: The decoded value of type `D`. - /// - Throws: Any error that occurs during decode. - func decode(_ type: D.Type, from data: Data) throws -> D -} - -/// `JSONDecoder` automatically conforms to `DataDecoder`. -extension JSONDecoder: DataDecoder {} -/// `PropertyListDecoder` automatically conforms to `DataDecoder`. -extension PropertyListDecoder: DataDecoder {} - -// MARK: - Decodable - -/// A `ResponseSerializer` that decodes the response data as a generic value using any type that conforms to -/// `DataDecoder`. By default, this is an instance of `JSONDecoder`. Additionally, a request returning `nil` or no data -/// is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code valid -/// for empty responses then an empty value will be returned. If the decoded type conforms to `EmptyResponse`, the -/// type's `emptyValue()` will be returned. If the decoded type is `Empty`, the `.value` instance is returned. If the -/// decoded type *does not* conform to `EmptyResponse` and isn't `Empty`, an error will be produced. -public final class DecodableResponseSerializer: ResponseSerializer { - public let dataPreprocessor: DataPreprocessor - /// The `DataDecoder` instance used to decode responses. - public let decoder: DataDecoder - public let emptyResponseCodes: Set - public let emptyRequestMethods: Set - - /// Creates an instance using the values provided. - /// - /// - Parameters: - /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. - /// - decoder: The `DataDecoder`. `JSONDecoder()` by default. - /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. - /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. - public init(dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, - decoder: DataDecoder = JSONDecoder(), - emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) { - self.dataPreprocessor = dataPreprocessor - self.decoder = decoder - self.emptyResponseCodes = emptyResponseCodes - self.emptyRequestMethods = emptyRequestMethods - } - - public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T { - guard error == nil else { throw error! } - - guard var data, !data.isEmpty else { - guard emptyResponseAllowed(forRequest: request, response: response) else { - throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) - } - - guard let emptyResponseType = T.self as? EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else { - throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)")) - } - - return emptyValue - } - - data = try dataPreprocessor.preprocess(data) - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) - } - } -} - -extension ResponseSerializer { - /// Creates a `DecodableResponseSerializer` using the values provided. - /// - /// - Parameters: - /// - type: `Decodable` type to decode from response data. - /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. - /// - decoder: The `DataDecoder`. `JSONDecoder()` by default. - /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. - /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. - /// - /// - Returns: The `DecodableResponseSerializer`. - public static func decodable(of type: T.Type, - dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, - decoder: DataDecoder = JSONDecoder(), - emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DecodableResponseSerializer where Self == DecodableResponseSerializer { - DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, - decoder: decoder, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods) - } -} - -extension DataRequest { - /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. - /// - /// - Parameters: - /// - type: `Decodable` type to decode from response data. - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the - /// `completionHandler`. `PassthroughPreprocessor()` by default. - /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. - /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. - /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. - /// - completionHandler: A closure to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func responseDecodable(of type: T.Type = T.self, - queue: DispatchQueue = .main, - dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, - decoder: DataDecoder = JSONDecoder(), - emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, - completionHandler: @escaping (AFDataResponse) -> Void) -> Self { - response(queue: queue, - responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, - decoder: decoder, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods), - completionHandler: completionHandler) - } -} - -extension DownloadRequest { - /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. - /// - /// - Parameters: - /// - type: `Decodable` type to decode from response data. - /// - queue: The queue on which the completion handler is dispatched. `.main` by default. - /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the - /// `completionHandler`. `PassthroughPreprocessor()` by default. - /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. - /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. - /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. - /// - completionHandler: A closure to be executed once the request has finished. - /// - /// - Returns: The request. - @discardableResult - public func responseDecodable(of type: T.Type = T.self, - queue: DispatchQueue = .main, - dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, - decoder: DataDecoder = JSONDecoder(), - emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, - emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, - completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { - response(queue: queue, - responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, - decoder: decoder, - emptyResponseCodes: emptyResponseCodes, - emptyRequestMethods: emptyRequestMethods), - completionHandler: completionHandler) - } -} - -// MARK: - DataStreamRequest - -/// A type which can serialize incoming `Data`. -public protocol DataStreamSerializer { - /// Type produced from the serialized `Data`. - associatedtype SerializedObject - - /// Serializes incoming `Data` into a `SerializedObject` value. - /// - /// - Parameter data: `Data` to be serialized. - /// - /// - Throws: Any error produced during serialization. - func serialize(_ data: Data) throws -> SerializedObject -} - -/// `DataStreamSerializer` which uses the provided `DataPreprocessor` and `DataDecoder` to serialize the incoming `Data`. -public struct DecodableStreamSerializer: DataStreamSerializer { - /// `DataDecoder` used to decode incoming `Data`. - public let decoder: DataDecoder - /// `DataPreprocessor` incoming `Data` is passed through before being passed to the `DataDecoder`. - public let dataPreprocessor: DataPreprocessor - - /// Creates an instance with the provided `DataDecoder` and `DataPreprocessor`. - /// - Parameters: - /// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default. - /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the - /// `decoder`. `PassthroughPreprocessor()` by default. - public init(decoder: DataDecoder = JSONDecoder(), dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) { - self.decoder = decoder - self.dataPreprocessor = dataPreprocessor - } - - public func serialize(_ data: Data) throws -> T { - let processedData = try dataPreprocessor.preprocess(data) - do { - return try decoder.decode(T.self, from: processedData) - } catch { - throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) - } - } -} - -/// `DataStreamSerializer` which performs no serialization on incoming `Data`. -public struct PassthroughStreamSerializer: DataStreamSerializer { - /// Creates an instance. - public init() {} - - public func serialize(_ data: Data) throws -> Data { data } -} - -/// `DataStreamSerializer` which serializes incoming stream `Data` into `UTF8`-decoded `String` values. -public struct StringStreamSerializer: DataStreamSerializer { - /// Creates an instance. - public init() {} - - public func serialize(_ data: Data) throws -> String { - String(decoding: data, as: UTF8.self) - } -} - -extension DataStreamSerializer { - /// Creates a `DecodableStreamSerializer` instance with the provided `DataDecoder` and `DataPreprocessor`. - /// - /// - Parameters: - /// - type: `Decodable` type to decode from stream data. - /// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default. - /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the - /// `decoder`. `PassthroughPreprocessor()` by default. - public static func decodable(of type: T.Type, - decoder: DataDecoder = JSONDecoder(), - dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) -> Self where Self == DecodableStreamSerializer { - DecodableStreamSerializer(decoder: decoder, dataPreprocessor: dataPreprocessor) - } -} - -extension DataStreamSerializer where Self == PassthroughStreamSerializer { - /// Provides a `PassthroughStreamSerializer` instance. - public static var passthrough: PassthroughStreamSerializer { PassthroughStreamSerializer() } -} - -extension DataStreamSerializer where Self == StringStreamSerializer { - /// Provides a `StringStreamSerializer` instance. - public static var string: StringStreamSerializer { StringStreamSerializer() } -} - -extension DataStreamRequest { - /// Adds a `StreamHandler` which performs no parsing on incoming `Data`. - /// - /// - Parameters: - /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. - /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. - /// - /// - Returns: The `DataStreamRequest`. - @discardableResult - public func responseStream(on queue: DispatchQueue = .main, stream: @escaping Handler) -> Self { - let parser = { [unowned self] (data: Data) in - queue.async { - self.capturingError { - try stream(.init(event: .stream(.success(data)), token: .init(self))) - } - - self.updateAndCompleteIfPossible() - } - } - - streamMutableState.write { $0.streams.append(parser) } - appendStreamCompletion(on: queue, stream: stream) - - return self - } - - /// Adds a `StreamHandler` which uses the provided `DataStreamSerializer` to process incoming `Data`. - /// - /// - Parameters: - /// - serializer: `DataStreamSerializer` used to process incoming `Data`. Its work is done on the `serializationQueue`. - /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. - /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. - /// - /// - Returns: The `DataStreamRequest`. - @discardableResult - public func responseStream(using serializer: Serializer, - on queue: DispatchQueue = .main, - stream: @escaping Handler) -> Self { - let parser = { [unowned self] (data: Data) in - serializationQueue.async { - // Start work on serialization queue. - let result = Result { try serializer.serialize(data) } - .mapError { $0.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: $0))) } - // End work on serialization queue. - self.underlyingQueue.async { - self.eventMonitor?.request(self, didParseStream: result) - - if result.isFailure, self.automaticallyCancelOnStreamError { - self.cancel() - } - - queue.async { - self.capturingError { - try stream(.init(event: .stream(result), token: .init(self))) - } - - self.updateAndCompleteIfPossible() - } - } - } - } - - streamMutableState.write { $0.streams.append(parser) } - appendStreamCompletion(on: queue, stream: stream) - - return self - } - - /// Adds a `StreamHandler` which parses incoming `Data` as a UTF8 `String`. - /// - /// - Parameters: - /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. - /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. - /// - /// - Returns: The `DataStreamRequest`. - @discardableResult - public func responseStreamString(on queue: DispatchQueue = .main, - stream: @escaping Handler) -> Self { - let parser = { [unowned self] (data: Data) in - serializationQueue.async { - // Start work on serialization queue. - let string = String(decoding: data, as: UTF8.self) - // End work on serialization queue. - self.underlyingQueue.async { - self.eventMonitor?.request(self, didParseStream: .success(string)) - - queue.async { - self.capturingError { - try stream(.init(event: .stream(.success(string)), token: .init(self))) - } - - self.updateAndCompleteIfPossible() - } - } - } - } - - streamMutableState.write { $0.streams.append(parser) } - appendStreamCompletion(on: queue, stream: stream) - - return self - } - - private func updateAndCompleteIfPossible() { - streamMutableState.write { state in - state.numberOfExecutingStreams -= 1 - - guard state.numberOfExecutingStreams == 0, !state.enqueuedCompletionEvents.isEmpty else { return } - - let completionEvents = state.enqueuedCompletionEvents - self.underlyingQueue.async { completionEvents.forEach { $0() } } - state.enqueuedCompletionEvents.removeAll() - } - } - - /// Adds a `StreamHandler` which parses incoming `Data` using the provided `DataDecoder`. - /// - /// - Parameters: - /// - type: `Decodable` type to parse incoming `Data` into. - /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. - /// - decoder: `DataDecoder` used to decode the incoming `Data`. - /// - preprocessor: `DataPreprocessor` used to process the incoming `Data` before it's passed to the `decoder`. - /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. - /// - /// - Returns: The `DataStreamRequest`. - @discardableResult - public func responseStreamDecodable(of type: T.Type = T.self, - on queue: DispatchQueue = .main, - using decoder: DataDecoder = JSONDecoder(), - preprocessor: DataPreprocessor = PassthroughPreprocessor(), - stream: @escaping Handler) -> Self { - responseStream(using: DecodableStreamSerializer(decoder: decoder, dataPreprocessor: preprocessor), - stream: stream) - } -} diff --git a/Tests/ConcurrencyTests.swift b/Tests/ConcurrencyTests.swift index c52dd004a..06bba13d2 100644 --- a/Tests/ConcurrencyTests.swift +++ b/Tests/ConcurrencyTests.swift @@ -816,7 +816,7 @@ final class ClosureAPIConcurrencyTests: BaseTestCase { tasks: [URLSessionTask], descriptions: [String], response: AFDataResponse) - #if swift(>=5.10) + #if swift(>=5.11) values = try! await (httpResponses, uploadProgress, downloadProgress, requests, tasks, descriptions, response) #else values = await (httpResponses, uploadProgress, downloadProgress, requests, tasks, descriptions, response) diff --git a/Tests/DownloadTests.swift b/Tests/DownloadTests.swift index a2b7c6378..a7de0ab95 100644 --- a/Tests/DownloadTests.swift +++ b/Tests/DownloadTests.swift @@ -268,15 +268,13 @@ final class DownloadResponseTests: BaseTestCase { XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) - if - let data = try? Data(contentsOf: fileURL), - let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), - let json = jsonObject as? [String: Any], - let headers = json["headers"] as? [String: String] { - XCTAssertEqual(headers["Authorization"], "123456") - } else { + guard let data = try? Data(contentsOf: fileURL), + let response = try? JSONDecoder().decode(TestResponse.self, from: data) else { XCTFail("headers parameter in JSON should not be nil") + return } + + XCTAssertEqual(response.headers["Authorization"], "123456") } func testThatDownloadingFileAndMovingToDirectoryThatDoesNotExistThrowsError() { @@ -748,7 +746,7 @@ final class DownloadResponseMapTestCase: BaseTestCase { // When AF.download(.get, parameters: ["foo": "bar"]).responseDecodable(of: TestResponse.self) { resp in response = resp.map { response in - response.args?["foo"] ?? "invalid" + response.args["foo"] ?? "invalid" } expectation.fulfill() @@ -804,7 +802,7 @@ final class DownloadResponseTryMapTestCase: BaseTestCase { // When AF.download(.get, parameters: ["foo": "bar"]).responseDecodable(of: TestResponse.self) { resp in response = resp.tryMap { response in - response.args?["foo"] ?? "invalid" + response.args["foo"] ?? "invalid" } expectation.fulfill() diff --git a/Tests/ResponseTests.swift b/Tests/ResponseTests.swift index 144bd7b57..b04311bad 100644 --- a/Tests/ResponseTests.swift +++ b/Tests/ResponseTests.swift @@ -376,7 +376,7 @@ final class ResponseMapTestCase: BaseTestCase { // When AF.request(.default, parameters: ["foo": "bar"]).responseDecodable(of: TestResponse.self) { resp in response = resp.map { response in - response.args?["foo"] ?? "invalid" + response.args["foo"] ?? "invalid" } expectation.fulfill() @@ -430,7 +430,7 @@ final class ResponseTryMapTestCase: BaseTestCase { // When AF.request(.default, parameters: ["foo": "bar"]).responseDecodable(of: TestResponse.self) { resp in response = resp.tryMap { response in - response.args?["foo"] ?? "invalid" + response.args["foo"] ?? "invalid" } expectation.fulfill() diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index ac8fda94a..60ba40e23 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -161,23 +161,35 @@ final class SessionTestCase: BaseTestCase { } private class UploadHandler: RequestInterceptor { - var adaptCalledCount = 0 - var adaptedCount = 0 - var retryCalledCount = 0 - var retryCount = 0 - var retryErrors: [Error] = [] + struct MutableState { + var adaptCalledCount = 0 + var adaptedCount = 0 + var retryCalledCount = 0 + var retryCount = 0 + var retryErrors: [Error] = [] + } + + private let mutableState = Protected(MutableState()) + + var adaptCalledCount: Int { mutableState.adaptCalledCount } + var adaptedCount: Int { mutableState.adaptedCount } + var retryCalledCount: Int { mutableState.retryCalledCount } + var retryCount: Int { mutableState.retryCount } + var retryErrors: [Error] { mutableState.retryErrors } func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { - adaptCalledCount += 1 + let result: Result = mutableState.write { mutableState in + mutableState.adaptCalledCount += 1 - let result: Result = Result { - adaptedCount += 1 + return Result { + mutableState.adaptedCount += 1 - if adaptedCount == 1 { throw AFError.invalidURL(url: "") } + if mutableState.adaptedCount == 1 { throw AFError.invalidURL(url: "") } - return urlRequest + return urlRequest + } } completion(result) @@ -187,10 +199,12 @@ final class SessionTestCase: BaseTestCase { for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { - retryCalledCount += 1 + mutableState.write { mutableState in + mutableState.retryCalledCount += 1 - retryCount += 1 - retryErrors.append(error) + mutableState.retryCount += 1 + mutableState.retryErrors.append(error) + } completion(.retry) } @@ -310,9 +324,17 @@ final class SessionTestCase: BaseTestCase { #elseif os(tvOS) return "tvOS" #elseif os(macOS) + #if targetEnvironment(macCatalyst) + return "macOS(Catalyst)" + #else return "macOS" + #endif + #elseif swift(>=5.9.2) && os(visionOS) + return "visionOS" #elseif os(Linux) return "Linux" + #elseif os(Windows) + return "Windows" #elseif os(Android) return "Android" #else @@ -323,7 +345,7 @@ final class SessionTestCase: BaseTestCase { return "\(osName) \(versionString)" }() - let alamofireVersion = "Alamofire/\(Alamofire.version)" + let alamofireVersion = "Alamofire/\(AFInfo.version)" XCTAssertTrue(userAgent?.contains(alamofireVersion) == true) XCTAssertTrue(userAgent?.contains(osNameVersion) == true) diff --git a/Tests/TestHelpers.swift b/Tests/TestHelpers.swift index 3e266f273..f8349dae9 100644 --- a/Tests/TestHelpers.swift +++ b/Tests/TestHelpers.swift @@ -467,12 +467,37 @@ extension Data { } struct TestResponse: Decodable { - let headers: [String: String] + let headers: HTTPHeaders let origin: String - let url: String? + let url: String let data: String? let form: [String: String]? - let args: [String: String]? + let args: [String: String] +} + +extension HTTPHeaders: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + let headers = try container.decode([HTTPHeader].self) + + self = .init(headers) + } +} + +extension HTTPHeader: Decodable { + enum CodingKeys: String, CodingKey { + case name, value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let name = try container.decode(String.self, forKey: .name) + let value = try container.decode(String.self, forKey: .value) + + self = .init(name: name, value: value) + } } struct TestParameters: Encodable { diff --git a/Tests/ValidationTests.swift b/Tests/ValidationTests.swift index b5abb43a9..f86a377a6 100644 --- a/Tests/ValidationTests.swift +++ b/Tests/ValidationTests.swift @@ -388,115 +388,6 @@ final class ContentTypeValidationTestCase: BaseTestCase { XCTAssertNil(requestError) XCTAssertNil(downloadError) } - - func testThatValidationForRequestWithAcceptableWildcardContentTypeResponseSucceedsWhenResponseIsNil() { - // Given - class MockManager: Session { - override func request(_ convertible: URLRequestConvertible, - interceptor: RequestInterceptor? = nil) -> DataRequest { - let request = MockDataRequest(convertible: convertible, - underlyingQueue: rootQueue, - serializationQueue: serializationQueue, - eventMonitor: eventMonitor, - interceptor: interceptor, - delegate: self) - - perform(request) - - return request - } - - override func download(_ convertible: URLRequestConvertible, - interceptor: RequestInterceptor? = nil, - to destination: DownloadRequest.Destination?) - -> DownloadRequest { - let request = MockDownloadRequest(downloadable: .request(convertible), - underlyingQueue: rootQueue, - serializationQueue: serializationQueue, - eventMonitor: eventMonitor, - interceptor: interceptor, - delegate: self, - destination: destination ?? MockDownloadRequest.defaultDestination) - - perform(request) - - return request - } - } - - class MockDataRequest: DataRequest { - override var response: HTTPURLResponse? { - MockHTTPURLResponse(url: request!.url!, - statusCode: 204, - httpVersion: "HTTP/1.1", - headerFields: nil) - } - } - - class MockDownloadRequest: DownloadRequest { - override var response: HTTPURLResponse? { - MockHTTPURLResponse(url: request!.url!, - statusCode: 204, - httpVersion: "HTTP/1.1", - headerFields: nil) - } - } - - class MockHTTPURLResponse: HTTPURLResponse { - override var mimeType: String? { nil } - } - - let manager: Session = { - let configuration: URLSessionConfiguration = { - let configuration = URLSessionConfiguration.ephemeral - configuration.headers = HTTPHeaders.default - - return configuration - }() - - return MockManager(configuration: configuration) - }() - - let endpoint = Endpoint.method(.delete) - - let expectation1 = expectation(description: "request should be stubbed and return 204 status code") - let expectation2 = expectation(description: "download should be stubbed and return 204 status code") - - var requestResponse: DataResponse? - var downloadResponse: DownloadResponse? - - // When - manager.request(endpoint) - .validate(contentType: ["*/*"]) - .response { resp in - requestResponse = resp - expectation1.fulfill() - } - - manager.download(endpoint) - .validate(contentType: ["*/*"]) - .response { resp in - downloadResponse = resp - expectation2.fulfill() - } - - waitForExpectations(timeout: timeout) - - // Then - XCTAssertNotNil(requestResponse?.response) - XCTAssertNotNil(requestResponse?.data) - XCTAssertNil(requestResponse?.error) - - XCTAssertEqual(requestResponse?.response?.statusCode, 204) - XCTAssertNil(requestResponse?.response?.mimeType) - - XCTAssertNotNil(downloadResponse?.response) - XCTAssertNotNil(downloadResponse?.fileURL) - XCTAssertNil(downloadResponse?.error) - - XCTAssertEqual(downloadResponse?.response?.statusCode, 204) - XCTAssertNil(downloadResponse?.response?.mimeType) - } } // MARK: - diff --git a/watchOS Example/watchOS Example.xcodeproj/xcshareddata/xcschemes/watchOS Example WatchKit App.xcscheme b/watchOS Example/watchOS Example.xcodeproj/xcshareddata/xcschemes/watchOS Example WatchKit App.xcscheme index fd88b0196..ebf9d204d 100644 --- a/watchOS Example/watchOS Example.xcodeproj/xcshareddata/xcschemes/watchOS Example WatchKit App.xcscheme +++ b/watchOS Example/watchOS Example.xcodeproj/xcshareddata/xcschemes/watchOS Example WatchKit App.xcscheme @@ -1,6 +1,6 @@