Whenever I use URLComponents, the server returns a 400 status code

While trying to create an API manager for my projects, I came across an issue using URLComponents. When I build my url using URLComponents, I get a 400 status code from the API, but if I build a request using a URL I build manually, I get back status code 200 (and the data I requested). I've put together some code to duplicate my issue. In the code I am trying to access Yelp's API, but I also tested it using OpenWeather's API and I get the same codes. Is there something about URLComponents that I'm not understanding? Thanks in advance.

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

let baseUrl = "https://api.yelp.com/v3"
let apiKey = "Bearer <MY API KEY>"
let endpoint = "/businesses/search"
let query = ["location": "90120"]
let headers = ["Authorization": apiKey]
let fullURL = URL(string: baseUrl + endpoint + "?location=90210")

print("****** TRY USING URLComponents ******")

// set base url
var urlComponent = URLComponents(string: baseUrl)!
// add enpoint path
urlComponent.path = endpoint
// map all query items
urlComponent.queryItems = query.map {
    URLQueryItem(name: $0, value: $1)
}
// build request based on url (should be baseUrl + endpoint + queries)
var request = URLRequest(url: urlComponent.url!)
// add headers
for (field, value) in headers {
    request.addValue(value, forHTTPHeaderField: field)
}

// make the request
makeReq(request: request)

// ****** END URLComponents *******

//Wait 5 seconds then try without URLComponents
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
    print("\n******* TRY WITHOUT URLComponents ***********")
    
    // reset request using fullUrl variable
    request = URLRequest(url: fullURL!)
    // add headers
    for (field, value) in headers {
        request.addValue(value, forHTTPHeaderField: field)
    }
    // make the request, again
    makeReq(request: request)
}

// make request function
func makeReq(request: URLRequest) {

    // start session
    let session = URLSession(configuration: .default)
    var dt: Void = session.dataTask(with: request) { (data, response, error) in
        
      /*  if let responseString = String(bytes: data!, encoding: .utf8) {
            print("Data:\n\(responseString)")
        } else { print("Data: No Data") } */
        
        if let httpResponse = response as? HTTPURLResponse {
            print("Response: \(httpResponse.statusCode)")
        } else { print("Response: No Response") }
    }.resume()
}

Debug Console Output:

Print the URLs created by both approaches and you might see a difference.

1 Like

I agree with @Jon_Shier it's probably best to print it out.

My guess is you don't need a leading / on your endpoint since Components will add it for you, so you're probably trying to access an endpoint which doesn't exist, hence the access denied error

The baseURL should only be the hostname + scheme (http://), whereas the path should include your v3, without a leading /.

1 Like

I think it makes sense to keep the version as part of the base url here. Presumably if the api version changes, it's easier to change this in exactly one location rather than for each one of the endpoints.

The problem is that if you modify the path variable (as hes doing, which is presumably why it breaks) you are also removing the v3, as its part of the path. Its all and good to try to separate the 2 for easier upgrading but then I'd recommend a function that can build the URLs for you that knows both about a baseURL and a baseAPIVersion or whatever.

Thanks everyone. Adding v3 to the baseURL was the issue. Ya know, I looked at the difference between the 2 URLs a hundred times, and I didn't notice it was omitted when using URLComponents. I guess I was too focused on making sure the queries added correctly. Maybe I should have looked at them one after the other. It's always that little thing you don't notice.

So I moved the version to the endpoint. I had to add a leading / or else the url would return as nil. I agree with @Spencer_Kohan in that I don't like attaching the version to the endpoint. I will probably end up adding a version option when initializing with a base URL.

let baseUrl = "https://api.yelp.com"
let apiKey = "Bearer <API KEY>"
let endpoint = "/v3/businesses/search"
let query = ["location": "90210"]
let headers = ["Authorization": apiKey]