How to get original requests in Alamofire 5? - swift

I made a wrapper for Alamofire which makes the data request first and then it prints the details of original URLRequest.
let dataRequest = session.request(url, method: .get, parameters: parameters)
let originalRequest = dataRequest.request
// Now print somehow the details of original request.
It worked fine on Alamofire 4.9, but it stopped in the newest 5.0 version. The problem is that dataRequest.request is nil. Why this behavior has changed? How can I access URLRequest underneath DataRequest?

URLRequests are now created asynchronously in Alamofire 5, so you're not going to be able to access the value immediately. Depending on what you're doing with the URLRequest there may be other solutions. For logging we recommend using the new EventMonitor protocol. You can read our documentation to see more, but adding a simple logger is straightforward:
final class Logger: EventMonitor {
let queue = DispatchQueue(label: ...)
// Event called when any type of Request is resumed.
func requestDidResume(_ request: Request) {
print("Resuming: \(request)")
}
// Event called whenever a DataRequest has parsed a response.
func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
debugPrint("Finished: \(response)")
}
}
let logger = Logger()
let session = Session(eventMonitors: [logger])

I had to obtain the URLRequest in a test case. Solved it by adding .responseData and using XCTestExpectation to wait for the async code to return:
func testThatHeadersContainContentEncoding() {
let exp = expectation(description: "\(#function)\(#line)")
let request = AF.request("http://test.org",
method: .post, parameters: ["test":"parameter"],
encoding: GZIPEncoding.default,
headers: ["Other":"Header"])
request.responseData { data in
let myHeader = request.request?.value(forHTTPHeaderField: additionalHeader.dictionary.keys.first!)
// .. do my tests
exp.fulfill()
}
waitForExpectations(timeout: 10, handler: nil)
}

Related

Removing Swift RxAlamofire dependency

I'm trying to remove my dependency on RxAlamofire.
I currently have this function:
func requestData(_ urlRequest: URLRequestConvertible) -> Observable<(HTTPURLResponse, Data)> {
RxAlamofire.request(urlRequest).responseData()
}
How can I refactor this and use Alamofire directly to build and return an RxSwift Observable?
I suggest you look at the way the library wraps URLRequest to get an idea on how to do it...
Below is an abbreviated example from the library. In essence, you need to use Observable.create, make the network call passing in a closure that knows how to use the observer that create gives you.
Make sure you send a completed when done and make sure the disposable knows how to cancel the request.
Your Base will be something in Alamofire (I don't use Alamofire so I'm not sure what that might be.)
extension Reactive where Base: URLSession {
/**
Observable sequence of responses for URL request.
Performing of request starts after observer is subscribed and not after invoking this method.
**URL requests will be performed per subscribed observer.**
Any error during fetching of the response will cause observed sequence to terminate with error.
- parameter request: URL request.
- returns: Observable sequence of URL responses.
*/
public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable.create { observer in
let task = self.base.dataTask(with: request) { data, response, error in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
observer.on(.next((httpResponse, data)))
observer.on(.completed)
}
task.resume()
return Disposables.create(with: task.cancel)
}
}
}

How to handle waiting for a nested URLSession to complete

