mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
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.
This commit is contained in:
@@ -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 {
|
||||
switch instance.type {
|
||||
case .invidious:
|
||||
return await invidiousAPI(for: instance)
|
||||
case .piped:
|
||||
return await pipedAPI(for: instance)
|
||||
case .peertube:
|
||||
return await peerTubeAPI(for: instance)
|
||||
case .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)
|
||||
case .piped:
|
||||
return PipedAPI(httpClient: insecureClient)
|
||||
case .peertube:
|
||||
return PeerTubeAPI(httpClient: insecureClient)
|
||||
case .yatteeServer:
|
||||
fatalError("Should be handled above")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a YatteeServerAPI configured for the instance's SSL and auth requirements.
|
||||
private func yatteeServerAPI(for instance: Instance) async -> YatteeServerAPI {
|
||||
let api: YatteeServerAPI
|
||||
if let authClient = await httpClientWithBasicAuth(for: instance) {
|
||||
return YatteeServerAPI(httpClient: authClient)
|
||||
}
|
||||
if !instance.allowInvalidCertificates {
|
||||
api = defaultYatteeServerAPI
|
||||
} else {
|
||||
return defaultYatteeServerAPI
|
||||
}
|
||||
let insecureClient = httpClientFactory.createClient(for: instance)
|
||||
api = YatteeServerAPI(httpClient: insecureClient)
|
||||
return YatteeServerAPI(httpClient: insecureClient)
|
||||
}
|
||||
|
||||
// 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
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Returns an InvidiousAPI configured for the instance's SSL requirements.
|
||||
private func invidiousAPI(for instance: Instance) -> InvidiousAPI {
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user