summaryrefslogtreecommitdiff
path: root/Hutch/Networking/SRHTClient.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-16 01:06:46 -0500
committerChristian Cleberg <[email protected]>2026-07-16 01:06:46 -0500
commit871b04159aa47c0b0e62e2520c2f30e81f0f024b (patch)
tree5dcc31ad97b094f52a59a539ae3950ae0dc2ab4c /Hutch/Networking/SRHTClient.swift
parentd32ad837ac9eb153598c10c3cf47fbdb4e51e3ba (diff)
downloadhutch-871b04159aa47c0b0e62e2520c2f30e81f0f024b.tar.gz
hutch-871b04159aa47c0b0e62e2520c2f30e81f0f024b.tar.bz2
hutch-871b04159aa47c0b0e62e2520c2f30e81f0f024b.zip
fix: download artifacts through the API instead of handing them to Safari
Tapping download opened Artifact.url in the browser, which answered with "Authorization header is required". That URL is not a web page: git.sr.ht resolves it to <api origin>/query/artifact/<checksum>/<filename>, which demands a bearer token. Safari has none and no way to get one, so the download could never have worked — this predates the upload work. Fetch it with the client that already holds the token and hand the user the file through a share sheet. fetchData mirrors fetchText, including its host guard, so an authenticated request still cannot be aimed anywhere but *.sr.ht over https. Also guards zero-byte uploads. sr.ht streams into S3, which rejects a zero-part multipart completion with "MalformedXML: UnknownError" — an error that says nothing about the cause and cost a round of testing to identify. Empty files are now refused by name before the request is made.
Diffstat (limited to 'Hutch/Networking/SRHTClient.swift')
-rw-r--r--Hutch/Networking/SRHTClient.swift37
1 files changed, 37 insertions, 0 deletions
diff --git a/Hutch/Networking/SRHTClient.swift b/Hutch/Networking/SRHTClient.swift
index 94531f4..8aaa4aa 100644
--- a/Hutch/Networking/SRHTClient.swift
+++ b/Hutch/Networking/SRHTClient.swift
@@ -276,6 +276,43 @@ final class SRHTClient: Sendable {
// MARK: - Plain-text fetch
+ /// Fetch the bytes at a URL using the same authorization header.
+ ///
+ /// sr.ht serves some resources from the API origin rather than the web one —
+ /// `Artifact.url` is `https://git.sr.ht/query/artifact/<checksum>/<filename>`
+ /// — and those return an auth error to anything without a bearer token. They
+ /// cannot be handed to a browser; they have to be fetched here.
+ func fetchData(url: URL) async throws -> Data {
+ guard let token = tokenLock.withLock({ $0 }), !token.isEmpty else {
+ throw SRHTError.unauthorized
+ }
+ guard Self.isTrustedAuthenticatedTextURL(url) else {
+ throw SRHTError.invalidAuthenticatedURL(url)
+ }
+
+ var request = URLRequest(url: url)
+ request.setValue(Bundle.main.hutchUserAgent, forHTTPHeaderField: "User-Agent")
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+
+ let (data, response): (Data, URLResponse)
+ do {
+ (data, response) = try await session.data(for: request)
+ } catch {
+ throw SRHTError.networkError(error)
+ }
+
+ if let http = response as? HTTPURLResponse {
+ if http.statusCode == 401 {
+ throw SRHTError.unauthorized
+ }
+ if !(200...299).contains(http.statusCode) {
+ throw SRHTError.httpError(http.statusCode)
+ }
+ }
+
+ return data
+ }
+
/// Fetch the contents of a URL as plain text, using the same authorization header.
/// Used for build logs and other non-GraphQL resources.
func fetchText(url: URL) async throws -> String {