{"action":"closed","number":1450,"pull_request":{"url":"https://api.github.com/repos/Alamofire/Alamofire/pulls/1450","id":83913414,"html_url":"https://github.com/Alamofire/Alamofire/pull/1450","diff_url":"https://github.com/Alamofire/Alamofire/pull/1450.diff","patch_url":"https://github.com/Alamofire/Alamofire/pull/1450.patch","issue_url":"https://api.github.com/repos/Alamofire/Alamofire/issues/1450","number":1450,"state":"closed","locked":false,"title":"Feature - Request Adapter and Retrier System","user":{"login":"cnoon","id":169110,"avatar_url":"https://avatars.githubusercontent.com/u/169110?v=3","gravatar_id":"","url":"https://api.github.com/users/cnoon","html_url":"https://github.com/cnoon","followers_url":"https://api.github.com/users/cnoon/followers","following_url":"https://api.github.com/users/cnoon/following{/other_user}","gists_url":"https://api.github.com/users/cnoon/gists{/gist_id}","starred_url":"https://api.github.com/users/cnoon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cnoon/subscriptions","organizations_url":"https://api.github.com/users/cnoon/orgs","repos_url":"https://api.github.com/users/cnoon/repos","events_url":"https://api.github.com/users/cnoon/events{/privacy}","received_events_url":"https://api.github.com/users/cnoon/received_events","type":"User","site_admin":false},"body":"This PR adds a couple powerful protocols, `RequestAdapter` and `RequestRetrier`, that open up all sorts of possibilities including:\r\n\r\n* Easing `Request` modifications for `Authorization` headers\r\n* Retrying requests that encountered an error with a custom exponential backoff retry policy\r\n* Completely thread-safe refresh systems for web services behind OAuth2 authentication\r\n\r\nI'm sure there are all sorts of other cool things you could do with this, but these are the main things I had in mind when putting this together.\r\n\r\n## RequestAdapter\r\n\r\nThe `RequestAdapter` protocol allows each `Request` made on a `SessionManager` to be inspected and adapted before being created. One very specific way to use an adapter is to append an `Authorization` header to requests behind a certain type of authentication.\r\n\r\n```swift\r\nclass AccessTokenAdapter: RequestAdapter {\r\n\tprivate let accessToken: String\r\n\t\r\n\tinit(accessToken: String) {\r\n\t\tself.accessToken = accessToken\r\n\t}\r\n\r\n\tfunc adapt(_ urlRequest: URLRequest) -> URLRequest {\r\n\t    var urlRequest = urlRequest\r\n\r\n\t    if urlRequest.urlString.hasPrefix(\"https://httpbin.org\") {\r\n\t\t    urlRequest.setValue(\"Bearer \" + accessToken, forHTTPHeaderField: \"Authorization\")\r\n\t    }\r\n\r\n\t    return urlRequest\r\n\t}\r\n}\r\n\r\nlet sessionManager = SessionManager()\r\nsessionManager.adapter = AccessTokenAdapter(accessToken: \"1234\")\r\n\r\nsessionManager.request(\"https://httpbin.org/get\", withMethod: .get)\r\n```\r\n\r\n## RequestRetrier\r\n\r\nThe `RequestRetrier` protocol allows a `Request` that encountered an `Error` while being executed to be retried. When using both the `RequestAdapter` and `RequestRetrier` protocols together, you can create credential refresh systems for OAuth1, OAuth2, Basic Auth and even exponential backoff retry policies. The possibilities are endless. Here's a short example of how you could implement a refresh flow for OAuth2 access tokens.\r\n\r\n```swift\r\nclass OAuth2Handler: RequestAdapter, RequestRetrier {\r\n    private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void\r\n\r\n    private let sessionManager: SessionManager = {\r\n        let configuration = URLSessionConfiguration.default\r\n        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders\r\n\r\n        return SessionManager(configuration: configuration)\r\n    }()\r\n\r\n    private let lock = NSLock()\r\n\r\n    private var clientID: String\r\n    private var baseURLString: String\r\n    private var accessToken: String\r\n    private var refreshToken: String\r\n\r\n    private var isRefreshing = false\r\n    private var requestsToRetry: [RequestRetryCompletion] = []\r\n\r\n    // MARK: - Initialization\r\n\r\n    public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {\r\n        self.clientID = clientID\r\n        self.baseURLString = baseURLString\r\n        self.accessToken = accessToken\r\n        self.refreshToken = refreshToken\r\n    }\r\n\r\n    // MARK: - RequestAdapter\r\n\r\n    public func adapt(_ urlRequest: URLRequest) -> URLRequest {\r\n        if urlRequest.urlString.hasPrefix(baseURLString) {\r\n            var mutableURLRequest = urlRequest\r\n            mutableURLRequest.setValue(\"Bearer \" + accessToken, forHTTPHeaderField: \"Authorization\")\r\n            return mutableURLRequest\r\n        }\r\n\r\n        return urlRequest\r\n    }\r\n\r\n    // MARK: - RequestRetrier\r\n\r\n    public func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: RequestRetryCompletion) {\r\n        lock.lock() ; defer { lock.unlock() }\r\n\r\n        if let response = request.task.response as? HTTPURLResponse, response.statusCode == 401 {\r\n            requestsToRetry.append(completion)\r\n\r\n            if !isRefreshing {\r\n                refreshTokens { [weak self] succeeded, accessToken, refreshToken in\r\n                    guard let strongSelf = self else { return }\r\n\r\n                    strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }\r\n\r\n                    if let accessToken = accessToken, let refreshToken = refreshToken {\r\n                        strongSelf.accessToken = accessToken\r\n                        strongSelf.refreshToken = refreshToken\r\n                    }\r\n\r\n                    strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }\r\n                    strongSelf.requestsToRetry.removeAll()\r\n                }\r\n            }\r\n        } else {\r\n            completion(false, 0.0)\r\n        }\r\n    }\r\n\r\n    // MARK: - Private - Refresh Tokens\r\n\r\n    private func refreshTokens(completion: RefreshCompletion) {\r\n        guard !isRefreshing else { return }\r\n\r\n        isRefreshing = true\r\n\r\n        let urlString = \"\\(baseURLString)/oauth2/token\"\r\n\r\n        let parameters: [String: Any] = [\r\n            \"access_token\": accessToken,\r\n            \"refresh_token\": refreshToken,\r\n            \"client_id\": clientID,\r\n            \"grant_type\": \"refresh_token\"\r\n        ]\r\n\r\n        sessionManager.request(urlString, withMethod: .post, parameters: parameters, encoding: .json).responseJSON { [weak self] response in\r\n            guard let strongSelf = self else { return }\r\n\r\n            if let json = response.result.value as? [String: String] {\r\n                completion(true, json[\"access_token\"], json[\"refresh_token\"])\r\n            } else {\r\n                completion(false, nil, nil)\r\n            }\r\n\r\n            strongSelf.isRefreshing = false\r\n        }\r\n    }\r\n}\r\n\r\nlet baseURLString = \"https://some.domain-behind-oauth2.com\"\r\n\r\nlet oauthHandler = OAuth2Handler(\r\n    clientID: \"12345678\",\r\n    baseURLString: baseURLString,\r\n    accessToken: \"abcd1234\",\r\n    refreshToken: \"ef56789a\"\r\n)\r\n\r\nlet sessionManager = SessionManager()\r\nsessionManager.adapter = oauthHandler\r\nsessionManager.retrier = oauthHandler\r\n\r\nlet urlString = \"\\(baseURLString)/some/endpoint\"\r\n\r\nmanager.request(urlString, withMethod: .get).validate().responseJSON { response in\r\n    debugPrint(response)\r\n}\r\n```\r\n\r\n---\r\n\r\n## Tests\r\n\r\nI've added tests around the `RequestAdapter` to make sure all the `SessionManager` APIs actually  call the `adapter` if set. I also added tests verifying the `retrier` is called when errors occur and that the `adapter` will be called again when the request is retried.\r\n\r\n## Internal Modifications\r\n\r\nThere are several internal changes that are worth calling out to make this work.\r\n\r\n### Validations\r\n\r\nThe first is that `Validation` is no longer run on the delegate's `OperationQueue`. In order to determine whether a `Request` encountered an error, we need to make sure we run all the validations first, otherwise the error won't be set and the `retrier` won't be called. This was a trivial change to implement, but is an important callout.\r\n\r\n### TaskConvertible\r\n\r\nThe `TaskConvertible` enum nested in the `Request` class allows us to store the un-adapted version of the `Request` before it is adapted and turned into a `URLSessionTask`. The `retry` method on the `SessionManager` uses the new `originalTask` property to extract the original urlRequest, adapt it if necessary, then create the new task and apply it to the `Request`. By setting a new task on the `TaskDelegate`, it automatically resets all the data tracked in the `TaskDelegate` as though it is a brand new task.\r\n\r\n### SessionDelegate\r\n\r\nThe `SessionManager` must be passed down to the `SessionDelegate` as a `weak` property in order to allow the `SessionManager` to retry the `Request` from the `SessionDelegate`. I originally had the retry logic in the `Request` class, but it became problematic when trying to figure out how to lock down the task creation on the `SessionManager` queue. While I don't love having to store the parent reference in the child, it's not so bad since it's not exposed publicly and it doesn't result in a retain cycle since it's a `weak` reference. \r\n\r\n> I'm very open to suggestions here on a better approach, but I think this is about the cleanest way to do it.\r\n\r\n## README Updates\r\n\r\nI've added a `Adapting and Retrying Requests` section to the README to walk users through some of the ways you could use these protocols. I also created a fairly detailed `OAuth2` example to demonstrate how to start to build a thread-safe refresh system that could be shared across multiple session managers.\r\n\r\n---\r\n\r\n## Summary\r\n\r\nOverall I think these are two very powerful protocols that will open up all sorts of possibilities for the Alamofire community. I'd love to see custom `RequestRetrier` implementations for linear and exponential back-off policies for things like background sync operations. Can't wait to see what everyone comes up with!","created_at":"2016-09-04T04:44:04Z","updated_at":"2016-09-06T03:59:58Z","closed_at":"2016-09-06T03:59:57Z","merged_at":"2016-09-06T03:59:57Z","merge_commit_sha":"70067ba43ead7b2c109a894e9e288c44cdf33dd9","assignee":{"login":"jshier","id":51020,"avatar_url":"https://avatars.githubusercontent.com/u/51020?v=3","gravatar_id":"","url":"https://api.github.com/users/jshier","html_url":"https://github.com/jshier","followers_url":"https://api.github.com/users/jshier/followers","following_url":"https://api.github.com/users/jshier/following{/other_user}","gists_url":"https://api.github.com/users/jshier/gists{/gist_id}","starred_url":"https://api.github.com/users/jshier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jshier/subscriptions","organizations_url":"https://api.github.com/users/jshier/orgs","repos_url":"https://api.github.com/users/jshier/repos","events_url":"https://api.github.com/users/jshier/events{/privacy}","received_events_url":"https://api.github.com/users/jshier/received_events","type":"User","site_admin":false},"assignees":[{"login":"jshier","id":51020,"avatar_url":"https://avatars.githubusercontent.com/u/51020?v=3","gravatar_id":"","url":"https://api.github.com/users/jshier","html_url":"https://github.com/jshier","followers_url":"https://api.github.com/users/jshier/followers","following_url":"https://api.github.com/users/jshier/following{/other_user}","gists_url":"https://api.github.com/users/jshier/gists{/gist_id}","starred_url":"https://api.github.com/users/jshier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jshier/subscriptions","organizations_url":"https://api.github.com/users/jshier/orgs","repos_url":"https://api.github.com/users/jshier/repos","events_url":"https://api.github.com/users/jshier/events{/privacy}","received_events_url":"https://api.github.com/users/jshier/received_events","type":"User","site_admin":false},{"login":"kcharwood","id":198768,"avatar_url":"https://avatars.githubusercontent.com/u/198768?v=3","gravatar_id":"","url":"https://api.github.com/users/kcharwood","html_url":"https://github.com/kcharwood","followers_url":"https://api.github.com/users/kcharwood/followers","following_url":"https://api.github.com/users/kcharwood/following{/other_user}","gists_url":"https://api.github.com/users/kcharwood/gists{/gist_id}","starred_url":"https://api.github.com/users/kcharwood/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kcharwood/subscriptions","organizations_url":"https://api.github.com/users/kcharwood/orgs","repos_url":"https://api.github.com/users/kcharwood/repos","events_url":"https://api.github.com/users/kcharwood/events{/privacy}","received_events_url":"https://api.github.com/users/kcharwood/received_events","type":"User","site_admin":false}],"milestone":{"url":"https://api.github.com/repos/Alamofire/Alamofire/milestones/31","html_url":"https://github.com/Alamofire/Alamofire/milestones/4.0.0","labels_url":"https://api.github.com/repos/Alamofire/Alamofire/milestones/31/labels","id":1968608,"number":31,"title":"4.0.0","description":"","creator":{"login":"cnoon","id":169110,"avatar_url":"https://avatars.githubusercontent.com/u/169110?v=3","gravatar_id":"","url":"https://api.github.com/users/cnoon","html_url":"https://github.com/cnoon","followers_url":"https://api.github.com/users/cnoon/followers","following_url":"https://api.github.com/users/cnoon/following{/other_user}","gists_url":"https://api.github.com/users/cnoon/gists{/gist_id}","starred_url":"https://api.github.com/users/cnoon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cnoon/subscriptions","organizations_url":"https://api.github.com/users/cnoon/orgs","repos_url":"https://api.github.com/users/cnoon/repos","events_url":"https://api.github.com/users/cnoon/events{/privacy}","received_events_url":"https://api.github.com/users/cnoon/received_events","type":"User","site_admin":false},"open_issues":4,"closed_issues":1,"state":"open","created_at":"2016-08-29T02:15:08Z","updated_at":"2016-09-06T03:59:58Z","due_on":"2016-09-11T00:00:00Z","closed_at":null},"commits_url":"https://api.github.com/repos/Alamofire/Alamofire/pulls/1450/commits","review_comments_url":"https://api.github.com/repos/Alamofire/Alamofire/pulls/1450/comments","review_comment_url":"https://api.github.com/repos/Alamofire/Alamofire/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Alamofire/Alamofire/issues/1450/comments","statuses_url":"https://api.github.com/repos/Alamofire/Alamofire/statuses/2f005e490ba81bc73cf60cb83497a31b2a51137d","head":{"label":"Alamofire:feature/adapter-and-retrier-system","ref":"feature/adapter-and-retrier-system","sha":"2f005e490ba81bc73cf60cb83497a31b2a51137d","user":{"login":"Alamofire","id":7774181,"avatar_url":"https://avatars.githubusercontent.com/u/7774181?v=3","gravatar_id":"","url":"https://api.github.com/users/Alamofire","html_url":"https://github.com/Alamofire","followers_url":"https://api.github.com/users/Alamofire/followers","following_url":"https://api.github.com/users/Alamofire/following{/other_user}","gists_url":"https://api.github.com/users/Alamofire/gists{/gist_id}","starred_url":"https://api.github.com/users/Alamofire/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Alamofire/subscriptions","organizations_url":"https://api.github.com/users/Alamofire/orgs","repos_url":"https://api.github.com/users/Alamofire/repos","events_url":"https://api.github.com/users/Alamofire/events{/privacy}","received_events_url":"https://api.github.com/users/Alamofire/received_events","type":"Organization","site_admin":false},"repo":{"id":22458259,"name":"Alamofire","full_name":"Alamofire/Alamofire","owner":{"login":"Alamofire","id":7774181,"avatar_url":"https://avatars.githubusercontent.com/u/7774181?v=3","gravatar_id":"","url":"https://api.github.com/users/Alamofire","html_url":"https://github.com/Alamofire","followers_url":"https://api.github.com/users/Alamofire/followers","following_url":"https://api.github.com/users/Alamofire/following{/other_user}","gists_url":"https://api.github.com/users/Alamofire/gists{/gist_id}","starred_url":"https://api.github.com/users/Alamofire/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Alamofire/subscriptions","organizations_url":"https://api.github.com/users/Alamofire/orgs","repos_url":"https://api.github.com/users/Alamofire/repos","events_url":"https://api.github.com/users/Alamofire/events{/privacy}","received_events_url":"https://api.github.com/users/Alamofire/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Alamofire/Alamofire","description":"Elegant HTTP Networking in Swift","fork":false,"url":"https://api.github.com/repos/Alamofire/Alamofire","forks_url":"https://api.github.com/repos/Alamofire/Alamofire/forks","keys_url":"https://api.github.com/repos/Alamofire/Alamofire/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Alamofire/Alamofire/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Alamofire/Alamofire/teams","hooks_url":"https://api.github.com/repos/Alamofire/Alamofire/hooks","issue_events_url":"https://api.github.com/repos/Alamofire/Alamofire/issues/events{/number}","events_url":"https://api.github.com/repos/Alamofire/Alamofire/events","assignees_url":"https://api.github.com/repos/Alamofire/Alamofire/assignees{/user}","branches_url":"https://api.github.com/repos/Alamofire/Alamofire/branches{/branch}","tags_url":"https://api.github.com/repos/Alamofire/Alamofire/tags","blobs_url":"https://api.github.com/repos/Alamofire/Alamofire/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Alamofire/Alamofire/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Alamofire/Alamofire/git/refs{/sha}","trees_url":"https://api.github.com/repos/Alamofire/Alamofire/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Alamofire/Alamofire/statuses/{sha}","languages_url":"https://api.github.com/repos/Alamofire/Alamofire/languages","stargazers_url":"https://api.github.com/repos/Alamofire/Alamofire/stargazers","contributors_url":"https://api.github.com/repos/Alamofire/Alamofire/contributors","subscribers_url":"https://api.github.com/repos/Alamofire/Alamofire/subscribers","subscription_url":"https://api.github.com/repos/Alamofire/Alamofire/subscription","commits_url":"https://api.github.com/repos/Alamofire/Alamofire/commits{/sha}","git_commits_url":"https://api.github.com/repos/Alamofire/Alamofire/git/commits{/sha}","comments_url":"https://api.github.com/repos/Alamofire/Alamofire/comments{/number}","issue_comment_url":"https://api.github.com/repos/Alamofire/Alamofire/issues/comments{/number}","contents_url":"https://api.github.com/repos/Alamofire/Alamofire/contents/{+path}","compare_url":"https://api.github.com/repos/Alamofire/Alamofire/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Alamofire/Alamofire/merges","archive_url":"https://api.github.com/repos/Alamofire/Alamofire/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Alamofire/Alamofire/downloads","issues_url":"https://api.github.com/repos/Alamofire/Alamofire/issues{/number}","pulls_url":"https://api.github.com/repos/Alamofire/Alamofire/pulls{/number}","milestones_url":"https://api.github.com/repos/Alamofire/Alamofire/milestones{/number}","notifications_url":"https://api.github.com/repos/Alamofire/Alamofire/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Alamofire/Alamofire/labels{/name}","releases_url":"https://api.github.com/repos/Alamofire/Alamofire/releases{/id}","deployments_url":"https://api.github.com/repos/Alamofire/Alamofire/deployments","created_at":"2014-07-31T05:56:19Z","updated_at":"2016-09-06T03:19:40Z","pushed_at":"2016-09-06T03:59:57Z","git_url":"git://github.com/Alamofire/Alamofire.git","ssh_url":"git@github.com:Alamofire/Alamofire.git","clone_url":"https://github.com/Alamofire/Alamofire.git","svn_url":"https://github.com/Alamofire/Alamofire","homepage":"","size":3910,"stargazers_count":18939,"watchers_count":18939,"language":"Swift","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":3032,"mirror_url":null,"open_issues_count":35,"forks":3032,"open_issues":35,"watchers":18939,"default_branch":"master"}},"base":{"label":"Alamofire:swift3","ref":"swift3","sha":"1589afd23322d269ab0519cd15d17f88fbda259e","user":{"login":"Alamofire","id":7774181,"avatar_url":"https://avatars.githubusercontent.com/u/7774181?v=3","gravatar_id":"","url":"https://api.github.com/users/Alamofire","html_url":"https://github.com/Alamofire","followers_url":"https://api.github.com/users/Alamofire/followers","following_url":"https://api.github.com/users/Alamofire/following{/other_user}","gists_url":"https://api.github.com/users/Alamofire/gists{/gist_id}","starred_url":"https://api.github.com/users/Alamofire/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Alamofire/subscriptions","organizations_url":"https://api.github.com/users/Alamofire/orgs","repos_url":"https://api.github.com/users/Alamofire/repos","events_url":"https://api.github.com/users/Alamofire/events{/privacy}","received_events_url":"https://api.github.com/users/Alamofire/received_events","type":"Organization","site_admin":false},"repo":{"id":22458259,"name":"Alamofire","full_name":"Alamofire/Alamofire","owner":{"login":"Alamofire","id":7774181,"avatar_url":"https://avatars.githubusercontent.com/u/7774181?v=3","gravatar_id":"","url":"https://api.github.com/users/Alamofire","html_url":"https://github.com/Alamofire","followers_url":"https://api.github.com/users/Alamofire/followers","following_url":"https://api.github.com/users/Alamofire/following{/other_user}","gists_url":"https://api.github.com/users/Alamofire/gists{/gist_id}","starred_url":"https://api.github.com/users/Alamofire/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Alamofire/subscriptions","organizations_url":"https://api.github.com/users/Alamofire/orgs","repos_url":"https://api.github.com/users/Alamofire/repos","events_url":"https://api.github.com/users/Alamofire/events{/privacy}","received_events_url":"https://api.github.com/users/Alamofire/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Alamofire/Alamofire","description":"Elegant HTTP Networking in Swift","fork":false,"url":"https://api.github.com/repos/Alamofire/Alamofire","forks_url":"https://api.github.com/repos/Alamofire/Alamofire/forks","keys_url":"https://api.github.com/repos/Alamofire/Alamofire/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Alamofire/Alamofire/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Alamofire/Alamofire/teams","hooks_url":"https://api.github.com/repos/Alamofire/Alamofire/hooks","issue_events_url":"https://api.github.com/repos/Alamofire/Alamofire/issues/events{/number}","events_url":"https://api.github.com/repos/Alamofire/Alamofire/events","assignees_url":"https://api.github.com/repos/Alamofire/Alamofire/assignees{/user}","branches_url":"https://api.github.com/repos/Alamofire/Alamofire/branches{/branch}","tags_url":"https://api.github.com/repos/Alamofire/Alamofire/tags","blobs_url":"https://api.github.com/repos/Alamofire/Alamofire/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Alamofire/Alamofire/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Alamofire/Alamofire/git/refs{/sha}","trees_url":"https://api.github.com/repos/Alamofire/Alamofire/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Alamofire/Alamofire/statuses/{sha}","languages_url":"https://api.github.com/repos/Alamofire/Alamofire/languages","stargazers_url":"https://api.github.com/repos/Alamofire/Alamofire/stargazers","contributors_url":"https://api.github.com/repos/Alamofire/Alamofire/contributors","subscribers_url":"https://api.github.com/repos/Alamofire/Alamofire/subscribers","subscription_url":"https://api.github.com/repos/Alamofire/Alamofire/subscription","commits_url":"https://api.github.com/repos/Alamofire/Alamofire/commits{/sha}","git_commits_url":"https://api.github.com/repos/Alamofire/Alamofire/git/commits{/sha}","comments_url":"https://api.github.com/repos/Alamofire/Alamofire/comments{/number}","issue_comment_url":"https://api.github.com/repos/Alamofire/Alamofire/issues/comments{/number}","contents_url":"https://api.github.com/repos/Alamofire/Alamofire/contents/{+path}","compare_url":"https://api.github.com/repos/Alamofire/Alamofire/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Alamofire/Alamofire/merges","archive_url":"https://api.github.com/repos/Alamofire/Alamofire/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Alamofire/Alamofire/downloads","issues_url":"https://api.github.com/repos/Alamofire/Alamofire/issues{/number}","pulls_url":"https://api.github.com/repos/Alamofire/Alamofire/pulls{/number}","milestones_url":"https://api.github.com/repos/Alamofire/Alamofire/milestones{/number}","notifications_url":"https://api.github.com/repos/Alamofire/Alamofire/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Alamofire/Alamofire/labels{/name}","releases_url":"https://api.github.com/repos/Alamofire/Alamofire/releases{/id}","deployments_url":"https://api.github.com/repos/Alamofire/Alamofire/deployments","created_at":"2014-07-31T05:56:19Z","updated_at":"2016-09-06T03:19:40Z","pushed_at":"2016-09-06T03:59:57Z","git_url":"git://github.com/Alamofire/Alamofire.git","ssh_url":"git@github.com:Alamofire/Alamofire.git","clone_url":"https://github.com/Alamofire/Alamofire.git","svn_url":"https://github.com/Alamofire/Alamofire","homepage":"","size":3910,"stargazers_count":18939,"watchers_count":18939,"language":"Swift","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":3032,"mirror_url":null,"open_issues_count":35,"forks":3032,"open_issues":35,"watchers":18939,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/Alamofire/Alamofire/pulls/1450"},"html":{"href":"https://github.com/Alamofire/Alamofire/pull/1450"},"issue":{"href":"https://api.github.com/repos/Alamofire/Alamofire/issues/1450"},"comments":{"href":"https://api.github.com/repos/Alamofire/Alamofire/issues/1450/comments"},"review_comments":{"href":"https://api.github.com/repos/Alamofire/Alamofire/pulls/1450/comments"},"review_comment":{"href":"https://api.github.com/repos/Alamofire/Alamofire/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Alamofire/Alamofire/pulls/1450/commits"},"statuses":{"href":"https://api.github.com/repos/Alamofire/Alamofire/statuses/2f005e490ba81bc73cf60cb83497a31b2a51137d"}},"merged":true,"mergeable":null,"mergeable_state":"unknown","merged_by":{"login":"cnoon","id":169110,"avatar_url":"https://avatars.githubusercontent.com/u/169110?v=3","gravatar_id":"","url":"https://api.github.com/users/cnoon","html_url":"https://github.com/cnoon","followers_url":"https://api.github.com/users/cnoon/followers","following_url":"https://api.github.com/users/cnoon/following{/other_user}","gists_url":"https://api.github.com/users/cnoon/gists{/gist_id}","starred_url":"https://api.github.com/users/cnoon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cnoon/subscriptions","organizations_url":"https://api.github.com/users/cnoon/orgs","repos_url":"https://api.github.com/users/cnoon/repos","events_url":"https://api.github.com/users/cnoon/events{/privacy}","received_events_url":"https://api.github.com/users/cnoon/received_events","type":"User","site_admin":false},"comments":3,"review_comments":21,"commits":7,"additions":659,"deletions":61,"changed_files":11}}
{"actor":{"display_login":"cnoon"}}