URL.appending(path:) behandelt das ? in Query-Strings als Pfad- Component und URL-encoded es zu %3F. Server-Route-Matching scheiterte mit 404 für alle Endpoints mit Query-Parametern. Symptom in cards-native v0.8.x: alle Card-Counts und Due-Counts auf 0, DeckDetailView Cards-Liste leer mit "Server-Fehler (404)" auf /api/v1/cards?deck_id=X. Fix: String-Konkatenation baseURL.absoluteString + path. Caller liefert path inkl. führendem / und optionaler Query. URLRequest parsed das Resultat korrekt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
2.7 KiB
Swift
69 lines
2.7 KiB
Swift
import Foundation
|
|
|
|
/// Authentifizierter HTTP-Transport. Setzt automatisch den `Authorization: Bearer`-
|
|
/// Header, refreshed bei `401` einmal proaktiv und wiederholt den Request.
|
|
///
|
|
/// Eine Instanz pro App-Backend (z.B. memoro-api, cards-api). Beim Init
|
|
/// die Basis-URL des jeweiligen App-Servers übergeben, nicht die mana-auth-URL.
|
|
public actor AuthenticatedTransport {
|
|
private let baseURL: URL
|
|
private let session: URLSession
|
|
private let auth: AuthClient
|
|
|
|
public init(baseURL: URL, auth: AuthClient, session: URLSession = .shared) {
|
|
self.baseURL = baseURL
|
|
self.session = session
|
|
self.auth = auth
|
|
}
|
|
|
|
/// Führt einen authentifizierten Request aus. Bei `401` wird der
|
|
/// Access-Token einmal refreshed und der Call wiederholt.
|
|
public func request(
|
|
path: String,
|
|
method: String = "GET",
|
|
body: Data? = nil,
|
|
contentType: String = "application/json"
|
|
) async throws -> (Data, HTTPURLResponse) {
|
|
let token = try await auth.freshAccessToken()
|
|
let response = try await send(path: path, method: method, body: body, contentType: contentType, token: token)
|
|
if response.1.statusCode == 401 {
|
|
let refreshed = try await auth.refreshAccessToken()
|
|
return try await send(path: path, method: method, body: body, contentType: contentType, token: refreshed)
|
|
}
|
|
return response
|
|
}
|
|
|
|
private func send(
|
|
path: String,
|
|
method: String,
|
|
body: Data?,
|
|
contentType: String,
|
|
token: String
|
|
) async throws -> (Data, HTTPURLResponse) {
|
|
// String-Konkatenation statt `baseURL.appending(path:)`, weil
|
|
// letzteres das `?` in Query-Strings als Path-Component
|
|
// behandelt und URL-encoded (`?` → `%3F`) — der Server-Routes-
|
|
// Match scheitert dann mit 404. Caller liefert path inkl.
|
|
// führendem `/` und optionaler Query.
|
|
guard let url = URL(string: baseURL.absoluteString + path) else {
|
|
throw AuthError.networkFailure("Ungültige URL: \(path)")
|
|
}
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = method
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
if let body {
|
|
request.httpBody = body
|
|
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
|
|
}
|
|
|
|
do {
|
|
let (data, response) = try await session.data(for: request)
|
|
guard let http = response as? HTTPURLResponse else {
|
|
throw AuthError.networkFailure("Keine HTTP-Antwort")
|
|
}
|
|
return (data, http)
|
|
} catch let error as URLError {
|
|
throw AuthError.networkFailure(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|