When building a GET
request, there is no body to the request, but rather everything goes on the URL. To build a URL (and properly percent escaping it), you can also use URLComponents
.
var components = URLComponents(string: "https://www.google.com/search/")!
components.queryItems = [
URLQueryItem(name: "q", value: "War & Peace")
]
guard let url = components.url else {
throw URLError(.badURL)
}
The only trick is that most web services need +
character percent escaped (because they'll interpret that as a space character as dictated by the application/x-www-form-urlencoded
specification). But URLComponents
will not percent escape it. Apple contends that +
is a valid character in a query and therefore shouldn't be escaped. Technically, they are correct, that it is allowed in a query of a URI, but it has a special meaning in application/x-www-form-urlencoded
requests and really should not be passed unescaped.
When I presented this issue to Apple support, they advised to manually percent escaping the +
characters:
components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
This is an inelegant work-around, but it works, and is what Apple advises if your queries may include a +
character and you have a server that interprets them as spaces.
Anyway, combining that with your sendRequest
routine, you might end up with something like:
enum WebServiceError: Error {
case invalidResponse(Data, URLResponse)
case statusCode(Int, Data)
}
func object<T: Decodable>(from baseUrl: URL, parameters: [String: String]? = nil) async throws -> T {
guard var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: false) else {
throw URLError(.badURL)
}
components.queryItems = parameters?.map { (key, value) in
URLQueryItem(name: key, value: value)
}
components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
guard let url = components.url else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { // is there HTTP response
throw WebServiceError.invalidResponse(data, response)
}
guard 200 ..< 300 ~= statusCode else { // is statusCode 2XX
throw WebServiceError.statusCode(statusCode, data)
}
return try JSONDecoder().decode(T.self, from: data)
}
And you'd call it like:
do {
let foo: Foo = try await object(from: baseUrl)
// do something with `foo` here
} catch WebServiceError.statusCode(404, _) { // if you want, you can catch individual status codes here
// handle not found error here
} catch {
// handle other errors here
}
Clearly, there are lots of permutations on the idea, but hopefully this illustrates the basic idea of how to percent encode the parameters into the URL of a GET request.
See previous revisions of this answer for Swift 2, manual percent escaping renditions, and non-Swift concurrency renditions of the above.
extension string
is doing to the values ? Also when would I need to useHttpBody
then? – Apriorism