Skip to content

Commit aa06736

Browse files
Added methods to check the size of a message before sending it
1 parent caef21e commit aa06736

File tree

4 files changed

+199
-5
lines changed

4 files changed

+199
-5
lines changed

Sources/WebPush/Push Message/Notification.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,17 @@ extension PushMessage.Notification: Encodable {
278278
try messageContainer.encodeIfPresent(appBadgeCount, forKey: .appBadgeCount)
279279
if isMutable { try messageContainer.encode(isMutable, forKey: .isMutable) }
280280
}
281+
282+
/// Check to see if a notification is potentially too large to be sent to a push service.
283+
///
284+
/// - Note: _Some_ push services may still accept larger messages, so you can only truly know if a message is _too_ large by attempting to send it and checking for a ``MessageTooLargeError`` error. However, if a message passes this check, it is guaranteed to not fail for this reason, assuming the push service implements the minimum requirements of the spec, which you can assume for all major browsers. For these reasons, unless you are sending the same message to multiple subscribers, it's often faster to just try sending the message rather than checking before sending.
285+
///
286+
/// - Throws: ``MessageTooLargeError`` if the message is too large. Throws another error if encoding fails.
287+
@inlinable
288+
public func checkMessageSize() throws {
289+
guard try WebPushManager.messageEncoder.encode(self).count <= WebPushManager.maximumMessageSize
290+
else { throw MessageTooLargeError() }
291+
}
281292
}
282293

283294
extension PushMessage.Notification: Decodable where Contents: Decodable {

Sources/WebPush/WebPushManager.swift

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ public actor WebPushManager: Sendable {
4343
/// This is currently set to 3,993 plaintext bytes. See the discussion for ``maximumEncryptedPayloadSize`` for more information.
4444
public static let maximumMessageSize = maximumEncryptedPayloadSize - 103
4545

46+
/// The encoder used when serializing JSON messages.
47+
public static let messageEncoder: JSONEncoder = {
48+
let encoder = JSONEncoder()
49+
encoder.dateEncodingStrategy = .millisecondsSince1970
50+
encoder.outputFormatting = [.withoutEscapingSlashes]
51+
52+
return encoder
53+
}()
54+
4655
/// The internal logger to use when reporting misconfiguration and background activity.
4756
nonisolated let backgroundActivityLogger: Logger
4857

@@ -366,6 +375,19 @@ public actor WebPushManager: Sendable {
366375
)
367376
}
368377

378+
/// Check to see if a message is potentially too large to be sent to a push service.
379+
///
380+
/// - Note: _Some_ push services may still accept larger messages, so you can only truly know if a message is _too_ large by attempting to send it and checking for a ``MessageTooLargeError`` error. However, if a message passes this check, it is guaranteed to not fail for this reason, assuming the push service implements the minimum requirements of the spec, which you can assume for all major browsers.
381+
///
382+
/// - Parameters:
383+
/// - message: The message to send as raw data.
384+
/// - Throws: ``MessageTooLargeError`` if the message is too large.
385+
@inlinable
386+
public nonisolated func checkMessageSize(data message: some DataProtocol) throws(MessageTooLargeError) {
387+
guard message.count <= Self.maximumMessageSize
388+
else { throw MessageTooLargeError() }
389+
}
390+
369391
/// Send a push message as a string.
370392
///
371393
/// The service worker you registered is expected to know how to decode the string you send.
@@ -428,6 +450,19 @@ public actor WebPushManager: Sendable {
428450
)
429451
}
430452

