Skip to content

Check Size #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Sources/WebPush/Push Message/Notification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,17 @@ extension PushMessage.Notification: Encodable {
try messageContainer.encodeIfPresent(appBadgeCount, forKey: .appBadgeCount)
if isMutable { try messageContainer.encode(isMutable, forKey: .isMutable) }
}

/// Check to see if a notification is potentially too large to be sent to a push service.
///
/// - 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.
///
/// - Throws: ``MessageTooLargeError`` if the message is too large. Throws another error if encoding fails.
@inlinable
public func checkMessageSize() throws {
guard try WebPushManager.messageEncoder.encode(self).count <= WebPushManager.maximumMessageSize
else { throw MessageTooLargeError() }
}
}

extension PushMessage.Notification: Decodable where Contents: Decodable {
Expand Down
88 changes: 84 additions & 4 deletions Sources/WebPush/WebPushManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ public actor WebPushManager: Sendable {
/// This is currently set to 3,993 plaintext bytes. See the discussion for ``maximumEncryptedPayloadSize`` for more information.
public static let maximumMessageSize = maximumEncryptedPayloadSize - 103

/// The encoder used when serializing JSON messages.
public static let messageEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .millisecondsSince1970
encoder.outputFormatting = [.withoutEscapingSlashes]

return encoder
}()

/// The internal logger to use when reporting misconfiguration and background activity.
nonisolated let backgroundActivityLogger: Logger

Expand Down Expand Up @@ -366,6 +375,19 @@ public actor WebPushManager: Sendable {
)
}

/// Check to see if a message is potentially too large to be sent to a push service.
///
/// - 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.
///
/// - Parameters:
/// - message: The message to send as raw data.
/// - Throws: ``MessageTooLargeError`` if the message is too large.
@inlinable
public nonisolated func checkMessageSize(data message: some DataProtocol) throws(MessageTooLargeError) {
guard message.count <= Self.maximumMessageSize
else { throw MessageTooLargeError() }
}

/// Send a push message as a string.
///
/// The service worker you registered is expected to know how to decode the string you send.
Expand Down Expand Up @@ -428,6 +450,19 @@ public actor WebPushManager: Sendable {
)
}

/// Check to see if a message is potentially too large to be sent to a push service.
///
/// - 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.
///
/// - Parameters:
/// - message: The message to send as a string.
/// - Throws: ``MessageTooLargeError`` if the message is too large.
@inlinable
public nonisolated func checkMessageSize(string message: some StringProtocol) throws(MessageTooLargeError) {
guard message.utf8.count <= Self.maximumMessageSize
else { throw MessageTooLargeError() }
}

/// Send a push message as encoded JSON.
///
/// 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``.
Expand Down Expand Up @@ -490,6 +525,18 @@ public actor WebPushManager: Sendable {
)
}

/// Check to see if a message is potentially too large to be sent to a push service.
///
/// - 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.
///
/// - Parameters:
/// - message: The message to send as JSON.
/// - Throws: ``MessageTooLargeError`` if the message is too large. Throws another error if encoding fails.
@inlinable
public nonisolated func checkMessageSize(json message: some Encodable&Sendable) throws {
try _Message.json(message).checkMessageSize()
}

/// Send a push notification.
///
/// 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``.
Expand Down Expand Up @@ -549,6 +596,18 @@ public actor WebPushManager: Sendable {
)
}

/// Check to see if a message is potentially too large to be sent to a push service.
///
/// - 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.
///
/// - Parameters:
/// - notification: The ``PushMessage/Notification`` push notification.
/// - Throws: ``MessageTooLargeError`` if the message is too large. Throws another error if encoding fails.
@inlinable
public nonisolated func checkMessageSize<Contents>(notification: PushMessage.Notification<Contents>) throws {
try notification.checkMessageSize()
}

