I'm building a chat app in Swift using URLSession
. I have an authenticated GET request to fetch the current user's friends list. The request works perfectly in Postman using the same token and URL, but my iOS app consistently returns a 403 with { "detail": "Not Authenticated" }
.
What I’ve Tried:
Bearer <token>
)Info.plist
(server is http://52.23.164.179:8000
)My Code:
ContactListVC.swift:
private func fetchContacts() {
ContactService.shared.getFriends { [weak self] success, contacts in
guard let self = self else { return }
DispatchQueue.main.async {
if success, let contacts = contacts, !contacts.isEmpty {
self.contacts = contacts
self.tableView.reloadData()
} else {
self.contacts = []
self.showEmptyStateIfNeeded()
}
}
}
}
ContactService.swift:
func getFriends(completion: @escaping (Bool, [ChatPartner]?) -> Void) {
guard let request = contactRequest.getFriends() else {
completion(false, nil)
return
}
NetworkService.shared.sendRequest(request, parse: { data in
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let dataObject = json["data"] as? [String: Any],
let friendsArray = dataObject["friends"] as? [[String: Any]] else {
return nil
}
return friendsArray.compactMap { ChatPartner(json: $0) }
}) { result in
switch result {
case .success(let partners):
completion(true, partners)
case .failure:
completion(false, nil)
}
}
}
ContactRequest.swift:
func getFriends() -> URLRequest? {
guard let baseURL = BASE_URL else { return nil }
let url = baseURL.appendingPathComponent("friends")
var request = URLRequest(url: url)
request.setValue("Bearer \(token ?? "")", forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
return request
}
NetworkService.swift:
func sendRequest<T>(_ request: URLRequest, parse: @escaping (Data) -> T?, completion: @escaping (Result<T, NetworkError>) -> Void) {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse, let data = data else {
completion(.failure(.invalidResponse))
return
}
switch httpResponse.statusCode {
case 200:
if let result = parse(data) {
completion(.success(result))
} else {
completion(.failure(.parsingFailed))
}
default:
completion(.failure(.statusCode(httpResponse.statusCode)))
}
}
task.resume()
}
Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>52.23.164.179</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Questions:
Extra Notes:
Authorization
header by logging it.By using Instruments.app, I found that the response status code to the request is actually 307 - a redirect. URLSession
follows the redirect by sending a new request, without the authorisation header, and so you get a 403 at the end.
On the other hand, Postman retains the header, as long as the destination has the same host name. There is also a setting to retain the header even if it has a different host name.
The redirect is from /friends
to /friends/
, so I imagine changing the path to /friends/
would prevent the redirect.
Otherwise, you can pass a URLSessionTaskDelegate
when you call dataTask(...)
. The delegate would pass on the Authorization
header to the new request. Here is an example:
actor SomeDelegate: NSObject, URLSessionTaskDelegate {
nonisolated func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest? {
var newRequest = request
let oldRequest = task.currentRequest
newRequest.setValue(oldRequest?.value(forHTTPHeaderField: "Authorization"), forHTTPHeaderField: "Authorization")
return newRequest
}
}
let task = URLSession.shared.dataTask(with: request) { ... }
task.delegate = SomeDelegate()
task.resume()