Skip to content

Commit d363e83

Browse files
Improve metadata request handling (#285)
* Use cache and static urlsession for metadata requests * Formatting * Update .gitignore * Fix comment * Cleanup httpHead interface * Formatting * Remove metadata request caching - Better handled with a follow up PR to prevent unintended edge cases * Update doc comment --------- Co-authored-by: Pedro Cuenca <[email protected]>
1 parent ef18d80 commit d363e83

File tree

2 files changed

+38
-12
lines changed

2 files changed

+38
-12
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.DS_Store
2-
/.build
2+
**/.build
33
/.swiftpm
44
Package.resolved
55
/Packages

Sources/Hub/HubApi.swift

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ public struct HubApi: Sendable {
7979
public typealias RepoType = Hub.RepoType
8080
public typealias Repo = Hub.Repo
8181

82+
/// Session actor for metadata requests with relative redirect handling (used in HEAD requests).
83+
///
84+
/// Static to share a single URLSession across all HubApi instances, preventing resource
85+
/// exhaustion when many instances are created. Persists for process lifetime.
86+
private static let redirectSession: RedirectSessionActor = .init()
87+
8288
/// Initializes a new Hub API client.
8389
///
8490
/// - Parameters:
@@ -184,7 +190,7 @@ public extension HubApi {
184190
/// - Throws: HubClientError for authentication, network, or HTTP errors
185191
func httpGet(for url: URL) async throws -> (Data, HTTPURLResponse) {
186192
var request = URLRequest(url: url)
187-
if let hfToken {
193+
if let hfToken, !hfToken.isEmpty {
188194
request.setValue("Bearer \(hfToken)", forHTTPHeaderField: "Authorization")
189195
}
190196

@@ -213,23 +219,23 @@ public extension HubApi {
213219

214220
/// Performs an HTTP HEAD request to retrieve metadata without downloading content.
215221
///
216-
/// Allows relative redirects but ignores absolute ones for LFS files.
222+
/// Uses a shared URLSession with custom redirect handling that only allows relative redirects
223+
/// and blocks absolute redirects (important for LFS file security).
217224
///
218225
/// - Parameter url: The URL to request
219-
/// - Returns: A tuple containing the response data and HTTP response
226+
/// - Returns: The HTTP response containing headers and status code
220227
/// - Throws: HubClientError if the page does not exist or is not accessible
221-
func httpHead(for url: URL) async throws -> (Data, HTTPURLResponse) {
228+
func httpHead(for url: URL) async throws -> HTTPURLResponse {
222229
var request = URLRequest(url: url)
223230
request.httpMethod = "HEAD"
224-
if let hfToken {
231+
if let hfToken, !hfToken.isEmpty {
225232
request.setValue("Bearer \(hfToken)", forHTTPHeaderField: "Authorization")
226233
}
227234
request.setValue("identity", forHTTPHeaderField: "Accept-Encoding")
228235

229-
let redirectDelegate = RedirectDelegate()
230-
let session = URLSession(configuration: .default, delegate: redirectDelegate, delegateQueue: nil)
231-
232-
let (data, response) = try await session.data(for: request)
236+
// Use shared session with redirect handling to avoid creating multiple URLSession instances
237+
let session = await Self.redirectSession.get()
238+
let (_, response) = try await session.data(for: request)
233239
guard let response = response as? HTTPURLResponse else { throw Hub.HubClientError.unexpectedError }
234240

235241
switch response.statusCode {
@@ -239,7 +245,7 @@ public extension HubApi {
239245
default: throw Hub.HubClientError.httpStatusCode(response.statusCode)
240246
}
241247

242-
return (data, response)
248+
return response
243249
}
244250

245251
/// Retrieves the list of filenames in a repository that match the specified glob patterns.
@@ -732,7 +738,7 @@ public extension HubApi {
732738
}
733739

734740
func getFileMetadata(url: URL) async throws -> FileMetadata {
735-
let (_, response) = try await httpHead(for: url)
741+
let response = try await httpHead(for: url)
736742
let location = response.statusCode == 302 ? response.value(forHTTPHeaderField: "Location") : response.url?.absoluteString
737743

738744
return FileMetadata(
@@ -1067,3 +1073,23 @@ private final class RedirectDelegate: NSObject, URLSessionTaskDelegate, Sendable
10671073
completionHandler(nil)
10681074
}
10691075
}
1076+
1077+
/// Actor to manage shared URLSession for redirect handling.
1078+
///
1079+
/// Lazily initializes and reuses a single URLSession across all HubApi instances
1080+
/// to avoid resource exhaustion when running multiple tests or creating many instances.
1081+
private actor RedirectSessionActor {
1082+
private var urlSession: URLSession?
1083+
1084+
func get() -> URLSession {
1085+
if let urlSession = urlSession {
1086+
return urlSession
1087+
}
1088+
1089+
// Create session once and reuse
1090+
let redirectDelegate = RedirectDelegate()
1091+
let session = URLSession(configuration: .default, delegate: redirectDelegate, delegateQueue: nil)
1092+
self.urlSession = session
1093+
return session
1094+
}
1095+
}

0 commit comments

Comments
 (0)