Skip to content

Commit 492ea0a

Browse files
Added Bad Subscriber and HTTP Error types
1 parent bce5cb5 commit 492ea0a

File tree

4 files changed

+64
-4
lines changed

4 files changed

+64
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// BadSubscriberError.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2024-12-13.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// The subscription is no longer valid and should be removed and re-registered.
12+
///
13+
/// - Warning: Do not continue to send notifications to invalid subscriptions or you'll risk being rate limited by push services.
14+
public struct BadSubscriberError: LocalizedError, Hashable {
15+
public init() {}
16+
17+
public var errorDescription: String? {
18+
"The subscription is no longer valid."
19+
}
20+
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// HTTPError.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2024-12-13.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import AsyncHTTPClient
10+
import Foundation
11+
12+
/// An unknown HTTP error was encountered.
13+
///
14+
/// - SeeAlso: [RFC8030 Generic Event Delivery Using HTTP Push](https://datatracker.ietf.org/doc/html/rfc8030)
15+
/// - SeeAlso: [RFC8292 Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292)
16+
/// - SeeAlso: [Sending web push notifications in web apps and browsers — Review responses for push notification errors](https://developer.apple.com/documentation/usernotifications/sending-web-push-notifications-in-web-apps-and-browsers#Review-responses-for-push-notification-errors)
17+
public struct HTTPError: LocalizedError {
18+
let response: HTTPClientResponse
19+
20+
init(response: HTTPClientResponse) {
21+
self.response = response
22+
}
23+
24+
public var errorDescription: String? {
25+
"A \(response.status) HTTP error was encountered: \(response)."
26+
}
27+
}

Sources/WebPush/Errors/VAPIDConfigurationError.swift

+6
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,23 @@ extension VAPID {
1313
public struct ConfigurationError: LocalizedError, Hashable {
1414
enum Kind {
1515
case keysNotProvided
16+
case matchingKeyNotFound
1617
}
1718

1819
var kind: Kind
1920

2021
/// VAPID keys not found during initialization.
2122
public static let keysNotProvided = Self(kind: .keysNotProvided)
2223

24+
/// A VAPID key for the subscriber was not found.
25+
public static let matchingKeyNotFound = Self(kind: .matchingKeyNotFound)
26+
2327
public var errorDescription: String? {
2428
switch kind {
2529
case .keysNotProvided:
2630
"VAPID keys not found during initialization."
31+
case .matchingKeyNotFound:
32+
"A VAPID key for the subscriber was not found."
2733
}
2834
}
2935
}

Sources/WebPush/WebPushManager.swift

+11-4
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,12 @@ public actor WebPushManager: Sendable {
296296
urgency: Urgency
297297
) async throws {
298298
guard let signingKey = vapidKeyLookup[subscriber.vapidKeyID]
299-
else { throw CancellationError() } // throw key not found error
299+
else {
300+
logger.warning("A key was not found for this subscriber.", metadata: [
301+
"vapidKeyID": "\(subscriber.vapidKeyID)"
302+
])
303+
throw VAPID.ConfigurationError.matchingKeyNotFound
304+
}
300305

301306
/// Prepare authorization, private keys, and payload ahead of time to bail early if they can't be created.
302307
let authorization = try loadCurrentVAPIDAuthorizationHeader(endpoint: subscriber.endpoint, signingKey: signingKey)
@@ -305,7 +310,7 @@ public actor WebPushManager: Sendable {
305310
/// Perform key exchange between the user agent's public key and our private key, deriving a shared secret.
306311
let userAgent = subscriber.userAgentKeyMaterial
307312
guard let sharedSecret = try? applicationServerECDHPrivateKey.sharedSecretFromKeyAgreement(with: userAgent.publicKey)
308-
else { throw CancellationError() } // throw bad subscription
313+
else { throw BadSubscriberError() }
309314

310315
/// Generate a 16-byte salt.
311316
var salt: [UInt8] = Array(repeating: 0, count: 16)
@@ -376,8 +381,10 @@ public actor WebPushManager: Sendable {
376381
/// Check the response and determine if the subscription should be removed from our records, or if the notification should just be skipped.
377382
switch response.status {
378383
case .created: break
379-
case .notFound, .gone: throw CancellationError() // throw bad subscription
380-
default: throw CancellationError() //Abort(response.status, headers: response.headers, reason: response.description)
384+
case .notFound, .gone: throw BadSubscriberError()
385+
// TODO: 413 payload too large - warn and throw error
386+
// TODO: 429 too many requests, 500 internal server error, 503 server shutting down - check config and perform a retry after a delay?
387+
default: throw HTTPError(response: response)
381388
}
382389
logger.trace("Sent \(message) notification to \(subscriber): \(response)")
383390
}

0 commit comments

Comments
 (0)