Преглед изворни кода

Add RequestModifier for Top Level APIs (#3137)

* Adopt 5.1 formatting.

* Turn Protector into Protected property wrapper, add tests.

* Update README for Swift 5.1 / Xcode 11 requirement.

* Formatting.

* Fix build after master merge.

* Build fixes after master merge.

* Add custom timeout parameter.

* Add RequestModifier.

* Fix build after merge.

* Add inline docs.

* Restructure tests, remove old doc comments.

* Add more tests.

* Add documentation.

* Fix typo.

* Adjust ToCs.

* Add inline docs, docs for where the modifers are in the pipeline.
Jon Shier пре 5 година
родитељ
комит
b18b808a3b

+ 8 - 0
Alamofire.xcodeproj/project.pbxproj

@@ -113,6 +113,9 @@
 		319917BA209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; };
 		319917BB209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; };
 		319917BC209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; };
+		31B51E8C2434FECB005356DB /* RequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */; };
+		31B51E8D2434FECB005356DB /* RequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */; };
+		31B51E8E2434FECB005356DB /* RequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */; };
 		31C2B0EA20B271040089BA7C /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; };
 		31C2B0EB20B271050089BA7C /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; };
 		31C2B0EC20B271060089BA7C /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; };
@@ -383,6 +386,7 @@
 		319917A9209CDCB000103A19 /* HTTPHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaders.swift; sourceTree = "<group>"; };
 		319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperationQueue+Alamofire.swift"; sourceTree = "<group>"; };
 		31B2CA9521AA25CD005B371A /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
+		31B51E8B2434FECB005356DB /* RequestModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestModifierTests.swift; sourceTree = "<group>"; };
 		31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLConvertible+URLRequestConvertible.swift"; sourceTree = "<group>"; };
 		31DADDFA224811ED0051390F /* AlamofireExtended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlamofireExtended.swift; sourceTree = "<group>"; };
 		31ED52E61D73889D00199085 /* AFError+AlamofireTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AFError+AlamofireTests.swift"; sourceTree = "<group>"; };
@@ -545,6 +549,7 @@
 				31425AC0241F098000EE3CCC /* InternalRequestTests.swift */,
 				31501E872196962A005829F2 /* ParameterEncoderTests.swift */,
 				F8111E5C19A9674D0040E7D1 /* ParameterEncodingTests.swift */,
+				31B51E8B2434FECB005356DB /* RequestModifierTests.swift */,
 				F8111E5D19A9674D0040E7D1 /* RequestTests.swift */,
 				F8111E5E19A9674D0040E7D1 /* ResponseTests.swift */,
 				4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */,
@@ -1318,6 +1323,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				31B51E8E2434FECB005356DB /* RequestModifierTests.swift in Sources */,
 				4CF627181BA7CC240011A099 /* RequestTests.swift in Sources */,
 				3111CE9720A7EC3A008315E2 /* ServerTrustEvaluatorTests.swift in Sources */,
 				3111CE9420A7EC32008315E2 /* ResponseSerializationTests.swift in Sources */,
@@ -1476,6 +1482,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				31B51E8C2434FECB005356DB /* RequestModifierTests.swift in Sources */,
 				31ED52E81D73891B00199085 /* AFError+AlamofireTests.swift in Sources */,
 				3111CE9520A7EC39008315E2 /* ServerTrustEvaluatorTests.swift in Sources */,
 				3111CE9220A7EC30008315E2 /* ResponseSerializationTests.swift in Sources */,
@@ -1514,6 +1521,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				31B51E8D2434FECB005356DB /* RequestModifierTests.swift in Sources */,
 				31ED52E91D73891C00199085 /* AFError+AlamofireTests.swift in Sources */,
 				3111CE9620A7EC3A008315E2 /* ServerTrustEvaluatorTests.swift in Sources */,
 				3111CE9320A7EC31008315E2 /* ResponseSerializationTests.swift in Sources */,

+ 86 - 68
Documentation/AdvancedUsage.md

