APIClient: URL.appending(path:) frisst Query-Strings — String-Concat statt

URL.appending(path:) behandelt den Input als reinen Path-Component und
percent-encoded Sonderzeichen, also auch ?. Damit wurde aus
/api/dogs/123/diary?limit=50 ein /api/dogs/123/diary%3Flimit=50, und der
Server lieferte was Anderes als JSON zurück → 'data was not valid JSON'.

Betraf auch Wetter, Giftköder, Verlorene Hunde, Gassi-Zeiten und alle
anderen Endpoints mit Query. Jetzt: baseURL.absoluteString + path.
This commit is contained in:
rene 2026-05-30 12:41:50 +02:00
parent 3373305b23
commit 12f8ba0be8

View file

@ -42,7 +42,7 @@ final class APIClient {
mimeType: String = "image/jpeg"
) async throws -> Data {
let boundary = "Boundary-\(UUID().uuidString)"
let url = baseURL.appending(path: path)
let url = Self.makeURL(baseURL: baseURL, path: path)
var req = URLRequest(url: url)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Accept")
@ -69,7 +69,7 @@ final class APIClient {
}
private func perform<T: Decodable>(method: String, path: String, body: Data?) async throws -> T {
let url = baseURL.appending(path: path)
let url = Self.makeURL(baseURL: baseURL, path: path)
var req = URLRequest(url: url)
req.httpMethod = method
req.setValue("application/json", forHTTPHeaderField: "Accept")
@ -98,7 +98,7 @@ final class APIClient {
/// Convenience for DELETE with no response body.
func delete(_ path: String) async throws {
let url = baseURL.appending(path: path)
let url = Self.makeURL(baseURL: baseURL, path: path)
var req = URLRequest(url: url)
req.httpMethod = "DELETE"
if let token { req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") }
@ -116,7 +116,7 @@ final class APIClient {
/// Convenience for PATCH with JSON body, decoding the response.
func patch<T: Decodable, B: Encodable>(_ path: String, body: B) async throws -> T {
let data = try encoder.encode(body)
let url = baseURL.appending(path: path)
let url = Self.makeURL(baseURL: baseURL, path: path)
var req = URLRequest(url: url)
req.httpMethod = "PATCH"
req.setValue("application/json", forHTTPHeaderField: "Accept")
@ -135,6 +135,12 @@ final class APIClient {
return try decoder.decode(T.self, from: respData)
}
/// Constructs a URL by joining baseURL and path as strings `URL.appending(path:)`
/// percent-encodes `?` and breaks query strings.
private static func makeURL(baseURL: URL, path: String) -> URL {
URL(string: baseURL.absoluteString + path) ?? baseURL
}
/// FastAPI returns errors as {"detail": "message"} or {"detail": [{...}]}.
private static func parseErrorDetail(from data: Data) -> String? {
if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {