diff --git a/GiteaPR/Networking/GiteaClient.swift b/GiteaPR/Networking/GiteaClient.swift index 4624208..f8ae9f1 100644 --- a/GiteaPR/Networking/GiteaClient.swift +++ b/GiteaPR/Networking/GiteaClient.swift @@ -22,6 +22,32 @@ struct GiteaClient { let token: String let session: URLSession + /// Shared decoder. Gitea returns ISO8601 date strings WITH a timezone + /// offset (e.g. "2026-08-12T11:09:13+02:00"), which neither the default + /// `.deferredToDate` strategy (expects a Unix timestamp number) nor + /// `.iso8601` (expects a trailing "Z") can parse. Without this, decoding + /// any `Date?` field throws and the whole array fails — the inbox shows + /// nothing even though the data is present. + private static let decoder: JSONDecoder = { + let d = JSONDecoder() + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, + .withFractionalSeconds] + d.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let str = try container.decode(String.self) + if let date = formatter.date(from: str) { return date } + // Fall back to a formatter without fractional seconds. + let plain = ISO8601DateFormatter() + plain.formatOptions = [.withInternetDateTime] + if let date = plain.date(from: str) { return date } + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Cannot decode date string \(str)") + } + return d + }() + init(instanceURL: URL, token: String, session: URLSession = .shared) { self.instanceURL = instanceURL self.token = token @@ -62,7 +88,7 @@ struct GiteaClient { case 404: throw APIError.notFound default: - let message = (try? JSONDecoder().decode([String: String].self, + let message = (try? Self.decoder.decode([String: String].self, from: data))?["message"] throw APIError.httpStatus(http.statusCode, message) } @@ -72,7 +98,7 @@ struct GiteaClient { query: [URLQueryItem]? = nil) async throws -> T { let data = try await request(path, query: query) do { - return try JSONDecoder().decode(T.self, from: data) + return try Self.decoder.decode(T.self, from: data) } catch { throw APIError.decoding(String(describing: error)) } @@ -80,7 +106,7 @@ struct GiteaClient { private func post(_ path: String, body: Data? = nil) async throws -> T { let data = try await request(path, method: "POST", body: body) - return try JSONDecoder().decode(T.self, from: data) + return try Self.decoder.decode(T.self, from: data) } private func postNoBody(_ path: String) async throws {