I have a function that provides a layer over URLSession. When this function is called I would like to check if the current access token as expired, if it has, I would to pause the current call, make a call to request a new token, replace the existing entry in the Keychain, then continue with the call.
func profile(with endpoint: ProfilesEndpoint, method: HTTPMethod, body: String?, headers: [String: String]?, useAuthToken: Bool = true, completion: #escaping (Either<ProfileResponse>) -> Void) {
var request = endpoint.request
request.httpMethod = method.rawValue
if let body = body {
request.httpBody = body.data(using: .utf8)
}
if useAuthToken {
if !AuthService.shared.isTokenValid {
let group = DispatchGroup()
group.enter()
OAuthService.shared.requestRefreshToken()
group.leave()
}
let (header, token) = AuthService.shared.createAuthHeaderForNetworkRequest()
request.addValue(token, forHTTPHeaderField: header)
}
if let headers = headers {
for (key, value) in headers {
request.addValue(value, forHTTPHeaderField: key)
}
}
execute(with: request, completion: completion)
}
A mechanism existing for handling the Keychain so please assume this is in place.
The function to request a new token looks like
func requestRefreshToken() -> Void {
if let refreshToken = KeychainWrapper.standard.string(forKey: "RefreshToken") {
var postBody = "grant_type=\(refreshTokenGrantType)&"
postBody += "client_id=\(clientId)&"
postBody += "refresh_token=\(refreshToken)&"
let additionalHeaders = [
"Content-Type": "application/x-www-form-urlencoded;"
]
APIClient.shared.identity(with: .token, method: .post, body: postBody, headers: additionalHeaders, useAuthToken: false) { either in
switch either {
case .success(let results):
guard let accessToken = results.accessToken, let refreshToken = results.refreshToken else { return }
AuthService.shared.addTokensToKeyChain(tokens: ["AccessToken": accessToken, "RefreshToken": refreshToken])
case .error(let error):
print("Error:", error)
}
}
}
}
I was expecting the executing to pause here
group.enter()
OAuthService.shared.requestRefreshToken()
group.leave()
However it does not.
How I can await this call to complete before completing the rest of the function?
Add to your requestRefreshToken method completion handler which will get executed when your request for token is completed
func requestRefreshToken(_ completion: #escaping () -> Void) {
if let refreshToken = KeychainWrapper.standard.string(forKey: "RefreshToken") {
var postBody = "grant_type=\(refreshTokenGrantType)&"
postBody += "client_id=\(clientId)&"
postBody += "refresh_token=\(refreshToken)&"
let additionalHeaders = [
"Content-Type": "application/x-www-form-urlencoded;"
]
APIClient.shared.identity(with: .token, method: .post, body: postBody, headers: additionalHeaders, useAuthToken: false) { either in
switch either {
case .success(let results):
guard let accessToken = results.accessToken, let refreshToken = results.refreshToken else {
completion()
return
}
AuthService.shared.addTokensToKeyChain(tokens: ["AccessToken": accessToken, "RefreshToken": refreshToken])
case .error(let error):
print("Error:", error)
}
completion()
}
}
}
then leave dispatchGroup in closure and also add group.wait() (after calling request method) for pausing current thread until group's task has completed
group.enter()
OAuthService.shared.requestRefreshToken {
group.leave()
}
group.wait()
Note: you can add boolean parameter to completion to check if request for token was successful or not
Why not do this underneath instead of on top? This seems like an ideal use for NSURLProtocol. Basically:
The URL protocol snags creation of the request in its init method and saves off all the provided parameters.
The URL protocol allocates a private NSURLSession instance and stores it in a property. That session should not be configured to use the protocol, or else you'll get an infinite loop.
When the protocol gets a startLoading() call, it checks the validity of the token, then:
Fires off a request for a new token if needed, in that private session.
Upon response, or if the token is still valid, fires off the real request — again, in that private session.
With that approach, the entire authentication process becomes basically transparent to the app except for the need to add the protocol into protocolClasses on the session configuration when creating a new session.
(There are a number of websites, including developer.apple.com, that provide examples of custom NSURLProtocol subclasses; if you decide to go with this approach, you should probably use one of those sample code projects as a starting point.)
Alternatively, if you want to stick with the layer-on-top approach, you need to stop thinking about "stopping" the method execution and start thinking about it as "doing the last part of the method later". It's all about asynchronous thinking.
Basically:
Factor out the last part of the method (the code that performs the actual request) into a new method (or a block).
If the token is valid, call that method immediately.
If the token is invalid, asynchronously fetch the new token.
In the token fetch call's completion callback, call the method to perform the actual request.

Alamofire 4 sends method as POST but server reads it GET

