@@ -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