@@ -1,67 +1,69 @@
-- [Advanced Usage](#advanced-usage)
-  * [`Session`](#session)
-	+ [Creating Custom `Session` Instances](#creating-custom-session-instances)
-	  - [Creating a `Session` With a `URLSessionConfiguration`](#creating-a-session-with-a-urlsessionconfiguration)
-	+ [`SessionDelegate`](#sessiondelegate)
-	+ [`startRequestsImmediately`](#startrequestsimmediately)
-	+ [A `Session`’s `DispatchQueue`s](#a-sessions-dispatchqueues)
-	+ [Adding a `RequestInterceptor`](#adding-a-requestinterceptor)
-	+ [Adding a `ServerTrustManager`](#adding-a-servertrustmanager)
-	+ [Adding a `RedirectHandler`](#adding-a-redirecthandler)
-	+ [Adding a `CachedResponseHandler`](#adding-a-cachedresponsehandler)
-	+ [Adding `EventMonitor`s](#adding-eventmonitors)
-	+ [Creating Instances From `URLSession`s](#creating-instances-from-urlsessions)
-  * [Requests](#requests)
-	+ [The Request Pipeline](#the-request-pipeline)
-	+ [`Request`](#request)
-	  - [State](#state)
-	  - [Progress](#progress)
-	  - [Handling Redirects](#handling-redirects)
-	  - [Customizing Caching](#customizing-caching)
-	  - [Credentials](#credentials)
-	  - [A `Request`’s `URLRequest`s](#a-requests-urlrequests)
-	  - [`URLSessionTask`s](#urlsessiontasks)
-	  - [Response](#response)
-	  - [`URLSessionTaskMetrics`](#urlsessiontaskmetrics)
-	+ [`DataRequest`](#datarequest)
-	  - [Additional State](#additional-state)
-	  - [Validation](#validation)
-	+ [`UploadRequest`](#uploadrequest)
-	  - [Additional State](#additional-state-1)
-	+ [`DownloadRequest`](#downloadrequest)
-	  - [Additional State](#additional-state-2)
-	  - [Cancellation](#cancellation)
-	  - [Validation](#validation-1)
-  * [Adapting and Retrying Requests with `RequestInterceptor`](#adapting-and-retrying-requests-with-requestinterceptor)
-	+ [`RequestAdapter`](#requestadapter)
-	+ [`RequestRetrier`](#requestretrier)
-  * [Security](#security)
-	+ [Evaluating Server Trusts with `ServerTrustManager` and `ServerTrustEvaluating`](#evaluating-server-trusts-with-servertrustmanager-and-servertrustevaluating)
-	  - [`ServerTrustEvaluting`](#servertrustevaluting)
-	  - [`ServerTrustManager`](#servertrustmanager)
-		* [Subclassing Server Trust Policy Manager](#subclassing-server-trust-policy-manager)
-	+ [App Transport Security](#app-transport-security)
-	  - [Using Self-Signed Certificates with Local Networking](#using-self-signed-certificates-with-local-networking)
-  * [Customizing Caching and Redirect Handling](#customizing-caching-and-redirect-handling)
-	+ [`CachedResponseHandler`](#cachedresponsehandler)
-	+ [`RedirectHandler`](#redirecthandler)
-  * [Using `EventMonitor`s](#using-eventmonitors)
-	+ [Logging](#logging)
-  * [Making Requests](#making-requests)
-	+ [`URLConvertible`](#urlconvertible)
-	+ [`URLRequestConvertible`](#urlrequestconvertible)
-	+ [Routing Requests](#routing-requests)
-  * [Response Handling](#response-handling)
-	+ [Handling Responses Without Serialization](#handling-responses-without-serialization)
-	+ [`ResponseSerializer`](#responseserializer)
-	  - [`DataResponseSerializer`](#dataresponseserializer)
-	  - [`StringResponseSerializer`](#stringresponseserializer)
-	  - [`JSONResponseSerializer`](#jsonresponseserializer)
-	  - [`DecodableResponseSerializer`](#decodableresponseserializer)
-	+ [Customizing Response Handlers](#customizing-response-handlers)
-	  - [Response Transforms](#response-transforms)
-	  - [Creating a Custom Response Serializer](#creating-a-custom-response-serializer)
-  * [Network Reachability](#network-reachability)
+* [`Session`](#session)
+  + [Creating Custom `Session` Instances](#creating-custom-session-instances)
+    - [Creating a `Session` With a `URLSessionConfiguration`](#creating-a-session-with-a-urlsessionconfiguration)
+  + [`SessionDelegate`](#sessiondelegate)
+  + [`startRequestsImmediately`](#startrequestsimmediately)
+  + [A `Session`’s `DispatchQueue`s](#a-sessions-dispatchqueues)
+  + [Adding a `RequestInterceptor`](#adding-a-requestinterceptor)
+  + [Adding a `ServerTrustManager`](#adding-a-servertrustmanager)
+  + [Adding a `RedirectHandler`](#adding-a-redirecthandler)
+  + [Adding a `CachedResponseHandler`](#adding-a-cachedresponsehandler)
+  + [Adding `EventMonitor`s](#adding-eventmonitors)
+  + [Creating Instances From `URLSession`s](#creating-instances-from-urlsessions)
+* [Requests](#requests)
+  + [The Request Pipeline](#the-request-pipeline)
+  + [`Request`](#request)
+    - [State](#state)
+    - [Progress](#progress)
+    - [Handling Redirects](#handling-redirects)
+    - [Customizing Caching](#customizing-caching)
+    - [Credentials](#credentials)
+    - [A `Request`’s `URLRequest`s](#a-requests-urlrequests)
+    - [`URLSessionTask`s](#urlsessiontasks)
+    - [Response](#response)
+    - [`URLSessionTaskMetrics`](#urlsessiontaskmetrics)
+  + [`DataRequest`](#datarequest)
+    - [Additional State](#additional-state)
+    - [Validation](#validation)
+  + [`DataStreamRequest`](#datastreamrequest)
+    - [Additional State](#additional-state)
+    - [Validation](#validation)
+  + [`UploadRequest`](#uploadrequest)
+    - [Additional State](#additional-state-1)
+  + [`DownloadRequest`](#downloadrequest)
+    - [Additional State](#additional-state-2)
+    - [Cancellation](#cancellation)
+    - [Validation](#validation-1)
+* [Adapting and Retrying Requests with `RequestInterceptor`](#adapting-and-retrying-requests-with-requestinterceptor)
+  + [`RequestAdapter`](#requestadapter)
+  + [`RequestRetrier`](#requestretrier)
+* [Security](#security)
+  + [Evaluating Server Trusts with `ServerTrustManager` and `ServerTrustEvaluating`](#evaluating-server-trusts-with-servertrustmanager-and-servertrustevaluating)
+    - [`ServerTrustEvaluting`](#servertrustevaluting)
+    - [`ServerTrustManager`](#servertrustmanager)
+* [Subclassing Server Trust Policy Manager](#subclassing-server-trust-policy-manager)
+  + [App Transport Security](#app-transport-security)
+    - [Using Self-Signed Certificates with Local Networking](#using-self-signed-certificates-with-local-networking)
+* [Customizing Caching and Redirect Handling](#customizing-caching-and-redirect-handling)
+  + [`CachedResponseHandler`](#cachedresponsehandler)
+  + [`RedirectHandler`](#redirecthandler)
+* [Using `EventMonitor`s](#using-eventmonitors)
+  + [Logging](#logging)
+* [Making Requests](#making-requests)
+  + [`URLConvertible`](#urlconvertible)
+  + [`URLRequestConvertible`](#urlrequestconvertible)
+  + [Routing Requests](#routing-requests)
+* [Response Handling](#response-handling)
+  + [Handling Responses Without Serialization](#handling-responses-without-serialization)
+  + [`ResponseSerializer`](#responseserializer)
+    - [`DataResponseSerializer`](#dataresponseserializer)
+    - [`StringResponseSerializer`](#stringresponseserializer)
+    - [`JSONResponseSerializer`](#jsonresponseserializer)
+    - [`DecodableResponseSerializer`](#decodableresponseserializer)
+  + [Customizing Response Handlers](#customizing-response-handlers)
+    - [Response Transforms](#response-transforms)
+    - [Creating a Custom Response Serializer](#creating-a-custom-response-serializer)
+* [Network Reachability](#network-reachability)
 
 # Advanced Usage
 
@@ -222,7 +224,7 @@ Each request performed by Alamofire is encapsulated by particular class, `DataRe
 Once a `Request` subclass has been created with it’s initial parameters or `URLRequestConvertible` value, it is passed through the series of steps making up Alamofire’s request pipeline. For a successful request, these include:
 
 1. Initial parameters, like HTTP method, headers, and parameters are encapsulated into an internal `URLRequestConvertible` value. If a `URLRequestConvertible` value is passed directly, that value is used unchanged.
-2. `asURLRequest()` is called on the the `URLRequestConvertible` value, creating the first `URLRequest` value. This value is passed to the `Request` and stored in `requests`.
+2. `asURLRequest()` is called on the the `URLRequestConvertible` value, creating the first `URLRequest` value. This value is passed to the `Request` and stored in `requests`. If the `URLRequestConvertible` value was created from the parameters passed to a `Session` method, any provided `RequestModifier` is called when the `URLRequest` is created.
 3. If there are any `Session` or `Request` `RequestAdapter`s or `RequestInterceptor`s, they’re called using the previously created `URLRequest`. The adapted `URLRequest` is then passed to the `Request` and stored in `request`s as well.
 4. `Session` calls the `Request` to create the `URLSessionTask` to perform the network request based on the `URLRequest`.
 5. Once the `URLSessionTask` is complete and `URLSessionTaskMetrics` have been gathered, the `Request` executes its `Validator`s. 
@@ -372,13 +374,13 @@ AF.request(...)
 `DataRequest`s have a few properties in addition to those provided by `Request`. These include `data`, which is the accumulated `Data` from the server response, and `convertible`, which is the `URLRequestConvertible` the `DataRequest` was created with, containing the original parameters creating the instance.
 
 #### Validation
-`DataRequest`s do not validate responses by default. Instead, a call to `validate()` must be added to the in order to verify various properties are valid. 
+`DataRequest`s do not validate responses by default. Instead, a call to `validate()` must be added to the request in order to verify various properties are valid. 
 
 ```swift
 public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> Result<Void, Error>
 ```
 
-By default, adding `validate()` ensures the response status code is within the `200..<300` range and that the response’s `Content-Type` matches the request `Accept` value. Validation can be further customized by passing a `Validation` closure:
+By default, adding `validate()` ensures the response status code is within the `200..<300` range and that the response’s `Content-Type` matches the request's `Accept` value. Validation can be further customized by passing a `Validation` closure:
 
 ```swift
 AF.request(...)
@@ -393,6 +395,22 @@ AF.request(...)
 #### Additional State
 `DataStreamRequest` contains no additional public state.
 
+#### Validation
+`DataStreamRequest`s do not validate responses by default. Instead, a call to `validate()` must be added to the request in order to verify various properties are valid. 
+
+```swift
+public typealias Validation = (_ request: URLRequest?, _ response: HTTPURLResponse) -> Result<Void, Error>
+```
+
+By default, adding `validate()` ensures the response status code is within the `200..<300` range and that the response’s `Content-Type` matches the request's `Accept` value. Validation can be further customized by passing a `Validation` closure:
+
+```swift
+AF.request(...)
+    .validate { request, response in
+        ...
+    }
+```
+
 ### `UploadRequest`
 `UploadRequest` is a subclass of `DataRequest` which encapsulates a `URLSessionUploadTask`, uploading a `Data` value, file on disk, or `InputStream` to a remote server. 
 
@@ -697,7 +715,7 @@ AF.request(urlComponents)
 Applications interacting with web applications in a significant manner are encouraged to have custom types conform to `URLConvertible` as a convenient way to map domain-specific models to server resources.
 
 ### `URLRequestConvertible`
-Types adopting the `URLRequestConvertible` protocol can be used to construct `URLRequest`s. `URLRequest` conforms to `URLRequestConvertible` by default, allowing it to be passed into `request`, `upload`, and `download` methods directly. Alamofire uses `URLRequestConvertible` as the foundation of all requests flowing through the request pipeline. Using `URLRequest`s directly the recommended way to customize `URLRequest` creation outside of the `ParamterEncoder`s that Alamofire provides.
+Types adopting the `URLRequestConvertible` protocol can be used to construct `URLRequest`s. `URLRequest` conforms to `URLRequestConvertible` by default, allowing it to be passed into `request`, `upload`, and `download` methods directly. Alamofire uses `URLRequestConvertible` as the foundation of all requests flowing through the request pipeline. Using `URLRequest`s directly the recommended way to customize `URLRequest` creation outside of the `ParameterEncoder`s that Alamofire provides.
 
 ```swift
 let url = URL(string: "https://httpbin.org/post")!

+ 73 - 54
Documentation/Usage.md

@@ -1,57 +1,56 @@
-- [Using Alamofire](#using-alamofire)
-  * [Introduction](#introduction)
-      - [Aside: The `AF` Namespace](#aside-the-af-namespace-and-reference)
-  * [Making Requests](#making-requests)
-    + [HTTP Methods](#http-methods)
-    + [Request Parameters and Parameter Encoders](#request-parameters-and-parameter-encoders)
-      - [`URLEncodedFormParameterEncoder`](#urlencodedformparameterencoder)
-        * [GET Request With URL-Encoded Parameters](#get-request-with-url-encoded-parameters)
-        * [POST Request With URL-Encoded Parameters](#post-request-with-url-encoded-parameters)
-        * [Configuring the Sorting of Encoded Parameters](#configuring-the-sorting-of-encoded-parameters)
-        * [Configuring the Encoding of `Array` Parameters](#configuring-the-encoding-of-array-parameters)
-        * [Configuring the Encoding of `Bool` Parameters](#configuring-the-encoding-of-bool-parameters)
-        * [Configuring the Encoding of `Data` Parameters](#configuring-the-encoding-of-data-parameters)
-        * [Configuring the Encoding of `Date` Parameters](#configuring-the-encoding-of-date-parameters)
-        * [Configuring the Encoding of Coding Keys](#configuring-the-encoding-of-coding-keys)
-        * [Configuring the Encoding of Spaces](#configuring-the-encoding-of-spaces)
-      - [`JSONParameterEncoder`](#jsonparameterencoder)
-        * [POST Request with JSON-Encoded Parameters](#post-request-with-json-encoded-parameters)
-        * [Configuring a Custom `JSONEncoder`](#configuring-a-custom-jsonencoder)
-        * [Manual Parameter Encoding of a `URLRequest`](#manual-parameter-encoding-of-a-urlrequest)
-    + [HTTP Headers](#http-headers)
-    + [Response Validation](#response-validation)
-      - [Automatic Validation](#automatic-validation)
-      - [Manual Validation](#manual-validation)
-    + [Response Handling](#response-handling)
-      - [Response Handler](#response-handler)
-      - [Response Data Handler](#response-data-handler)
-      - [Response String Handler](#response-string-handler)
-      - [Response JSON Handler](#response-json-handler)
-      - [Response `Decodable` Handler](#response-decodable-handler)
-      - [Chained Response Handlers](#chained-response-handlers)
-      - [Response Handler Queue](#response-handler-queue)
-    + [Response Caching](#response-caching)
-    + [Authentication](#authentication)
-      - [HTTP Basic Authentication](#http-basic-authentication)
-      - [Authentication with `URLCredential`](#authentication-with-urlcredential)
-      - [Manual Authentication](#manual-authentication)
-    + [Downloading Data to a File](#downloading-data-to-a-file)
-      - [Download File Destination](#download-file-destination)
-      - [Download Progress](#download-progress)
-      - [Canceling and Resuming a Download](#canceling-and-resuming-a-download)
-    + [Uploading Data to a Server](#uploading-data-to-a-server)
-      - [Uploading Data](#uploading-data)
-      - [Uploading a File](#uploading-a-file)
-      - [Uploading Multipart Form Data](#uploading-multipart-form-data)
-      - [Upload Progress](#upload-progress)
-    + [Streaming Data from a Server](#streaming-data-from-a-server)
-      - [Streaming `Data`](#streaming-data)
-      - [Streaming `String`s](#streaming-strings)
-      - [Streaming `Decodable` Values](#streaming-decodable-values)
-      - [Producing an `InputStream`](#producing-an-inputstream)
-    + [Statistical Metrics](#statistical-metrics)
-      - [`URLSessionTaskMetrics`](#urlsessiontaskmetrics)
-    + [cURL Command Output](#curl-command-output)
+* [Introduction](#introduction)
+    - [Aside: The `AF` Namespace](#aside-the-af-namespace-and-reference)
+* [Making Requests](#making-requests)
+  + [HTTP Methods](#http-methods)
+  + [Request Parameters and Parameter Encoders](#request-parameters-and-parameter-encoders)
+    - [`URLEncodedFormParameterEncoder`](#urlencodedformparameterencoder)
+      * [GET Request With URL-Encoded Parameters](#get-request-with-url-encoded-parameters)
+      * [POST Request With URL-Encoded Parameters](#post-request-with-url-encoded-parameters)
+      * [Configuring the Sorting of Encoded Parameters](#configuring-the-sorting-of-encoded-parameters)
+      * [Configuring the Encoding of `Array` Parameters](#configuring-the-encoding-of-array-parameters)
+      * [Configuring the Encoding of `Bool` Parameters](#configuring-the-encoding-of-bool-parameters)
+      * [Configuring the Encoding of `Data` Parameters](#configuring-the-encoding-of-data-parameters)
+      * [Configuring the Encoding of `Date` Parameters](#configuring-the-encoding-of-date-parameters)
+      * [Configuring the Encoding of Coding Keys](#configuring-the-encoding-of-coding-keys)
+      * [Configuring the Encoding of Spaces](#configuring-the-encoding-of-spaces)
+    - [`JSONParameterEncoder`](#jsonparameterencoder)
+      * [POST Request with JSON-Encoded Parameters](#post-request-with-json-encoded-parameters)
+      * [Configuring a Custom `JSONEncoder`](#configuring-a-custom-jsonencoder)
+      * [Manual Parameter Encoding of a `URLRequest`](#manual-parameter-encoding-of-a-urlrequest)
+  + [HTTP Headers](#http-headers)
+  + [Response Validation](#response-validation)
+    - [Automatic Validation](#automatic-validation)
+    - [Manual Validation](#manual-validation)
+  + [Response Handling](#response-handling)
+    - [Response Handler](#response-handler)
+    - [Response Data Handler](#response-data-handler)
+    - [Response String Handler](#response-string-handler)
+    - [Response JSON Handler](#response-json-handler)
+    - [Response `Decodable` Handler](#response-decodable-handler)
+    - [Chained Response Handlers](#chained-response-handlers)
+    - [Response Handler Queue](#response-handler-queue)
+  + [Response Caching](#response-caching)
+  + [Authentication](#authentication)
+    - [HTTP Basic Authentication](#http-basic-authentication)
+    - [Authentication with `URLCredential`](#authentication-with-urlcredential)
+    - [Manual Authentication](#manual-authentication)
+  + [Downloading Data to a File](#downloading-data-to-a-file)
+    - [Download File Destination](#download-file-destination)
+    - [Download Progress](#download-progress)
+    - [Canceling and Resuming a Download](#canceling-and-resuming-a-download)
+  + [Uploading Data to a Server](#uploading-data-to-a-server)
+    - [Uploading Data](#uploading-data)
+    - [Uploading a File](#uploading-a-file)
+    - [Uploading Multipart Form Data](#uploading-multipart-form-data)
+    - [Upload Progress](#upload-progress)
+  + [Streaming Data from a Server](#streaming-data-from-a-server)
+    - [Streaming `Data`](#streaming-data)
+    - [Streaming `String`s](#streaming-strings)
+    - [Streaming `Decodable` Values](#streaming-decodable-values)
+    - [Producing an `InputStream`](#producing-an-inputstream)
+  + [Statistical Metrics](#statistical-metrics)
+    - [`URLSessionTaskMetrics`](#urlsessiontaskmetrics)
+  + [cURL Command Output](#curl-command-output)
 
 # Using Alamofire
 
@@ -152,6 +151,26 @@ extension HTTPMethod {
 }
 ```
 
+### Setting Other `URLRequest` Properties
+
+Alamofire's request creation methods offer the most common parameters for customization but sometimes those just aren't enough. The `URLRequest`s created from the passed values can be modified by using a `RequestModifier` closure when creating requests. For example, to set the `URLRequest`'s `timeoutInterval` to 5 seconds, modify the request in the closure.
+
+```swift
+AF.request("https://httpbin.org/get", requestModifier: { $0.timeoutInterval = 5 }).response(...)
+```
+
+`RequestModifier`s also work with trailing closure syntax.
+
+```swift
+AF.request("https://httpbin.org/get") { urlRequest in
+    urlRequest.timeoutInterval = 5
+    urlRequest.allowsConstrainedNetworkAccess = false
+}
+.response(...)
+```
+
+`RequestModifier`s only apply to request created using methods taking a `URL` and other individual components, not to values created directly from `URLRequestConvertible` values, as those values should be able to set all parameters themselves. Additionally, adoption of `URLRequestConvertible` is recommended once *most* requests start needing to be modified during creation. You can read more in our [Advanced Usage documentation](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#making-requests).
+
 ### Request Parameters and Parameter Encoders
 
 Alamofire supports passing any `Encodable` type as the parameters of a request. These parameters are then passed through a type conforming to the `ParameterEncoder` protocol and added to the `URLRequest` which is then sent over the network. Alamofire includes two `ParameterEncoder` conforming types: `JSONParameterEncoder` and `URLEncodedFormParameterEncoder`. These types cover the most common encodings used by modern services (XML encoding is left as an exercise for the reader).

+ 142 - 74
Source/Session.swift

@@ -220,15 +220,21 @@ open class Session {
 
     // MARK: - DataRequest
 
+    /// Closure which provides a `URLRequest` for mutation.
+    public typealias RequestModifier = (inout URLRequest) throws -> Void
+
     struct RequestConvertible: URLRequestConvertible {
         let url: URLConvertible
         let method: HTTPMethod
         let parameters: Parameters?
         let encoding: ParameterEncoding
         let headers: HTTPHeaders?
+        let requestModifier: RequestModifier?
 
         func asURLRequest() throws -> URLRequest {
-            let request = try URLRequest(url: url, method: method, headers: headers)
+            var request = try URLRequest(url: url, method: method, headers: headers)
+            try requestModifier?(&request)
+
             return try encoding.encode(request, with: parameters)
         }
     }
@@ -236,13 +242,16 @@ open class Session {
     /// Creates a `DataRequest` from a `URLRequest` created using the passed components and a `RequestInterceptor`.
     ///
     /// - Parameters:
-    ///   - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`.
-    ///   - method:      `HTTPMethod` for the `URLRequest`. `.get` by default.
-    ///   - parameters:  `Parameters` (a.k.a. `[String: Any]`) value to be encoded into the `URLRequest`. `nil` by default.
-    ///   - encoding:    `ParameterEncoding` to be used to encode the `parameters` value into the `URLRequest`.
-    ///                  `URLEncoding.default` by default.
-    ///   - headers:     `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
-    ///   - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
+    ///   - convertible:     `URLConvertible` value to be used as the `URLRequest`'s `URL`.
+    ///   - method:          `HTTPMethod` for the `URLRequest`. `.get` by default.
+    ///   - parameters:      `Parameters` (a.k.a. `[String: Any]`) value to be encoded into the `URLRequest`. `nil` by
+    ///                      default.
+    ///   - encoding:        `ParameterEncoding` to be used to encode the `parameters` value into the `URLRequest`.
+    ///                      `URLEncoding.default` by default.
+    ///   - headers:         `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
+    ///   - interceptor:     `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
+    ///   - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided
+    ///                      parameters. `nil` by default.
     ///
     /// - Returns:       The created `DataRequest`.
     open func request(_ convertible: URLConvertible,
@@ -250,12 +259,14 @@ open class Session {
                       parameters: Parameters? = nil,
                       encoding: ParameterEncoding = URLEncoding.default,
                       headers: HTTPHeaders? = nil,
-                      interceptor: RequestInterceptor? = nil) -> DataRequest {
+                      interceptor: RequestInterceptor? = nil,
+                      requestModifier: RequestModifier? = nil) -> DataRequest {
         let convertible = RequestConvertible(url: convertible,
                                              method: method,
                                              parameters: parameters,
                                              encoding: encoding,
-                                             headers: headers)
+                                             headers: headers,
+                                             requestModifier: requestModifier)
 
         return request(convertible, interceptor: interceptor)
     }
@@ -266,9 +277,11 @@ open class Session {
         let parameters: Parameters?
         let encoder: ParameterEncoder
         let headers: HTTPHeaders?
+        let requestModifier: RequestModifier?
 
         func asURLRequest() throws -> URLRequest {
-            let request = try URLRequest(url: url, method: method, headers: headers)
+            var request = try URLRequest(url: url, method: method, headers: headers)
+            try requestModifier?(&request)
 
             return try parameters.map { try encoder.encode($0, into: request) } ?? request
         }
@@ -292,12 +305,14 @@ open class Session {
                                              parameters: Parameters? = nil,
                                              encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default,
                                              headers: HTTPHeaders? = nil,
-                                             interceptor: RequestInterceptor? = nil) -> DataRequest {
+                                             interceptor: RequestInterceptor? = nil,
+                                             requestModifier: RequestModifier? = nil) -> DataRequest {
         let convertible = RequestEncodableConvertible(url: convertible,
                                                       method: method,
                                                       parameters: parameters,
                                                       encoder: encoder,
-                                                      headers: headers)
+                                                      headers: headers,
+                                                      requestModifier: requestModifier)
 
         return request(convertible, interceptor: interceptor)
     }
@@ -338,6 +353,8 @@ open class Session {
     ///                                       is thrown while serializing stream `Data`. `false` by default.
     ///   - interceptor:                      `RequestInterceptor` value to be used by the returned `DataRequest`. `nil`
     ///                                       by default.
+    ///   - requestModifier:                  `RequestModifier` which will be applied to the `URLRequest` created from
+    ///                                       the provided parameters. `nil` by default.
     ///
     /// - Returns:       The created `DataStream` request.
     open func streamRequest<Parameters: Encodable>(_ convertible: URLConvertible,
@@ -346,12 +363,14 @@ open class Session {
                                                    encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default,
                                                    headers: HTTPHeaders? = nil,
                                                    automaticallyCancelOnStreamError: Bool = false,
-                                                   interceptor: RequestInterceptor? = nil) -> DataStreamRequest {
+                                                   interceptor: RequestInterceptor? = nil,
+                                                   requestModifier: RequestModifier? = nil) -> DataStreamRequest {
         let convertible = RequestEncodableConvertible(url: convertible,
                                                       method: method,
                                                       parameters: parameters,
                                                       encoder: encoder,
-                                                      headers: headers)
+                                                      headers: headers,
+                                                      requestModifier: requestModifier)
 
         return streamRequest(convertible,
                              automaticallyCancelOnStreamError: automaticallyCancelOnStreamError,
@@ -368,18 +387,22 @@ open class Session {
     ///                                       is thrown while serializing stream `Data`. `false` by default.
     ///   - interceptor:                      `RequestInterceptor` value to be used by the returned `DataRequest`. `nil`
     ///                                       by default.
+    ///   - requestModifier:                  `RequestModifier` which will be applied to the `URLRequest` created from
+    ///                                       the provided parameters. `nil` by default.
     ///
     /// - Returns:       The created `DataStream` request.
     open func streamRequest(_ convertible: URLConvertible,
                             method: HTTPMethod = .get,
                             headers: HTTPHeaders? = nil,
                             automaticallyCancelOnStreamError: Bool = false,
-                            interceptor: RequestInterceptor? = nil) -> DataStreamRequest {
+                            interceptor: RequestInterceptor? = nil,
+                            requestModifier: RequestModifier? = nil) -> DataStreamRequest {
         let convertible = RequestEncodableConvertible(url: convertible,
                                                       method: method,
                                                       parameters: Optional<Empty>.none,
                                                       encoder: URLEncodedFormParameterEncoder.default,
-                                                      headers: headers)
+                                                      headers: headers,
+                                                      requestModifier: requestModifier)
 
         return streamRequest(convertible,
                              automaticallyCancelOnStreamError: automaticallyCancelOnStreamError,
@@ -418,29 +441,34 @@ open class Session {
     /// `Destination`.
     ///
     /// - Parameters:
-    ///   - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`.
-    ///   - method:      `HTTPMethod` for the `URLRequest`. `.get` by default.
-    ///   - parameters:  `Parameters` (a.k.a. `[String: Any]`) value to be encoded into the `URLRequest`. `nil` by default.
-    ///   - encoding:    `ParameterEncoding` to be used to encode the `parameters` value into the `URLRequest`. Defaults
-    ///                  to `URLEncoding.default`.
-    ///   - headers:     `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
-    ///   - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
-    ///   - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file
-    ///                  should be moved. `nil` by default.
+    ///   - convertible:     `URLConvertible` value to be used as the `URLRequest`'s `URL`.
+    ///   - method:          `HTTPMethod` for the `URLRequest`. `.get` by default.
+    ///   - parameters:      `Parameters` (a.k.a. `[String: Any]`) value to be encoded into the `URLRequest`. `nil` by
+    ///                      default.
+    ///   - encoding:        `ParameterEncoding` to be used to encode the `parameters` value into the `URLRequest`.
+    ///                      Defaults to `URLEncoding.default`.
+    ///   - headers:         `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
+    ///   - interceptor:     `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
+    ///   - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided
+    ///                      parameters. `nil` by default.
+    ///   - destination:     `DownloadRequest.Destination` closure used to determine how and where the downloaded file
+    ///                      should be moved. `nil` by default.
     ///
-    /// - Returns:       The created `DownloadRequest`.
+    /// - Returns:           The created `DownloadRequest`.
     open func download(_ convertible: URLConvertible,
                        method: HTTPMethod = .get,
                        parameters: Parameters? = nil,
                        encoding: ParameterEncoding = URLEncoding.default,
                        headers: HTTPHeaders? = nil,
                        interceptor: RequestInterceptor? = nil,
+                       requestModifier: RequestModifier? = nil,
                        to destination: DownloadRequest.Destination? = nil) -> DownloadRequest {
         let convertible = RequestConvertible(url: convertible,
                                              method: method,
                                              parameters: parameters,
                                              encoding: encoding,
-                                             headers: headers)
+                                             headers: headers,
+                                             requestModifier: requestModifier)
 
         return download(convertible, interceptor: interceptor, to: destination)
     }
@@ -449,29 +477,33 @@ open class Session {
     /// a `RequestInterceptor`.
     ///
     /// - Parameters:
-    ///   - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`.
-    ///   - method:      `HTTPMethod` for the `URLRequest`. `.get` by default.
-    ///   - parameters:  Value conforming to `Encodable` to be encoded into the `URLRequest`. `nil` by default.
-    ///   - encoder:     `ParameterEncoder` to be used to encode the `parameters` value into the `URLRequest`. Defaults
-    ///                  to `URLEncodedFormParameterEncoder.default`.
-    ///   - headers:     `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
-    ///   - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
-    ///   - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file
-    ///                  should be moved. `nil` by default.
+    ///   - convertible:     `URLConvertible` value to be used as the `URLRequest`'s `URL`.
+    ///   - method:          `HTTPMethod` for the `URLRequest`. `.get` by default.
+    ///   - parameters:      Value conforming to `Encodable` to be encoded into the `URLRequest`. `nil` by default.
+    ///   - encoder:         `ParameterEncoder` to be used to encode the `parameters` value into the `URLRequest`.
+    ///                      Defaults to `URLEncodedFormParameterEncoder.default`.
+    ///   - headers:         `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
+    ///   - interceptor:     `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
+    ///   - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided
+    ///                      parameters. `nil` by default.
+    ///   - destination:     `DownloadRequest.Destination` closure used to determine how and where the downloaded file
+    ///                      should be moved. `nil` by default.
     ///
-    /// - Returns:       The created `DownloadRequest`.
+    /// - Returns:           The created `DownloadRequest`.
     open func download<Parameters: Encodable>(_ convertible: URLConvertible,
                                               method: HTTPMethod = .get,
                                               parameters: Parameters? = nil,
                                               encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default,
                                               headers: HTTPHeaders? = nil,
                                               interceptor: RequestInterceptor? = nil,
+                                              requestModifier: RequestModifier? = nil,
                                               to destination: DownloadRequest.Destination? = nil) -> DownloadRequest {
         let convertible = RequestEncodableConvertible(url: convertible,
                                                       method: method,
                                                       parameters: parameters,
                                                       encoder: encoder,
-                                                      headers: headers)
+                                                      headers: headers,
+                                                      requestModifier: requestModifier)
 
         return download(convertible, interceptor: interceptor, to: destination)
     }
@@ -541,9 +573,13 @@ open class Session {
         let url: URLConvertible
         let method: HTTPMethod
         let headers: HTTPHeaders?
+        let requestModifier: RequestModifier?
 
         func asURLRequest() throws -> URLRequest {
-            try URLRequest(url: url, method: method, headers: headers)
+            var request = try URLRequest(url: url, method: method, headers: headers)
+            try requestModifier?(&request)
+
+            return request
         }
     }
 
@@ -565,22 +601,28 @@ open class Session {
     /// Creates an `UploadRequest` for the given `Data`, `URLRequest` components, and `RequestInterceptor`.
     ///
     /// - Parameters:
-    ///   - data:        The `Data` to upload.
-    ///   - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`.
-    ///   - method:      `HTTPMethod` for the `URLRequest`. `.post` by default.
-    ///   - headers:     `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
-    ///   - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
-    ///   - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by
-    ///                  default.
+    ///   - data:            The `Data` to upload.
+    ///   - convertible:     `URLConvertible` value to be used as the `URLRequest`'s `URL`.
+    ///   - method:          `HTTPMethod` for the `URLRequest`. `.post` by default.
+    ///   - headers:         `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
+    ///   - interceptor:     `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
+    ///   - fileManager:     `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by
+    ///                      default.
+    ///   - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided
+    ///                      parameters. `nil` by default.
     ///
-    /// - Returns:       The created `UploadRequest`.
+    /// - Returns:           The created `UploadRequest`.
     open func upload(_ data: Data,
                      to convertible: URLConvertible,
                      method: HTTPMethod = .post,
                      headers: HTTPHeaders? = nil,
                      interceptor: RequestInterceptor? = nil,
-                     fileManager: FileManager = .default) -> UploadRequest {
-        let convertible = ParameterlessRequestConvertible(url: convertible, method: method, headers: headers)
+                     fileManager: FileManager = .default,
+                     requestModifier: RequestModifier? = nil) -> UploadRequest {
+        let convertible = ParameterlessRequestConvertible(url: convertible,
+                                                          method: method,
+                                                          headers: headers,
+                                                          requestModifier: requestModifier)
 
         return upload(data, with: convertible, interceptor: interceptor, fileManager: fileManager)
     }
@@ -608,22 +650,28 @@ open class Session {
     /// components and `RequestInterceptor`.
     ///
     /// - Parameters:
-    ///   - fileURL:     The `URL` of the file to upload.
-    ///   - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`.
-    ///   - method:      `HTTPMethod` for the `URLRequest`. `.post` by default.
-    ///   - headers:     `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
-    ///   - interceptor: `RequestInterceptor` value to be used by the returned `UploadRequest`. `nil` by default.
-    ///   - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by
-    ///                  default.
+    ///   - fileURL:         The `URL` of the file to upload.
+    ///   - convertible:     `URLConvertible` value to be used as the `URLRequest`'s `URL`.
+    ///   - method:          `HTTPMethod` for the `URLRequest`. `.post` by default.
+    ///   - headers:         `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
+    ///   - interceptor:     `RequestInterceptor` value to be used by the returned `UploadRequest`. `nil` by default.
+    ///   - fileManager:     `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by
+    ///                      default.
+    ///   - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided
+    ///                      parameters. `nil` by default.
     ///
-    /// - Returns:       The created `UploadRequest`.
+    /// - Returns:           The created `UploadRequest`.
     open func upload(_ fileURL: URL,
                      to convertible: URLConvertible,
                      method: HTTPMethod = .post,
                      headers: HTTPHeaders? = nil,
                      interceptor: RequestInterceptor? = nil,
-                     fileManager: FileManager = .default) -> UploadRequest {
-        let convertible = ParameterlessRequestConvertible(url: convertible, method: method, headers: headers)
+                     fileManager: FileManager = .default,
+                     requestModifier: RequestModifier? = nil) -> UploadRequest {
+        let convertible = ParameterlessRequestConvertible(url: convertible,
+                                                          method: method,
+                                                          headers: headers,
+                                                          requestModifier: requestModifier)
 
         return upload(fileURL, with: convertible, interceptor: interceptor, fileManager: fileManager)
     }
@@ -652,22 +700,28 @@ open class Session {
     /// `RequestInterceptor`.
     ///
     /// - Parameters:
-    ///   - stream:      The `InputStream` that provides the data to upload.
-    ///   - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`.
-    ///   - method:      `HTTPMethod` for the `URLRequest`. `.post` by default.
-    ///   - headers:     `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
-    ///   - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
-    ///   - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by
-    ///                  default.
+    ///   - stream:          The `InputStream` that provides the data to upload.
+    ///   - convertible:     `URLConvertible` value to be used as the `URLRequest`'s `URL`.
+    ///   - method:          `HTTPMethod` for the `URLRequest`. `.post` by default.
+    ///   - headers:         `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default.
+    ///   - interceptor:     `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
+    ///   - fileManager:     `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by
+    ///                      default.
+    ///   - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided
+    ///                      parameters. `nil` by default.
     ///
-    /// - Returns:       The created `UploadRequest`.
+    /// - Returns:           The created `UploadRequest`.
     open func upload(_ stream: InputStream,
                      to convertible: URLConvertible,
                      method: HTTPMethod = .post,
                      headers: HTTPHeaders? = nil,
                      interceptor: RequestInterceptor? = nil,
-                     fileManager: FileManager = .default) -> UploadRequest {
-        let convertible = ParameterlessRequestConvertible(url: convertible, method: method, headers: headers)
+                     fileManager: FileManager = .default,
+                     requestModifier: RequestModifier? = nil) -> UploadRequest {
+        let convertible = ParameterlessRequestConvertible(url: convertible,
+                                                          method: method,
+                                                          headers: headers,
+                                                          requestModifier: requestModifier)
 
         return upload(stream, with: convertible, interceptor: interceptor, fileManager: fileManager)
     }
@@ -719,6 +773,8 @@ open class Session {
     ///   - interceptor:             `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
     ///   - fileManager:             `FileManager` to be used if the form data exceeds the memory threshold and is
     ///                              written to disk before being uploaded. `.default` instance by default.
+    ///   - requestModifier:         `RequestModifier` which will be applied to the `URLRequest` created from the
+    ///                              provided parameters. `nil` by default.
     ///
     /// - Returns:                   The created `UploadRequest`.
     open func upload(multipartFormData: @escaping (MultipartFormData) -> Void,
@@ -727,8 +783,12 @@ open class Session {
                      method: HTTPMethod = .post,
                      headers: HTTPHeaders? = nil,
                      interceptor: RequestInterceptor? = nil,
-                     fileManager: FileManager = .default) -> UploadRequest {
-        let convertible = ParameterlessRequestConvertible(url: url, method: method, headers: headers)
+                     fileManager: FileManager = .default,
+                     requestModifier: RequestModifier? = nil) -> UploadRequest {
+        let convertible = ParameterlessRequestConvertible(url: url,
+                                                          method: method,
+                                                          headers: headers,
+                                                          requestModifier: requestModifier)
 
         let formData = MultipartFormData(fileManager: fileManager)
         multipartFormData(formData)
@@ -765,6 +825,8 @@ open class Session {
     ///   - interceptor:             `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
     ///   - fileManager:             `FileManager` to be used if the form data exceeds the memory threshold and is
     ///                              written to disk before being uploaded. `.default` instance by default.
+    ///   - requestModifier:         `RequestModifier` which will be applied to the `URLRequest` created from the
+    ///                              provided parameters. `nil` by default.
     ///
     /// - Returns:                   The created `UploadRequest`.
     open func upload(multipartFormData: @escaping (MultipartFormData) -> Void,
@@ -809,6 +871,8 @@ open class Session {
     ///   - interceptor:             `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default.
     ///   - fileManager:             `FileManager` to be used if the form data exceeds the memory threshold and is
     ///                              written to disk before being uploaded. `.default` instance by default.
+    ///   - requestModifier:         `RequestModifier` which will be applied to the `URLRequest` created from the
+    ///                              provided parameters. `nil` by default.
     ///
     /// - Returns:                   The created `UploadRequest`.
     open func upload(multipartFormData: MultipartFormData,
@@ -817,8 +881,12 @@ open class Session {
                      method: HTTPMethod = .post,
                      headers: HTTPHeaders? = nil,
                      interceptor: RequestInterceptor? = nil,
-                     fileManager: FileManager = .default) -> UploadRequest {
-        let convertible = ParameterlessRequestConvertible(url: url, method: method, headers: headers)
+                     fileManager: FileManager = .default,
+                     requestModifier: RequestModifier? = nil) -> UploadRequest {
+        let convertible = ParameterlessRequestConvertible(url: url,
+                                                          method: method,
+                                                          headers: headers,
+                                                          requestModifier: requestModifier)
 
         let multipartUpload = MultipartUpload(isInBackgroundSession: session.configuration.identifier != nil,
                                               encodingMemoryThreshold: encodingMemoryThreshold,

+ 201 - 0
Tests/RequestModifierTests.swift

@@ -0,0 +1,201 @@
+//
+//  RequestModifierTests.swift
+//
+//  Copyright (c) 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 Alamofire
+import XCTest
+
+final class RequestModifierTests: BaseTestCase {
+    // MARK: - DataRequest
+
+    func testThatDataRequestsCanHaveCustomTimeoutValueSet() {
+        // Given
+        let url = URL.makeHTTPBinURL(path: "delay/1")
+        let completed = expectation(description: "request completed")
+        let modified = expectation(description: "request should be modified")
+        var response: AFDataResponse<Data?>?
+
+        // When
+        AF.request(url) { $0.timeoutInterval = 0.01; modified.fulfill() }
+            .response { response = $0; completed.fulfill() }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual((response?.error?.underlyingError as? URLError)?.code, .timedOut)
+    }
+
+    func testThatDataRequestsCallRequestModifiersOnRetry() {
+        // Given
+        let url = URL.makeHTTPBinURL(path: "delay/1")
+        let inspector = InspectorInterceptor(RetryPolicy(retryLimit: 1, exponentialBackoffScale: 0))
+        let session = Session(interceptor: inspector)
+        let completed = expectation(description: "request completed")
+        let modified = expectation(description: "request should be modified twice")
+        modified.expectedFulfillmentCount = 2
+        var response: AFDataResponse<Data?>?
+
+        // When
+        session.request(url) { $0.timeoutInterval = 0.01; modified.fulfill() }
+            .response { response = $0; completed.fulfill() }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual((response?.error?.underlyingError as? URLError)?.code, .timedOut)
+        XCTAssertEqual(inspector.retryCalledCount, 2)
+    }
+
+    // MARK: - UploadRequest
+
+    func testThatUploadRequestsCanHaveCustomTimeoutValueSet() {
+        // Given
+        let url = URL.makeHTTPBinURL(path: "delay/1")
+        let data = Data("data".utf8)
+        let completed = expectation(description: "request completed")
+        let modified = expectation(description: "request should be modified")
+        var response: AFDataResponse<Data?>?
+
+        // When
+        AF.upload(data, to: url) { $0.timeoutInterval = 0.01; modified.fulfill() }
+            .response { response = $0; completed.fulfill() }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual((response?.error?.underlyingError as? URLError)?.code, .timedOut)
+    }
+
+    func testThatUploadRequestsCallRequestModifiersOnRetry() {
+        // Given
+        let url = URL.makeHTTPBinURL(path: "delay/1")
+        let data = Data("data".utf8)
+        let policy = RetryPolicy(retryLimit: 1, exponentialBackoffScale: 0, retryableHTTPMethods: [.post])
+        let inspector = InspectorInterceptor(policy)
+        let session = Session(interceptor: inspector)
+        let completed = expectation(description: "request completed")
+        let modified = expectation(description: "request should be modified twice")
+        modified.expectedFulfillmentCount = 2
+        var response: AFDataResponse<Data?>?
+
+        // When
+        session.upload(data, to: url) { $0.timeoutInterval = 0.01; modified.fulfill() }
+            .response { response = $0; completed.fulfill() }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual((response?.error?.underlyingError as? URLError)?.code, .timedOut)
+        XCTAssertEqual(inspector.retryCalledCount, 2)
+    }
+
+    // MARK: - DownloadRequest
+
+    func testThatDownloadRequestsCanHaveCustomTimeoutValueSet() {
+        // Given
+        let url = URL.makeHTTPBinURL(path: "delay/1")
+        let completed = expectation(description: "request completed")
+        let modified = expectation(description: "request should be modified")
+        var response: AFDownloadResponse<URL?>?
+
+        // When
+        AF.download(url, requestModifier: { $0.timeoutInterval = 0.01; modified.fulfill() })
+            .response { response = $0; completed.fulfill() }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual((response?.error?.underlyingError as? URLError)?.code, .timedOut)
+    }
+
+    func testThatDownloadRequestsCallRequestModifiersOnRetry() {
+        // Given
+        let url = URL.makeHTTPBinURL(path: "delay/1")
+        let inspector = InspectorInterceptor(RetryPolicy(retryLimit: 1, exponentialBackoffScale: 0))
+        let session = Session(interceptor: inspector)
+        let completed = expectation(description: "request completed")
+        let modified = expectation(description: "request should be modified twice")
+        modified.expectedFulfillmentCount = 2
+        var response: AFDownloadResponse<URL?>?
+
+        // When
+        session.download(url, requestModifier: { $0.timeoutInterval = 0.01; modified.fulfill() })
+            .response { response = $0; completed.fulfill() }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual((response?.error?.underlyingError as? URLError)?.code, .timedOut)
+        XCTAssertEqual(inspector.retryCalledCount, 2)
+    }
+
+    // MARK: - DataStreamRequest
+
+    func testThatDataStreamRequestsCanHaveCustomTimeoutValueSet() {
+        // Given
+        let url = URL.makeHTTPBinURL(path: "delay/1")
+        let completed = expectation(description: "request completed")
+        let modified = expectation(description: "request should be modified")
+        var response: DataStreamRequest.Completion?
+
+        // When
+        AF.streamRequest(url) { $0.timeoutInterval = 0.01; modified.fulfill() }
+            .responseStream { stream in
+                guard case let .complete(completion) = stream.event else { return }
+
+                response = completion
+                completed.fulfill()
+            }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual((response?.error?.underlyingError as? URLError)?.code, .timedOut)
+    }
+
+    func testThatDataStreamRequestsCallRequestModifiersOnRetry() {
+        // Given
+        let url = URL.makeHTTPBinURL(path: "delay/1")
+        let inspector = InspectorInterceptor(RetryPolicy(retryLimit: 1, exponentialBackoffScale: 0))
+        let session = Session(interceptor: inspector)
+        let completed = expectation(description: "request completed")
+        let modified = expectation(description: "request should be modified twice")
+        modified.expectedFulfillmentCount = 2
+        var response: DataStreamRequest.Completion?
+
+        // When
+        session.streamRequest(url) { $0.timeoutInterval = 0.01; modified.fulfill() }
+            .responseStream { stream in
+                guard case let .complete(completion) = stream.event else { return }
+
+                response = completion
+                completed.fulfill()
+            }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual((response?.error?.underlyingError as? URLError)?.code, .timedOut)
+        XCTAssertEqual(inspector.retryCalledCount, 2)
+    }
+}

+ 2 - 1
Tests/RetryPolicyTests.swift

@@ -43,7 +43,8 @@ class BaseRetryPolicyTestCase: BaseTestCase {
                                                      method: method,
                                                      parameters: nil,
                                                      encoding: URLEncoding.default,
-                                                     headers: nil)
+                                                     headers: nil,
+                                                     requestModifier: nil)
 
             urlRequest = try! request.asURLRequest()