From b2311d8007ffff7021d491b86d510f1fd2ccc522 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 12 Aug 2026 17:51:46 +0000 Subject: [PATCH] Fix PR inbox: decode Gitea ISO8601 dates with timezone offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea returns dates like 2026-08-12T11:09:13+02:00. The default JSONDecoder date strategy expects a Unix timestamp number, so decoding any Date? field threw and the whole [PullRequest] array failed — the inbox showed no PRs. Add a shared decoder with a custom ISO8601 strategy (with and without fractional seconds) and use it for all get/post decoding. --- GiteaPR/Networking/GiteaClient.swift | 32 +++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) 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 {