I have a very weird problem here, using Alamofire 4 Swift 4 Xcode 9.1
let manager = Alamofire.SessionManager.default
manager.session.configuration.timeoutIntervalForRequest = 30
manager.session.configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
let request = manager.request(
url,
method: HTTPMethod.post,
parameters: [:])
But server replies with method not allowed because it reads it as GET, however, if I change HTTPMethod.put or .delete or any other method, the server reads it correctly, the problem is with post specifically!
By debugging Alamofire's class 'SessionManager', specially the following method:
open func request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)
-> DataRequest
The method here is correct, POST, so it's just fine before it goes out the application, what's wrong?
It was adding '/' at the end of the URL that was causing this.

Cannot do Alamofire request in Swift 4

I get this error and I have no idea why I get it. It is not shown in the picture but I do import UIKit and Alamofire. It would be great if you could let me know what the problem is.
I think the main problem is in the Alamofire.request(.GET, currentWeatherURL) part.
func downloadWeatherDetails(completed: DownloadComplete) {
//Alamofire Download
let currentWeatherURL = URL(string: CURRENT_WEATHER_URL)!
Alamofire.request(.GET, currentWeatherURL).responseJSON { response in
let result = response.result
print(result)
}
}
There are several issues with your code:
The order of your parameters is wrong, the request URL should prepend the method type, which should have the name method.
Method types are now lowercased in Alamofire.
You should not force unwrap the URL initializer - moreover, Alamofire does this for you automatically, just pass the string as a parameter.
Here is your code, fixed:
func downloadWeatherDetails(completed: DownloadComplete) {
Alamofire.request(CURRENT_WEATHER_URL, method: .get).responseJSON { response in
let result = response.result
print(result)
}
}

HTTP Request error using Alamofire

I am trying to access my MAMP database webservice using Alamofire. Following is my code:
Following is my router to construct my URL:
enum Router: URLRequestConvertible {
static let baseURLString = "http://pushchat.local:44447/"
case PostJoinRequest(String,String,String,String,String)
var URLRequest: NSURLRequest {
let (path: String, parameters: [String: AnyObject]) = {
switch self {
case .PostJoinRequest (let addPath, let userID, let token, let nickName, let secretCode):
let params = ["cmd": "join", "user_id": "\(userID)", "token": "\(token)", "name": "\(nickName)", "code": "\(secretCode)"]
return (addPath, params)
}
}()
let URL = NSURL(string: Router.baseURLString)
let URLRequest = NSURLRequest(URL: URL!.URLByAppendingPathComponent(path))
let encoding = Alamofire.ParameterEncoding.URL
return encoding.encode(URLRequest, parameters: parameters).0
}
}
Following is my viewdidload code:
Alamofire.request(.POST,Router.PostJoinRequest("api.php","12345678901234","12345678901234","ABCDEF","TopSecret")).responseJSON()
{(request, response, JSON, error) in
println(JSON)
}
Following is the compile error:
Cannot invoke 'responseJSON' with an argument list of type '((,,,)->_)'
Following is the declaration from Almofire for your reference.
:param: method The HTTP method.
:param: URLString The URL string.
:param: parameters The parameters. `nil` by default.
:param: encoding The parameter encoding. `.URL` by default.
:returns: The created request.
*/
// public func request(method: Method, _ URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL) -> Request {
// return request(encoding.encode(URLRequest(method, URLString), parameters: parameters).0)
// }
Please let me know why am I facing this issue while chaining and what is it that I am not doing right?
Thanks for your help.
Dev
The compiler error message is really misleading – there is no problem with responseJSON but with request method itself.
In fact compiler does not like your second parameter. You are passing URLRequestConvertible but URLStringConvertible is expected (see the signature you posted).
Maybe you wanted to call another version of request method:
//:param: URLRequest The URL request
//:returns: The created request.
public func request(URLRequest: URLRequestConvertible) -> Request
In that case you have to adjust your Router class and set HTTP method into NSURLRequest created inside. For example:
let URLRequest = NSMutableURLRequest(URL: URL!.URLByAppendingPathComponent(path))
URLRequest.HTTPMethod = "POST"
Note you will also probably need to use another parameter/data encoding.