From 63f1cb1f25f068504262e5bedb165aa07b4f43a0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 6 Apr 2026 19:53:47 +0200 Subject: [PATCH] Inject basic auth via per-instance HTTPClient default headers Replace the YatteeServerAPI setAuthHeader/buildHeaders pattern (which was race-prone on the shared actor across multiple instances) with a generic mechanism: HTTPClient now supports a defaultHeaders dictionary applied to every request, and ContentService builds a per-instance HTTPClient with the basic-auth Authorization header baked in whenever credentials are configured. The same code path now works uniformly for Invidious, Piped, PeerTube, and Yattee Server, so any instance sitting behind a reverse proxy that requires HTTP Basic Auth can be authenticated regardless of backend type. Cached default API actors are still reused when no basic-auth header is needed. --- Yattee/Services/API/ContentService.swift | 85 +++++++++---------- Yattee/Services/API/YatteeServerAPI.swift | 67 ++++++--------- .../BackgroundFeedRefresher.swift | 8 +- Yattee/Services/Networking/HTTPClient.swift | 20 ++++- Yattee/Services/SubscriptionFeedCache.swift | 16 ++-- .../Subscriptions/ManageChannelsView.swift | 8 +- 6 files changed, 103 insertions(+), 101 deletions(-) diff --git a/Yattee/Services/API/ContentService.swift b/Yattee/Services/API/ContentService.swift index 0df1efa9..2eabd494 100644 --- a/Yattee/Services/API/ContentService.swift +++ b/Yattee/Services/API/ContentService.swift @@ -68,60 +68,49 @@ actor ContentService: ContentServiceProtocol { // MARK: - Routing + /// Builds a per-instance HTTPClient with the basic-auth `Authorization` header baked in, + /// or returns nil if no basic-auth credentials are configured for the instance. + /// Used to inject reverse-proxy basic auth uniformly across all backends. + private func httpClientWithBasicAuth(for instance: Instance) async -> HTTPClient? { + guard let authHeader = await basicAuthCredentialsManager?.basicAuthHeader(for: instance) else { + return nil + } + let client = httpClientFactory.createClient(for: instance) + await client.setDefaultHeaders(["Authorization": authHeader]) + return client + } + /// Returns an API client configured for the instance's SSL and auth requirements. private func api(for instance: Instance) async -> any InstanceAPI { - // For Yattee Server, use the dedicated method that handles auth - if instance.type == .yatteeServer { - return await yatteeServerAPI(for: instance) - } - - // For instances with standard SSL, use cached default API clients - if !instance.allowInvalidCertificates { - switch instance.type { - case .invidious: - return defaultInvidiousAPI - case .piped: - return defaultPipedAPI - case .peertube: - return defaultPeerTubeAPI - case .yatteeServer: - fatalError("Should be handled above") - } - } - - // For instances with allowInvalidCertificates, create API with insecure HTTPClient - let insecureClient = httpClientFactory.createClient(for: instance) switch instance.type { case .invidious: - return InvidiousAPI(httpClient: insecureClient) + return await invidiousAPI(for: instance) case .piped: - return PipedAPI(httpClient: insecureClient) + return await pipedAPI(for: instance) case .peertube: - return PeerTubeAPI(httpClient: insecureClient) + return await peerTubeAPI(for: instance) case .yatteeServer: - fatalError("Should be handled above") + return await yatteeServerAPI(for: instance) } } /// Returns a YatteeServerAPI configured for the instance's SSL and auth requirements. private func yatteeServerAPI(for instance: Instance) async -> YatteeServerAPI { - let api: YatteeServerAPI - if !instance.allowInvalidCertificates { - api = defaultYatteeServerAPI - } else { - let insecureClient = httpClientFactory.createClient(for: instance) - api = YatteeServerAPI(httpClient: insecureClient) + if let authClient = await httpClientWithBasicAuth(for: instance) { + return YatteeServerAPI(httpClient: authClient) } - - // Fetch auth header directly from credentials manager (avoids race condition on app startup) - let authHeader = await basicAuthCredentialsManager?.basicAuthHeader(for: instance) - await api.setAuthHeader(authHeader) - - return api + if !instance.allowInvalidCertificates { + return defaultYatteeServerAPI + } + let insecureClient = httpClientFactory.createClient(for: instance) + return YatteeServerAPI(httpClient: insecureClient) } - /// Returns an InvidiousAPI configured for the instance's SSL requirements. - private func invidiousAPI(for instance: Instance) -> InvidiousAPI { + /// Returns an InvidiousAPI configured for the instance's SSL and auth requirements. + private func invidiousAPI(for instance: Instance) async -> InvidiousAPI { + if let authClient = await httpClientWithBasicAuth(for: instance) { + return InvidiousAPI(httpClient: authClient) + } if !instance.allowInvalidCertificates { return defaultInvidiousAPI } @@ -129,8 +118,11 @@ actor ContentService: ContentServiceProtocol { return InvidiousAPI(httpClient: insecureClient) } - /// Returns a PipedAPI configured for the instance's SSL requirements. - private func pipedAPI(for instance: Instance) -> PipedAPI { + /// Returns a PipedAPI configured for the instance's SSL and auth requirements. + private func pipedAPI(for instance: Instance) async -> PipedAPI { + if let authClient = await httpClientWithBasicAuth(for: instance) { + return PipedAPI(httpClient: authClient) + } if !instance.allowInvalidCertificates { return defaultPipedAPI } @@ -138,8 +130,11 @@ actor ContentService: ContentServiceProtocol { return PipedAPI(httpClient: insecureClient) } - /// Returns a PeerTubeAPI configured for the instance's SSL requirements. - private func peerTubeAPI(for instance: Instance) -> PeerTubeAPI { + /// Returns a PeerTubeAPI configured for the instance's SSL and auth requirements. + private func peerTubeAPI(for instance: Instance) async -> PeerTubeAPI { + if let authClient = await httpClientWithBasicAuth(for: instance) { + return PeerTubeAPI(httpClient: authClient) + } if !instance.allowInvalidCertificates { return defaultPeerTubeAPI } @@ -286,7 +281,7 @@ actor ContentService: ContentServiceProtocol { return try await yatteeServerAPI(for: instance).videoWithStreamsAndCaptionsAndStoryboards(id: id, instance: instance) case .piped: // Piped fallback - make separate calls (no storyboard support) - let pipedAPI = pipedAPI(for: instance) + let pipedAPI = await pipedAPI(for: instance) async let videoTask = pipedAPI.video(id: id, instance: instance) async let streamsTask = pipedAPI.streams(videoID: id, instance: instance) async let captionsTask = pipedAPI.captions(videoID: id, instance: instance) @@ -298,7 +293,7 @@ actor ContentService: ContentServiceProtocol { case .peertube: // PeerTube fallback - make separate calls (no storyboard support) - let peerTubeAPI = peerTubeAPI(for: instance) + let peerTubeAPI = await peerTubeAPI(for: instance) async let videoTask = peerTubeAPI.video(id: id, instance: instance) async let streamsTask = peerTubeAPI.streams(videoID: id, instance: instance) async let captionsTask = peerTubeAPI.captions(videoID: id, instance: instance) diff --git a/Yattee/Services/API/YatteeServerAPI.swift b/Yattee/Services/API/YatteeServerAPI.swift index 632458ce..95dd2d8c 100644 --- a/Yattee/Services/API/YatteeServerAPI.swift +++ b/Yattee/Services/API/YatteeServerAPI.swift @@ -14,42 +14,23 @@ actor YatteeServerAPI: InstanceAPI { private let httpClient: HTTPClient - /// Optional Basic Auth header value for authenticated requests. - /// Set this when the server requires authentication. - private var authHeader: String? - init(httpClient: HTTPClient) { self.httpClient = httpClient } - /// Sets the Basic Auth header to use for all subsequent requests. - /// - Parameter authHeader: The Authorization header value (e.g., "Basic dXNlcjpwYXNz") - func setAuthHeader(_ authHeader: String?) { - self.authHeader = authHeader - } - - /// Builds custom headers including auth if available. - private func buildHeaders(additionalHeaders: [String: String]? = nil) -> [String: String]? { - var headers = additionalHeaders ?? [:] - if let authHeader { - headers["Authorization"] = authHeader - } - return headers.isEmpty ? nil : headers - } - // MARK: - InstanceAPI func trending(instance: Instance) async throws -> [Video] { // Yattee server proxies trending from Invidious if configured let endpoint = GenericEndpoint.get("/api/v1/trending") - let response: [YatteeVideo] = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: [YatteeVideo] = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.map { $0.toVideo() } } func popular(instance: Instance) async throws -> [Video] { // Yattee server proxies popular from Invidious if configured let endpoint = GenericEndpoint.get("/api/v1/popular") - let response: [YatteeVideo] = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: [YatteeVideo] = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.map { $0.toVideo() } } @@ -71,7 +52,7 @@ actor YatteeServerAPI: InstanceAPI { } let endpoint = GenericEndpoint.get("/api/v1/search", query: queryParams) - let response: [YatteeSearchItem] = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: [YatteeSearchItem] = try await httpClient.fetch(endpoint, baseURL: instance.url) // Helper to detect playlist IDs that may be returned as "video" type // YouTube playlist IDs start with: PL (user playlist), RD (mix), OL (offline mix), UU (uploads) @@ -131,18 +112,18 @@ actor YatteeServerAPI: InstanceAPI { let endpoint = GenericEndpoint.get("/api/v1/search/suggestions", query: [ "q": query ]) - return try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + return try await httpClient.fetch(endpoint, baseURL: instance.url) } func video(id: String, instance: Instance) async throws -> Video { let endpoint = GenericEndpoint.get("/api/v1/videos/\(id)") - let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.toVideo() } func channel(id: String, instance: Instance) async throws -> Channel { let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)") - let response: YatteeChannel = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeChannel = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.toChannel() } @@ -152,7 +133,7 @@ actor YatteeServerAPI: InstanceAPI { query["continuation"] = continuation } let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)/videos", query: query) - let response: YatteeChannelVideosWithContinuation = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeChannelVideosWithContinuation = try await httpClient.fetch(endpoint, baseURL: instance.url) return ChannelVideosPage( videos: response.videos.map { $0.toVideo() }, continuation: response.continuation @@ -165,7 +146,7 @@ actor YatteeServerAPI: InstanceAPI { query["continuation"] = continuation } let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)/playlists", query: query) - let response: YatteeChannelPlaylists = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeChannelPlaylists = try await httpClient.fetch(endpoint, baseURL: instance.url) return ChannelPlaylistsPage( playlists: response.playlists.map { $0.toPlaylist() }, continuation: response.continuation @@ -178,7 +159,7 @@ actor YatteeServerAPI: InstanceAPI { query["continuation"] = continuation } let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)/shorts", query: query) - let response: YatteeChannelVideosWithContinuation = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeChannelVideosWithContinuation = try await httpClient.fetch(endpoint, baseURL: instance.url) return ChannelVideosPage( videos: response.videos.map { $0.toVideo() }, continuation: response.continuation @@ -191,7 +172,7 @@ actor YatteeServerAPI: InstanceAPI { query["continuation"] = continuation } let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)/streams", query: query) - let response: YatteeChannelVideosWithContinuation = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeChannelVideosWithContinuation = try await httpClient.fetch(endpoint, baseURL: instance.url) return ChannelVideosPage( videos: response.videos.map { $0.toVideo() }, continuation: response.continuation @@ -200,7 +181,7 @@ actor YatteeServerAPI: InstanceAPI { func playlist(id: String, instance: Instance) async throws -> Playlist { let endpoint = GenericEndpoint.get("/api/v1/playlists/\(id)") - let response: YatteePlaylist = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteePlaylist = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.toPlaylist() } @@ -212,7 +193,7 @@ actor YatteeServerAPI: InstanceAPI { } let endpoint = GenericEndpoint.get("/api/v1/comments/\(videoID)", query: query) do { - let response: YatteeComments = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeComments = try await httpClient.fetch(endpoint, baseURL: instance.url) return CommentsPage( comments: response.comments.map { $0.toComment() }, continuation: response.continuation @@ -227,13 +208,13 @@ actor YatteeServerAPI: InstanceAPI { func streams(videoID: String, instance: Instance) async throws -> [Stream] { let endpoint = GenericEndpoint.get("/api/v1/videos/\(videoID)") - let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.toStreams() } func captions(videoID: String, instance: Instance) async throws -> [Caption] { let endpoint = GenericEndpoint.get("/api/v1/videos/\(videoID)") - let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.toCaptions(baseURL: instance.url) } @@ -243,7 +224,7 @@ actor YatteeServerAPI: InstanceAPI { "page": String(page) ]) // Yattee Server returns {"videos": [...]} wrapper, not a plain array - let response: YatteeChannelSearchResponse = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeChannelSearchResponse = try await httpClient.fetch(endpoint, baseURL: instance.url) var items: [ChannelSearchItem] = [] @@ -264,7 +245,7 @@ actor YatteeServerAPI: InstanceAPI { func videoWithStreamsAndCaptionsAndStoryboards(id: String, instance: Instance) async throws -> (video: Video, streams: [Stream], captions: [Caption], storyboards: [Storyboard]) { let endpoint = GenericEndpoint.get("/api/v1/videos/\(id)") - let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) return ( video: response.toVideo(), streams: response.toStreams(), @@ -280,7 +261,7 @@ actor YatteeServerAPI: InstanceAPI { /// directly to YouTube CDN, allowing the server to download at full speed and serve locally. func proxyStreams(videoID: String, instance: Instance) async throws -> [Stream] { let endpoint = GenericEndpoint.get("/api/v1/videos/\(videoID)", query: ["proxy": "true"]) - let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.toStreams() } @@ -288,7 +269,7 @@ actor YatteeServerAPI: InstanceAPI { /// Use this for downloads to get streams that route through the server. func videoWithProxyStreamsAndCaptionsAndStoryboards(id: String, instance: Instance) async throws -> (video: Video, streams: [Stream], captions: [Caption], storyboards: [Storyboard]) { let endpoint = GenericEndpoint.get("/api/v1/videos/\(id)", query: ["proxy": "true"]) - let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) return ( video: response.toVideo(), streams: response.toStreams(), @@ -315,7 +296,7 @@ actor YatteeServerAPI: InstanceAPI { queryItems: [URLQueryItem(name: "url", value: url.absoluteString)], timeout: 180 // 3 minutes for slow site extraction ) - let response: YatteeExternalVideo = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeExternalVideo = try await httpClient.fetch(endpoint, baseURL: instance.url) return ( video: response.toVideo(originalURL: url), streams: response.toStreams(), @@ -343,7 +324,7 @@ actor YatteeServerAPI: InstanceAPI { ], timeout: 180 // 3 minutes for slow site extraction ) - let response: YatteeExternalChannel = try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + let response: YatteeExternalChannel = try await httpClient.fetch(endpoint, baseURL: instance.url) return ( channel: response.toChannel(originalURL: url), videos: response.toVideos(channelURL: url), @@ -357,14 +338,14 @@ actor YatteeServerAPI: InstanceAPI { func postFeed(channels: [StatelessChannelRequest], limit: Int, offset: Int, instance: Instance) async throws -> StatelessFeedResponse { let body = StatelessFeedRequest(channels: channels, limit: limit, offset: offset) let endpoint = GenericEndpoint.post("/api/v1/feed", body: body) - return try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + return try await httpClient.fetch(endpoint, baseURL: instance.url) } /// Checks feed status for given channels (lightweight polling). func postFeedStatus(channels: [StatelessChannelStatusRequest], instance: Instance) async throws -> StatelessFeedStatusResponse { let body = StatelessFeedStatusRequest(channels: channels) let endpoint = GenericEndpoint.post("/api/v1/feed/status", body: body) - return try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + return try await httpClient.fetch(endpoint, baseURL: instance.url) } // MARK: - Channel Metadata @@ -374,7 +355,7 @@ actor YatteeServerAPI: InstanceAPI { func channelsMetadata(channelIDs: [String], instance: Instance) async throws -> ChannelsMetadataResponse { let body = ChannelMetadataRequest(channelIds: channelIDs) let endpoint = GenericEndpoint.post("/api/v1/channels/metadata", body: body) - return try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + return try await httpClient.fetch(endpoint, baseURL: instance.url) } // MARK: - Server Info @@ -382,7 +363,7 @@ actor YatteeServerAPI: InstanceAPI { /// Fetches server info including version, dependencies, and enabled sites. func fetchServerInfo(for instance: Instance) async throws -> InstanceDetectorModels.YatteeServerFullInfo { let endpoint = GenericEndpoint.get("/info") - return try await httpClient.fetch(endpoint, baseURL: instance.url, customHeaders: buildHeaders()) + return try await httpClient.fetch(endpoint, baseURL: instance.url) } } diff --git a/Yattee/Services/BackgroundRefresh/BackgroundFeedRefresher.swift b/Yattee/Services/BackgroundRefresh/BackgroundFeedRefresher.swift index ebaf885c..e23146b4 100644 --- a/Yattee/Services/BackgroundRefresh/BackgroundFeedRefresher.swift +++ b/Yattee/Services/BackgroundRefresh/BackgroundFeedRefresher.swift @@ -101,9 +101,11 @@ final class BackgroundFeedRefresher { } do { - let yatteeServerAPI = YatteeServerAPI(httpClient: HTTPClient()) - let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: yatteeServer) - await yatteeServerAPI.setAuthHeader(authHeader) + let httpClient = HTTPClient() + if let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: yatteeServer) { + await httpClient.setDefaultHeaders(["Authorization": authHeader]) + } + let yatteeServerAPI = YatteeServerAPI(httpClient: httpClient) let response = try await yatteeServerAPI.postFeed( channels: channelRequests, limit: 5 * notifiableSubscriptions.count, diff --git a/Yattee/Services/Networking/HTTPClient.swift b/Yattee/Services/Networking/HTTPClient.swift index b6887f65..61a200ea 100644 --- a/Yattee/Services/Networking/HTTPClient.swift +++ b/Yattee/Services/Networking/HTTPClient.swift @@ -16,6 +16,12 @@ actor HTTPClient { private var userAgent: String? private var randomizeUserAgentPerRequest: Bool = false + /// Headers automatically applied to every request from this client. + /// Per-call `customHeaders` override these on key conflict. + /// Used to bake an HTTP Basic Auth `Authorization` header into a per-instance client + /// when the instance sits behind a reverse proxy that requires basic auth. + private var defaultHeaders: [String: String] = [:] + // MARK: - Initialization init(session: URLSession = .shared, decoder: JSONDecoder = .init()) { @@ -41,6 +47,12 @@ actor HTTPClient { self.randomizeUserAgentPerRequest = enabled } + /// Sets headers to be applied to every request from this client. + /// Per-call `customHeaders` override these on key conflict. + func setDefaultHeaders(_ headers: [String: String]) { + self.defaultHeaders = headers + } + // MARK: - Public Methods /// Fetches and decodes a response from the given endpoint. @@ -104,7 +116,13 @@ actor HTTPClient { mutableRequest.setValue(userAgent, forHTTPHeaderField: "User-Agent") } - // Apply custom headers (e.g., X-API-Key for authenticated requests) + // Apply default headers first (e.g., basic auth bound to a per-instance client) + for (key, value) in defaultHeaders { + mutableRequest.setValue(value, forHTTPHeaderField: key) + } + + // Apply custom headers (e.g., X-API-Key for authenticated requests). + // These override default headers on key conflict. if let customHeaders { for (key, value) in customHeaders { mutableRequest.setValue(value, forHTTPHeaderField: key) diff --git a/Yattee/Services/SubscriptionFeedCache.swift b/Yattee/Services/SubscriptionFeedCache.swift index 45237546..991b8df7 100644 --- a/Yattee/Services/SubscriptionFeedCache.swift +++ b/Yattee/Services/SubscriptionFeedCache.swift @@ -491,9 +491,11 @@ final class SubscriptionFeedCache { } do { - let yatteeServerAPI = YatteeServerAPI(httpClient: HTTPClient()) - let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: instance) - await yatteeServerAPI.setAuthHeader(authHeader) + let httpClient = HTTPClient() + if let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: instance) { + await httpClient.setDefaultHeaders(["Authorization": authHeader]) + } + let yatteeServerAPI = YatteeServerAPI(httpClient: httpClient) LoggingService.shared.debug("refreshFromStatelessServer: Calling postFeed for \(channelRequests.count) channels", category: .general) let response = try await yatteeServerAPI.postFeed( channels: channelRequests, @@ -555,9 +557,11 @@ final class SubscriptionFeedCache { let statusChannels = channels.map { StatelessChannelStatusRequest(channelId: $0.channelId, site: $0.site) } - let yatteeServerAPI = YatteeServerAPI(httpClient: HTTPClient()) - let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: instance) - await yatteeServerAPI.setAuthHeader(authHeader) + let httpClient = HTTPClient() + if let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: instance) { + await httpClient.setDefaultHeaders(["Authorization": authHeader]) + } + let yatteeServerAPI = YatteeServerAPI(httpClient: httpClient) let maxRetries = 5 var retryCount = 0 diff --git a/Yattee/Views/Subscriptions/ManageChannelsView.swift b/Yattee/Views/Subscriptions/ManageChannelsView.swift index bfae920a..617322f3 100644 --- a/Yattee/Views/Subscriptions/ManageChannelsView.swift +++ b/Yattee/Views/Subscriptions/ManageChannelsView.swift @@ -438,9 +438,11 @@ struct ManageChannelsView: View { guard !channelIDs.isEmpty else { return } do { - let api = YatteeServerAPI(httpClient: HTTPClient()) - let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: yatteeServer) - await api.setAuthHeader(authHeader) + let httpClient = HTTPClient() + if let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: yatteeServer) { + await httpClient.setDefaultHeaders(["Authorization": authHeader]) + } + let api = YatteeServerAPI(httpClient: httpClient) let response = try await api.channelsMetadata(channelIDs: channelIDs, instance: yatteeServer) // Update subscriptions in SwiftData