453+
/// Check to see if a message is potentially too large to be sent to a push service.
454+
///
455+
/// - Note: _Some_ push services may still accept larger messages, so you can only truly know if a message is _too_ large by attempting to send it and checking for a ``MessageTooLargeError`` error. However, if a message passes this check, it is guaranteed to not fail for this reason, assuming the push service implements the minimum requirements of the spec, which you can assume for all major browsers. For these reasons, unless you are sending the same message to multiple subscribers, it's often faster to just try sending the message rather than checking before sending.
456+
///
457+
/// - Parameters:
458+
/// - message: The message to send as a string.
459+
/// - Throws: ``MessageTooLargeError`` if the message is too large.
460+
@inlinable
461+
public nonisolated func checkMessageSize(string message: some StringProtocol) throws(MessageTooLargeError) {
462+
guard message.utf8.count <= Self.maximumMessageSize
463+
else { throw MessageTooLargeError() }
464+
}
465+
431466
/// Send a push message as encoded JSON.
432467
///
433468
/// The service worker you registered is expected to know how to decode the JSON you send. Note that dates are encoded using ``/Foundation/JSONEncoder/DateEncodingStrategy/millisecondsSince1970``, and data is encoded using ``/Foundation/JSONEncoder/DataEncodingStrategy/base64``.
@@ -490,6 +525,18 @@ public actor WebPushManager: Sendable {
490525
)
491526
}
492527

528+
/// Check to see if a message is potentially too large to be sent to a push service.
529+
///
530+
/// - Note: _Some_ push services may still accept larger messages, so you can only truly know if a message is _too_ large by attempting to send it and checking for a ``MessageTooLargeError`` error. However, if a message passes this check, it is guaranteed to not fail for this reason, assuming the push service implements the minimum requirements of the spec, which you can assume for all major browsers. For these reasons, unless you are sending the same message to multiple subscribers, it's often faster to just try sending the message rather than checking before sending.
531+
///
532+
/// - Parameters:
533+
/// - message: The message to send as JSON.
534+
/// - Throws: ``MessageTooLargeError`` if the message is too large. Throws another error if encoding fails.
535+
@inlinable
536+
public nonisolated func checkMessageSize(json message: some Encodable&Sendable) throws {
537+
try _Message.json(message).checkMessageSize()
538+
}
539+
493540
/// Send a push notification.
494541
///
495542
/// If you provide ``PushMessage/Notification/data``, the service worker you registered is expected to know how to decode it. Note that dates are encoded using ``/Foundation/JSONEncoder/DateEncodingStrategy/millisecondsSince1970``, and data is encoded using ``/Foundation/JSONEncoder/DataEncodingStrategy/base64``.
@@ -549,6 +596,18 @@ public actor WebPushManager: Sendable {
549596
)
550597
}
551598