/// Route a message to the current executor.
/// - Parameters:
/// - message: The message to send.
Expand Down Expand Up @@ -1067,6 +1126,7 @@ extension WebPushManager {
case json(any Encodable&Sendable)

/// The message, encoded as data.
@usableFromInline
var data: Data {
get throws {
switch self {
Expand All @@ -1076,28 +1136,28 @@ extension WebPushManager {
var string = string
return string.withUTF8 { Data($0) }
case .json(let json):
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .millisecondsSince1970
encoder.outputFormatting = [.withoutEscapingSlashes]
return try encoder.encode(json)
return try WebPushManager.messageEncoder.encode(json)
}
}
}

/// The string value from a ``string(_:)`` message.
@inlinable
public var string: String? {
guard case let .string(string) = self
else { return nil }
return string
}

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

@inlinable
public var description: String {
switch self {
case .data(let data):
Expand All @@ -1108,6 +1168,26 @@ extension WebPushManager {
return ".json(\(json))"
}
}

/// Check to see if a message is potentially too large to be sent to a push service.
///
/// - 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.
///
/// - Throws: ``MessageTooLargeError`` if the message is too large. Throws another error if encoding fails.
@inlinable
public func checkMessageSize() throws {
switch self {
case .data(let data):
guard data.count <= WebPushManager.maximumMessageSize
else { throw MessageTooLargeError() }
case .string(let string):
guard string.utf8.count <= WebPushManager.maximumMessageSize
else { throw MessageTooLargeError() }
case .json(let json):
guard try WebPushManager.messageEncoder.encode(json).count <= WebPushManager.maximumMessageSize
else { throw MessageTooLargeError() }
}
}
}

/// An internal type representing the executor for a push message.
Expand Down
2 changes: 1 addition & 1 deletion Sources/WebPushTesting/WebPushManager+Testing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ extension WebPushManager {
vapidConfiguration: VAPID.Configuration = .mockedConfiguration,
// TODO: Add networkConfiguration for proxy, number of simultaneous pushes, etc…
backgroundActivityLogger: Logger? = .defaultWebPushPrintLogger,
messageHandler: @escaping MessageHandler
messageHandler: @escaping MessageHandler = { _, _, _, _, _ in }
) -> WebPushManager {
let backgroundActivityLogger = backgroundActivityLogger ?? .defaultWebPushNoOpLogger

Expand Down
103 changes: 103 additions & 0 deletions Tests/WebPushTests/MessageSizeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// MessageSizeTests.swift
// swift-webpush
//
// Created by Dimitri Bouniol on 2025-03-02.
// Copyright © 2024 Mochi Development, Inc. All rights reserved.
//

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
import Testing
@testable import WebPush
@testable import WebPushTesting

@Suite("Message Size Tetss")
struct MessageSizeTests {
@Test func dataMessages() async throws {
let webPushManager = WebPushManager.makeMockedManager()
try webPushManager.checkMessageSize(data: Data(repeating: 0, count: 42))
try webPushManager.checkMessageSize(data: Data(repeating: 0, count: 3993))
#expect(throws: MessageTooLargeError()) {
try webPushManager.checkMessageSize(data: Data(repeating: 0, count: 3994))
}

try WebPushManager._Message.data(Data(repeating: 0, count: 42)).checkMessageSize()
try WebPushManager._Message.data(Data(repeating: 0, count: 3993)).checkMessageSize()
#expect(throws: MessageTooLargeError()) {
try WebPushManager._Message.data(Data(repeating: 0, count: 3994)).checkMessageSize()
}
}

@Test func stringMessages() async throws {
let webPushManager = WebPushManager.makeMockedManager()
try webPushManager.checkMessageSize(string: String(repeating: "A", count: 42))
try webPushManager.checkMessageSize(string: String(repeating: "A", count: 3993))
#expect(throws: MessageTooLargeError()) {
try webPushManager.checkMessageSize(string: String(repeating: "A", count: 3994))
}

try WebPushManager._Message.string(String(repeating: "A", count: 42)).checkMessageSize()
try WebPushManager._Message.string(String(repeating: "A", count: 3993)).checkMessageSize()
#expect(throws: MessageTooLargeError()) {
try WebPushManager._Message.string(String(repeating: "A", count: 3994)).checkMessageSize()
}
}

@Test func jsonMessages() async throws {
let webPushManager = WebPushManager.makeMockedManager()
try webPushManager.checkMessageSize(json: ["key" : String(repeating: "A", count: 42)])
try webPushManager.checkMessageSize(json: ["key" : String(repeating: "A", count: 3983)])
#expect(throws: MessageTooLargeError()) {
try webPushManager.checkMessageSize(json: ["key" : String(repeating: "A", count: 3984)])
}

try WebPushManager._Message.json(["key" : String(repeating: "A", count: 42)]).checkMessageSize()
try WebPushManager._Message.json(["key" : String(repeating: "A", count: 3983)]).checkMessageSize()
#expect(throws: MessageTooLargeError()) {
try WebPushManager._Message.json(["key" : String(repeating: "A", count: 3984)]).checkMessageSize()
}
}

@Test func notificationMessages() async throws {
let webPushManager = WebPushManager.makeMockedManager()
try webPushManager.checkMessageSize(notification: PushMessage.Notification(
destination: URL(string: "https://example.com")!,
title: String(repeating: "A", count: 42),
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
))
try webPushManager.checkMessageSize(notification: PushMessage.Notification(
destination: URL(string: "https://example.com")!,
title: String(repeating: "A", count: 3889),
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
))
#expect(throws: MessageTooLargeError()) {
try webPushManager.checkMessageSize(notification: PushMessage.Notification(
destination: URL(string: "https://example.com")!,
title: String(repeating: "A", count: 3890),
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
))
}

try PushMessage.Notification(
destination: URL(string: "https://example.com")!,
title: String(repeating: "A", count: 42),
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
).checkMessageSize()
try PushMessage.Notification(
destination: URL(string: "https://example.com")!,
title: String(repeating: "A", count: 3889),
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
).checkMessageSize()
#expect(throws: MessageTooLargeError()) {
try PushMessage.Notification(
destination: URL(string: "https://example.com")!,
title: String(repeating: "A", count: 3890),
timestamp: Date(timeIntervalSince1970: 1_000_000_000)
).checkMessageSize()
}
}
}