Swift: Urlsession.Shared.Datatask Says Status Code 304 = 200

Why isn't code being executed after/within a second URLSession.shared.dataTask, that is within an initial URLSession.shared.dataTask’s do block? Swift

Since you are targeting iOS 15, you are making things much harder for yourself by not embracing async/await. You can also use Codable to handle the JSON parsing.

First, create Codable structs to handle the result (hint app.quicktype.io can do this for you):

// MARK: - BusinessSearchResult
struct BusinessSearchResult: Codable {
let total: Int
let businesses: [Business]
let region: Region
}

// MARK: - Business
struct Business: Codable {
let rating: Double
let price, phone, alias: String?
let id: String
let isClosed: Bool?
let categories: [Category]
let reviewCount: Int?
let name: String
let url: String?
let coordinates: Center
let imageURL: String?
let location: Location
let distance: Double
let transactions: [String]

enum CodingKeys: String, CodingKey {
case rating, price, phone, id, alias
case isClosed
case categories
case reviewCount
case name, url, coordinates
case imageURL
case location, distance, transactions
}
}

// MARK: - Category
struct Category: Codable {
let alias, title: String
}

// MARK: - Center
struct Center: Codable {
let latitude, longitude: Double
}

// MARK: - Location
struct Location: Codable {
let city, country, address2, address3: String?
let state, address1, zipCode: String?

enum CodingKeys: String, CodingKey {
case city, country, address2, address3, state, address1
case zipCode
}
}

// MARK: - Region
struct Region: Codable {
let center: Center
}

Then you can create an api class that uses async/await to fetch the data.

The basic strategy is:

  • Fetch the first results
  • Take a note of the total results that are expected
  • Limit the total to 1000 (This is an API limit)
  • Keep making requests, increasing the offset each time, until you have the expected results.
  • Return the results
class YelpApi {

enum SortOption: String {
case bestMatch="best_match"
case rating="rating"
case reviewCount="review_count"
case distance="distance"
}

private var apiKey: String

init(apiKey: String) {
self.apiKey = apiKey
}

func searchBusiness(latitude: Double,
longitude: Double,
category: String? = nil,
sortBy: SortOption? = nil) async throws -> [Business] {

var queryItems = [URLQueryItem]()
queryItems.append(URLQueryItem(name:"latitude",value:"\(latitude)"))
queryItems.append(URLQueryItem(name:"longitude",value:"\(longitude)"))
if let category = category {
queryItems.append(URLQueryItem(name:"categories", value:category))
}
if let sortOption = sortBy {
queryItems.append(URLQueryItem(name:"sort_by",value:sortOption.rawValue))
}

var results = [Business]()

var expectedCount = 0
let countLimit = 50
var offset = 0

queryItems.append(URLQueryItem(name:"limit", value:"\(countLimit)"))

repeat {

var offsetQueryItems = queryItems

offsetQueryItems.append(URLQueryItem(name:"offset",value: "\(offset)"))

var urlComponents = URLComponents(string: "https://api.yelp.com/v3/businesses/search")
urlComponents?.queryItems = offsetQueryItems

guard let url = urlComponents?.url else {
throw URLError(.badURL)
}

var request = URLRequest(url: url)
request.setValue("Bearer \(self.apiKey)", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
let businessResults = try JSONDecoder().decode(BusinessSearchResult.self, from:data)

expectedCount = min(businessResults.total,1000)

results.append(contentsOf: businessResults.businesses)
offset += businessResults.businesses.count
} while (results.count < expectedCount)

return results
}
}

I have used URLComponents rather than string interpolation as this handles things like percent encoding where required.

Status code 400: NSHTTPURLResponse on calls made using URLSession.shared.dataTask

I have pinpoint the issue. Although the reason is still elusive.

In the generateRequest function I pass a dictionary of [String:String] with default values ["":""] for the request headers.

It appears that request.allHTTPHeaderFields = ["":""] will break your request.

In prior builds this did not cause any issues but this will result in a bad request on the current beta.

Update

I filled a bug with Apple when I first came across this issue and this was their response:

The problem was that in CFNetwork-856.4.7 (part of seed 1) we made a mistake and removed headers that were blank. In Seed 2, (CFNetwork-861.1) we limited the headers we would remove to 4 “special” headers (Content-Type, User-Agent, Accept-Language, Accept-Encoding). So, that’s why you saw the change in behavior.

macOS El Capitan also sends the “=“ header that results the 400 HTTP status code. So, this is behaving correctly now in Seed 2. Seed 1 was an anomaly and was wrong.

Please let us know whether the issue is resolved for you by updating your bug report.

How to display website status code to user?

You set up the .dataTask, but never actually execute that code...

    let dataURL = "https://google.com"
let request = URLRequest(url: URL(string: dataURL)!)

let task = URLSession.shared.dataTask(with: request) { [self] (data, response, error) in

// properly unwrap the optional response
if let httpResponse = response as? HTTPURLResponse {
let responsecode = httpResponse.statusCode
ServerStausLabel.text = String(responsecode)
} else {
print("Request Failed")
}

}

// this executes the request
task.resume()

How to use NSURLSession to determine if resource has changed?

I created a HTTPEntityFingerprint structure which stores some of the entity headers: Content-MD5, Etag, and Last-Modified.

import Foundation

struct HTTPEntityFingerprint {
let contentMD5 : String?
let etag : String?
let lastModified : String?
}

extension HTTPEntityFingerprint {
init?(response : NSURLResponse) {
if let httpResponse = response as? NSHTTPURLResponse {
let h = httpResponse.allHeaderFields
contentMD5 = h["Content-MD5"] as? String
etag = h["Etag"] as? String
lastModified = h["Last-Modified"] as? String

if contentMD5 == nil && etag == nil && lastModified == nil {
return nil
}
} else {
return nil
}
}

static func match(first : HTTPEntityFingerprint?, second : HTTPEntityFingerprint?) -> Bool {
if let a = first, b = second {
if let md5A = a.contentMD5, md5B = b.contentMD5 {
return md5A == md5B
}
if let etagA = a.etag, etagB = b.etag {
return etagA == etagB
}
if let lastA = a.lastModified, lastB = b.lastModified {
return lastA == lastB
}
}

return false
}
}

When I get an NSHTTPURLResponse from an NSURLSession, I create an HTTPEntityFingerprint from it and compare it against a previously stored fingerprint using HTTPEntityFingerprint.match. If the fingerprints match, then the HTTP resource hasn't changed and thus I do not need to deserialized the JSON response again; however, if the fingerprints do not match, then I deserialize the JSON response and save the new fingerprint.

This mechanism only works if your server returns at least one of the 3 entity headers: Content-MD5, Etag, or Last-Modified.

More Details on NSURLSession and NSURLCache Behavior

The caching provided by NSURLSession via NSURLCache is transparent, meaning when you request a previously cached resource NSURLSession will call the completion handlers/delegates as if a 200 response occurred.

If the cached response has expired then NSURLSession will send a new request to the origin server, but will include the If-Modified-Since and If-None-Match headers using the Last-Modified and Etag entity headers in the cached (though expired) result; this behavior is built in, you don't have to do anything besides enable caching. If the origin server returns a 304 (Not Modified), then NSURLSession will transform this to a 200 response the application (making it look like you fetched a new copy of the resource, even though it was still served from the cache).



Related Topics



Leave a reply



Submit