599+
/// Check to see if a message is potentially too large to be sent to a push service.
600+
///
601+
/// - Note: _Some_ push services may still accept larger messages, so you can only truly know if a message is _too_ large by attempting to send it and checking for a ``MessageTooLargeError`` error. However, if a message passes this check, it is guaranteed to not fail for this reason, assuming the push service implements the minimum requirements of the spec, which you can assume for all major browsers. For these reasons, unless you are sending the same message to multiple subscribers, it's often faster to just try sending the message rather than checking before sending.
602+
///
603+
/// - Parameters:
604+
/// - notification: The ``PushMessage/Notification`` push notification.
605+
/// - Throws: ``MessageTooLargeError`` if the message is too large. Throws another error if encoding fails.
606+
@inlinable
607+
public nonisolated func checkMessageSize<Contents>(notification: PushMessage.Notification<Contents>) throws {
608+
try notification.checkMessageSize()
609+
}
610+
552611
/// Route a message to the current executor.
553612
/// - Parameters:
554613
/// - message: The message to send.
@@ -1067,6 +1126,7 @@ extension WebPushManager {
10671126
case json(any Encodable&Sendable)
10681127

10691128
/// The message, encoded as data.
1129+
@usableFromInline
10701130
var data: Data {
10711131
get throws {
10721132
switch self {
@@ -1076,28 +1136,28 @@ extension WebPushManager {
10761136
var string = string
10771137
return string.withUTF8 { Data($0) }
10781138
case .json(let json):
1079-
let encoder = JSONEncoder()
1080-
encoder.dateEncodingStrategy = .millisecondsSince1970
1081-
encoder.outputFormatting = [.withoutEscapingSlashes]
1082-
return try encoder.encode(json)
1139+
return try WebPushManager.messageEncoder.encode(json)
10831140
}
10841141
}
10851142
}
10861143

10871144
/// The string value from a ``string(_:)`` message.
1145+
@inlinable
10881146
public var string: String? {
10891147
guard case let .string(string) = self
10901148
else { return nil }
10911149
return string
10921150
}
10931151

10941152
/// The json value from a ``json(_:)`` message.
1153+
@inlinable
10951154
public func json<JSON: Encodable&Sendable>(as: JSON.Type = JSON.self) -> JSON? {
10961155
guard case let .json(json) = self
10971156
else { return nil }
10981157
return json as? JSON
10991158
}
11001159

1160+
@inlinable
11011161
public var description: String {
11021162
switch self {
11031163
case .data(let data):
@@ -1108,6 +1168,26 @@ extension WebPushManager {
11081168
return ".json(\(json))"
11091169
}
11101170
}
1171+
1172+
/// Check to see if a message is potentially too large to be sent to a push service.
1173+
///
1174+
/// - Note: _Some_ push services may still accept larger messages, so you can only truly know if a message is _too_ large by attempting to send it and checking for a ``MessageTooLargeError`` error. However, if a message passes this check, it is guaranteed to not fail for this reason, assuming the push service implements the minimum requirements of the spec, which you can assume for all major browsers. For these reasons, unless you are sending the same message to multiple subscribers, it's often faster to just try sending the message rather than checking before sending.
1175+
///
1176+
/// - Throws: ``MessageTooLargeError`` if the message is too large. Throws another error if encoding fails.
1177+
@inlinable
1178+
public func checkMessageSize() throws {
1179+
switch self {
1180+
case .data(let data):
1181+
guard data.count <= WebPushManager.maximumMessageSize
1182+
else { throw MessageTooLargeError() }
1183+
case .string(let string):
1184+
guard string.utf8.count <= WebPushManager.maximumMessageSize
1185+
else { throw MessageTooLargeError() }
1186+
case .json(let json):
1187+
guard try WebPushManager.messageEncoder.encode(json).count <= WebPushManager.maximumMessageSize
1188+
else { throw MessageTooLargeError() }
1189+
}
1190+
}
11111191
}
11121192

11131193
/// An internal type representing the executor for a push message.

Sources/WebPushTesting/WebPushManager+Testing.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ extension WebPushManager {
3838
vapidConfiguration: VAPID.Configuration = .mockedConfiguration,
3939
// TODO: Add networkConfiguration for proxy, number of simultaneous pushes, etc…
4040
backgroundActivityLogger: Logger? = .defaultWebPushPrintLogger,
41-
messageHandler: @escaping MessageHandler
41+
messageHandler: @escaping MessageHandler = { _, _, _, _, _ in }
4242
) -> WebPushManager {
4343
let backgroundActivityLogger = backgroundActivityLogger ?? .defaultWebPushNoOpLogger
4444

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//
2+
// MessageSizeTests.swift
3+
// swift-webpush
4+
//
5+
// Created by Dimitri Bouniol on 2025-03-02.
6+
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
#if canImport(FoundationEssentials)
10+
import FoundationEssentials
11+
#else
12+
import Foundation
13+
#endif
14+
import Testing
15+
@testable import WebPush
16+
@testable import WebPushTesting
17+
18+
@Suite("Message Size Tetss")
19+
struct MessageSizeTests {
20+
@Test func dataMessages() async throws {
21+
let webPushManager = WebPushManager.makeMockedManager()
22+
try webPushManager.checkMessageSize(data: Data(repeating: 0, count: 42))
23+
try webPushManager.checkMessageSize(data: Data(repeating: 0, count: 3993))
24+
#expect(throws: MessageTooLargeError()) {
25+
try webPushManager.checkMessageSize(data: Data(repeating: 0, count: 3994))
26+
}
27+
28+
try WebPushManager._Message.data(Data(repeating: 0, count: 42)).checkMessageSize()
29+
try WebPushManager._Message.data(Data(repeating: 0, count: 3993)).checkMessageSize()
30+
#expect(throws: MessageTooLargeError()) {
31+
try WebPushManager._Message.data(Data(repeating: 0, count: 3994)).checkMessageSize()
32+
}
33+
}
34+
35+
@Test func stringMessages() async throws {
36+
let webPushManager = WebPushManager.makeMockedManager()
37+
try webPushManager.checkMessageSize(string: String(repeating: "A", count: 42))
38+
try webPushManager.checkMessageSize(string: String(repeating: "A", count: 3993))
39+
#expect(throws: MessageTooLargeError()) {
40+
try webPushManager.checkMessageSize(string: String(repeating: "A", count: 3994))
41+
}
42+
43+
try WebPushManager._Message.string(String(repeating: "A", count: 42)).checkMessageSize()
44+
try WebPushManager._Message.string(String(repeating: "A", count: 3993)).checkMessageSize()
45+
#expect(throws: MessageTooLargeError()) {
46+
try WebPushManager._Message.string(String(repeating: "A", count: 3994)).checkMessageSize()
47+
}
48+
}
49+
50+
@Test func jsonMessages() async throws {
51+
let webPushManager = WebPushManager.makeMockedManager()
52+
try webPushManager.checkMessageSize(json: ["key" : String(repeating: "A", count: 42)])
53+
try webPushManager.checkMessageSize(json: ["key" : String(repeating: "A", count: 3983)])
54+
#expect(throws: MessageTooLargeError()) {
55+
try webPushManager.checkMessageSize(json: ["key" : String(repeating: "A", count: 3984)])
56+
}
57+
58+
try WebPushManager._Message.json(["key" : String(repeating: "A", count: 42)]).checkMessageSize()
59+
try WebPushManager._Message.json(["key" : String(repeating: "A", count: 3983)]).checkMessageSize()
60+
#expect(throws: MessageTooLargeError()) {
61+
try WebPushManager._Message.json(["key" : String(repeating: "A", count: 3984)]).checkMessageSize()
62+
}
63+
}
64+
65+
@Test func notificationMessages() async throws {
66+
let webPushManager = WebPushManager.makeMockedManager()
67+
try webPushManager.checkMessageSize(notification: PushMessage.Notification(
68+
destination: URL(string: "https://example.com")!,
69+
title: String(repeating: "A", count: 42),
70+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
71+
))
72+
try webPushManager.checkMessageSize(notification: PushMessage.Notification(
73+
destination: URL(string: "https://example.com")!,
74+
title: String(repeating: "A", count: 3889),
75+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
76+
))
77+
#expect(throws: MessageTooLargeError()) {
78+
try webPushManager.checkMessageSize(notification: PushMessage.Notification(
79+
destination: URL(string: "https://example.com")!,
80+
title: String(repeating: "A", count: 3890),
81+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
82+
))
83+
}
84+
85+
try PushMessage.Notification(
86+
destination: URL(string: "https://example.com")!,
87+
title: String(repeating: "A", count: 42),
88+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
89+
).checkMessageSize()
90+
try PushMessage.Notification(
91+
destination: URL(string: "https://example.com")!,
92+
title: String(repeating: "A", count: 3889),
93+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
94+
).checkMessageSize()
95+
#expect(throws: MessageTooLargeError()) {
96+
try PushMessage.Notification(
97+
destination: URL(string: "https://example.com")!,
98+
title: String(repeating: "A", count: 3890),
99+
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
100+
).checkMessageSize()
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)