From 57fb3a4e95d33276ce5c4c1c8050fe6b4bf5d0a1 Mon Sep 17 00:00:00 2001 From: Matt Glover Date: Fri, 16 Jan 2026 01:39:22 +0000 Subject: [PATCH 1/4] Changed delete operation to supply `apns-channel-id` as header value Updated delete operation path to `/channels` --- Sources/APNS/APNSBroadcastClient.swift | 6 ++++++ .../APNSCore/Broadcast/APNSBroadcastRequest.swift | 13 +++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Sources/APNS/APNSBroadcastClient.swift b/Sources/APNS/APNSBroadcastClient.swift index 8cab985..e17145e 100644 --- a/Sources/APNS/APNSBroadcastClient.swift +++ b/Sources/APNS/APNSBroadcastClient.swift @@ -145,6 +145,12 @@ extension APNSBroadcastClient { headers.add(name: "authorization", value: token) } + if let operationHeaders = request.operation.headers { + for (name, value) in operationHeaders { + headers.add(name: name, value: value) + } + } + // Build the request URL let requestURL = "\(self.environment.url):\(self.environment.port)/1/apps/\(self.bundleID)\(request.operation.path)" diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift b/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift index 4f48bfa..dee3d6b 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift @@ -42,12 +42,21 @@ public struct APNSBroadcastRequest: Sendable where Message: /// The path for this operation. public var path: String { switch self { - case .create, .listAll: + case .create, .delete, .listAll: return "/channels" - case .read(let channelID), .delete(let channelID): + case .read(let channelID): return "/channels/\(channelID)" } } + + public var headers: [String: String]? { + switch self { + case .delete(let channelID): + return ["apns-channel-id": channelID] + default: + return nil + } + } } /// The operation to perform. From 12f3c76ae1a65cbbe2358c1773bde0bbcffd7c9e Mon Sep 17 00:00:00 2001 From: Matt Glover Date: Fri, 16 Jan 2026 02:04:37 +0000 Subject: [PATCH 2/4] Read channel operation now appeands apns-channel-id as HTTP header value --- Sources/APNS/APNSBroadcastClient.swift | 1 + Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/APNS/APNSBroadcastClient.swift b/Sources/APNS/APNSBroadcastClient.swift index e17145e..4369b49 100644 --- a/Sources/APNS/APNSBroadcastClient.swift +++ b/Sources/APNS/APNSBroadcastClient.swift @@ -145,6 +145,7 @@ extension APNSBroadcastClient { headers.add(name: "authorization", value: token) } + // Append operation specific HTTPS headers if let operationHeaders = request.operation.headers { for (name, value) in operationHeaders { headers.add(name: name, value: value) diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift b/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift index dee3d6b..827f0f4 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift @@ -42,16 +42,14 @@ public struct APNSBroadcastRequest: Sendable where Message: /// The path for this operation. public var path: String { switch self { - case .create, .delete, .listAll: + case .create, .delete, .read, .listAll: return "/channels" - case .read(let channelID): - return "/channels/\(channelID)" } } public var headers: [String: String]? { switch self { - case .delete(let channelID): + case .delete(let channelID), .read(channelID: let channelID): return ["apns-channel-id": channelID] default: return nil From a491cfc5d9c85b829af702aad0ee2a065301aa14 Mon Sep 17 00:00:00 2001 From: Matt Glover Date: Fri, 16 Jan 2026 02:04:37 +0000 Subject: [PATCH 3/4] Read channel operation now appeands apns-channel-id as HTTP header value --- Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift b/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift index 827f0f4..8e95584 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift @@ -47,6 +47,7 @@ public struct APNSBroadcastRequest: Sendable where Message: } } + /// HTTP Headers for this operation. public var headers: [String: String]? { switch self { case .delete(let channelID), .read(channelID: let channelID): From d4a30d9bf1bc6332d6229fb38b1c1286b9acbeea Mon Sep 17 00:00:00 2001 From: Matt Glover Date: Fri, 16 Jan 2026 13:42:35 +0000 Subject: [PATCH 4/4] Operations: create, delete, read and listAll now return valid responses Error handling not touched (may require additional work) --- Sources/APNS/APNSBroadcastClient.swift | 11 +++++++---- Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift | 8 +------- .../Broadcast/APNSBroadcastClientProtocol.swift | 2 +- .../APNSCore/Broadcast/APNSBroadcastResponse.swift | 8 ++++++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Sources/APNS/APNSBroadcastClient.swift b/Sources/APNS/APNSBroadcastClient.swift index 4369b49..b71f0ef 100644 --- a/Sources/APNS/APNSBroadcastClient.swift +++ b/Sources/APNS/APNSBroadcastClient.swift @@ -172,12 +172,15 @@ extension APNSBroadcastClient { // Extract request ID from response let apnsRequestID = response.headers.first(name: "apns-request-id").flatMap { UUID(uuidString: $0) } - + + // Extract channel ID from response, or from request headers (as 'read' operation doesn't return in payload + let channelID = response.headers.first(name: "apns-channel-id") ?? request.operation.headers?["apns-channel-id"] + // Handle successful responses - if response.status == .ok || response.status == .created { + if response.status == .ok || response.status == .created || response.status == .noContent { let body = try await response.body.collect(upTo: 1024 * 1024) // 1MB max - let responseBody = try responseDecoder.decode(ResponseBody.self, from: body) - return APNSBroadcastResponse(apnsRequestID: apnsRequestID, body: responseBody) + let responseBody = try? responseDecoder.decode(ResponseBody.self, from: body) + return APNSBroadcastResponse(apnsRequestID: apnsRequestID, channelID: channelID, body: responseBody) } // Handle error responses diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift b/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift index 092dd86..67686a0 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift @@ -15,14 +15,10 @@ /// Represents a broadcast channel configuration. public struct APNSBroadcastChannel: Codable, Sendable { enum CodingKeys: String, CodingKey { - case channelID = "channel-id" case messageStoragePolicy = "message-storage-policy" case pushType = "push-type" } - /// The unique identifier for the broadcast channel (only present in responses). - public let channelID: String? - /// The message storage policy for this channel. public let messageStoragePolicy: APNSBroadcastMessageStoragePolicy @@ -34,14 +30,12 @@ public struct APNSBroadcastChannel: Codable, Sendable { /// /// - Parameter messageStoragePolicy: The storage policy for messages in this channel. public init(messageStoragePolicy: APNSBroadcastMessageStoragePolicy) { - self.channelID = nil self.messageStoragePolicy = messageStoragePolicy self.pushType = "LiveActivity" } /// Internal initializer used for decoding responses that include channel ID. - public init(channelID: String?, messageStoragePolicy: APNSBroadcastMessageStoragePolicy, pushType: String = "LiveActivity") { - self.channelID = channelID + public init(messageStoragePolicy: APNSBroadcastMessageStoragePolicy, pushType: String = "LiveActivity") { self.messageStoragePolicy = messageStoragePolicy self.pushType = pushType } diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastClientProtocol.swift b/Sources/APNSCore/Broadcast/APNSBroadcastClientProtocol.swift index 28c5458..ae91701 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastClientProtocol.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastClientProtocol.swift @@ -35,7 +35,7 @@ extension APNSBroadcastClientProtocol { public func create( channel: APNSBroadcastChannel, apnsRequestID: UUID? = nil - ) async throws -> APNSBroadcastResponse { + ) async throws -> APNSBroadcastResponse { let request = APNSBroadcastRequest( operation: .create, message: channel, diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastResponse.swift b/Sources/APNSCore/Broadcast/APNSBroadcastResponse.swift index 9e2d3fb..dcf8847 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastResponse.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastResponse.swift @@ -19,11 +19,15 @@ public struct APNSBroadcastResponse: Sendable where Body: Senda /// The request ID returned by APNs. public let apnsRequestID: UUID? + /// The channel ID returned by APNs. + public let channelID: String? + /// The response body. - public let body: Body + public let body: Body? - public init(apnsRequestID: UUID?, body: Body) { + public init(apnsRequestID: UUID?, channelID: String?, body: Body?) { self.apnsRequestID = apnsRequestID + self.channelID = channelID self.body = body } }