From 032c55df14cd2e1b5f9b255e3d13d3a87d89f1be Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Mon, 14 Nov 2022 19:37:55 -0800 Subject: [PATCH] Implement Reason response as error (#3) --- src/apns/mod.rs | 16 +++-- src/apns/reason.rs | 155 +++++++++++++++++++++++++++++++++++++++++++ src/apns/response.rs | 139 -------------------------------------- src/lib.rs | 2 +- src/result.rs | 96 +-------------------------- 5 files changed, 169 insertions(+), 239 deletions(-) create mode 100644 src/apns/reason.rs delete mode 100644 src/apns/response.rs diff --git a/src/apns/mod.rs b/src/apns/mod.rs index 652079fe..a7e538fe 100644 --- a/src/apns/mod.rs +++ b/src/apns/mod.rs @@ -14,12 +14,12 @@ use self::header::{ ApnsPriority, ApnsPushType, APNS_COLLAPSE_ID, APNS_EXPIRATION, APNS_ID, APNS_PRIORITY, APNS_PUSH_TYPE, APNS_TOPIC, }; +use self::reason::Reason; use self::request::{Alert, ApnsPayload, InterruptionLevel, Sound}; -use self::response::ApnsResponse; pub mod header; +pub mod reason; pub mod request; -pub mod response; pub const DEVELOPMENT_SERVER: &str = "https://api.sandbox.push.apple.com"; pub const PRODUCTION_SERVER: &str = "https://api.push.apple.com"; @@ -97,14 +97,14 @@ pub struct ApnsClient { } impl ApnsClient { - pub async fn post(&self, request: ApnsRequest) -> Result + pub async fn post(&self, request: ApnsRequest) -> Result<()> where T: Serialize, { let url = self.base_url.join(&request.device_token)?; let (headers, request): (_, ApnsPayload) = request.try_into()?; - let req = self + let res = self .client .post(url) .headers(headers) @@ -112,8 +112,12 @@ impl ApnsClient { .send() .await?; - let response = req.json().await?; - Ok(response) + if res.status().is_success() { + Ok(()) + } else { + let reason: Reason = res.json::().await?; + Err(reason.into()) + } } } diff --git a/src/apns/reason.rs b/src/apns/reason.rs new file mode 100644 index 00000000..7d0d9db8 --- /dev/null +++ b/src/apns/reason.rs @@ -0,0 +1,155 @@ +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, skip_serializing_none, TimestampMilliSeconds}; +use time::OffsetDateTime; + +/// APNS error response reason JSON body. +#[serde_as] +#[skip_serializing_none] +#[derive(thiserror::Error, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(tag = "reason")] +pub enum Reason { + #[error("The collapse identifier exceeds the maximum allowed size.")] + BadCollapseId, + + #[error("The specified device token is invalid. Verify that the request contains a valid token and that the token matches the environment.")] + BadDeviceToken, + + #[error("The apns-expiration value is invalid.")] + BadExpirationDate, + + #[error("The apns-id value is invalid.")] + BadMessageId, + + #[error("The apns-priority value is invalid.")] + BadPriority, + + #[error("The apns-topic value is invalid.")] + BadTopic, + + #[error("The device token doesn’t match the specified topic.")] + DeviceTokenNotForTopic, + + #[error("One or more headers are repeated.")] + DuplicateHeaders, + + #[error("Idle timeout.")] + IdleTimeout, + + #[error("The apns-push-type value is invalid.")] + InvalidPushType, + + #[error("The device token isn’t specified in the request :path. Verify that the :path header contains the device token.")] + MissingDeviceToken, + + #[error("The apns-topic header of the request isn’t specified and is required. The apns-topic header is mandatory when the client is connected using a certificate that supports multiple topics.")] + MissingTopic, + + #[error("The message payload is empty.")] + PayloadEmpty, + + #[error("Pushing to this topic is not allowed.")] + TopicDisallowed, + + #[error("The certificate is invalid.")] + BadCertificate { + /// The time, in milliseconds since Epoch, at which APNs confirmed the token + /// was no longer valid for the topic. This key is included only when the + /// error in the `:status` field is 410. + #[serde_as(as = "Option")] + timestamp: Option, + }, + + #[error("The client certificate is for the wrong environment.")] + BadCertificateEnvironment { + /// The time, in milliseconds since Epoch, at which APNs confirmed the token + /// was no longer valid for the topic. This key is included only when the + /// error in the `:status` field is 410. + #[serde_as(as = "Option")] + timestamp: Option, + }, + + #[error("The provider token is stale and a new token should be generated.")] + ExpiredProviderToken, + + #[error("The specified action is not allowed.")] + Forbidden, + + #[error("The provider token is not valid, or the token signature can't be verified.")] + InvalidProviderToken, + + #[error("No provider certificate was used to connect to APNs, and the authorization header is missing or no provider token is specified.")] + MissingProviderToken, + + #[error("The request contained an invalid :path value.")] + BadPath, + + #[error("The specified :method value isn’t POST.")] + MethodNotAllowed, + + #[error("The device token has expired.")] + ExpiredToken, + + #[error("The device token is inactive for the specified topic. There is no need to send further pushes to the same device token, unless your application retrieves the same device token, see Registering Your App with APNs")] + Unregistered, + + #[error("The message payload is too large. For information about the allowed payload size, see Create and Send a POST Request to APNs.")] + PayloadTooLarge, + + #[error("The provider’s authentication token is being updated too often. Update the authentication token no more than once every 20 minutes.")] + TooManyProviderTokenUpdates, + + #[error("Too many requests were made consecutively to the same device token.")] + TooManyRequests, + + #[error("An internal server error occurred.")] + InternalServerError, + + #[error("The service is unavailable.")] + ServiceUnavailable, + + #[error("The APNs server is shutting down.")] + Shutdown, + + #[error("unknown")] + #[serde(other)] + Unknown, +} + +impl From for StatusCode { + fn from(this: Reason) -> Self { + match this { + Reason::BadCollapseId => StatusCode::BAD_REQUEST, + Reason::BadDeviceToken => StatusCode::BAD_REQUEST, + Reason::BadExpirationDate => StatusCode::BAD_REQUEST, + Reason::BadMessageId => StatusCode::BAD_REQUEST, + Reason::BadPriority => StatusCode::BAD_REQUEST, + Reason::BadTopic => StatusCode::BAD_REQUEST, + Reason::DeviceTokenNotForTopic => StatusCode::BAD_REQUEST, + Reason::DuplicateHeaders => StatusCode::BAD_REQUEST, + Reason::IdleTimeout => StatusCode::BAD_REQUEST, + Reason::InvalidPushType => StatusCode::BAD_REQUEST, + Reason::MissingDeviceToken => StatusCode::BAD_REQUEST, + Reason::MissingTopic => StatusCode::BAD_REQUEST, + Reason::PayloadEmpty => StatusCode::BAD_REQUEST, + Reason::TopicDisallowed => StatusCode::BAD_REQUEST, + Reason::BadCertificate { .. } => StatusCode::FORBIDDEN, + Reason::BadCertificateEnvironment { .. } => StatusCode::FORBIDDEN, + Reason::ExpiredProviderToken => StatusCode::FORBIDDEN, + Reason::Forbidden => StatusCode::FORBIDDEN, + Reason::InvalidProviderToken => StatusCode::FORBIDDEN, + Reason::MissingProviderToken => StatusCode::FORBIDDEN, + Reason::BadPath => StatusCode::NOT_FOUND, + Reason::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED, + Reason::ExpiredToken => StatusCode::GONE, + Reason::Unregistered => StatusCode::GONE, + Reason::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE, + Reason::TooManyProviderTokenUpdates => StatusCode::TOO_MANY_REQUESTS, + Reason::TooManyRequests => StatusCode::TOO_MANY_REQUESTS, + Reason::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + Reason::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE, + Reason::Shutdown => StatusCode::SERVICE_UNAVAILABLE, + Reason::Unknown => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/src/apns/response.rs b/src/apns/response.rs deleted file mode 100644 index b0c3446d..00000000 --- a/src/apns/response.rs +++ /dev/null @@ -1,139 +0,0 @@ -use http::StatusCode; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, skip_serializing_none, TimestampMilliSeconds}; -use time::OffsetDateTime; - -use crate::result::Error; - -#[serde_as] -#[skip_serializing_none] -#[derive(Debug, Default, PartialEq, Eq, Deserialize, Serialize)] -pub struct ApnsResponse { - /// The error code indicating the reason for the failure. - pub reason: Option, - - /// The time, in milliseconds since Epoch, at which APNs confirmed the token - /// was no longer valid for the topic. This key is included only when the - /// error in the `:status` field is 410. - #[serde_as(as = "Option")] - pub timestamp: Option, -} - -impl From for Error { - fn from(this: ApnsResponse) -> Self { - if let Some(reason) = this.reason { - match reason { - Reason::BadCollapseId => Error::ApnsBadCollapseId, - Reason::BadDeviceToken => Error::ApnsBadDeviceToken, - Reason::BadExpirationDate => Error::ApnsBadExpirationDate, - Reason::BadMessageId => Error::ApnsBadMessageId, - Reason::BadPriority => Error::ApnsBadPriority, - Reason::BadTopic => Error::ApnsBadTopic, - Reason::DeviceTokenNotForTopic => Error::ApnsDeviceTokenNotForTopic, - Reason::DuplicateHeaders => Error::ApnsDuplicateHeaders, - Reason::IdleTimeout => Error::ApnsIdleTimeout, - Reason::InvalidPushType => Error::ApnsInvalidPushType, - Reason::MissingDeviceToken => Error::ApnsMissingDeviceToken, - Reason::MissingTopic => Error::ApnsMissingTopic, - Reason::PayloadEmpty => Error::ApnsPayloadEmpty, - Reason::TopicDisallowed => Error::ApnsTopicDisallowed, - Reason::BadCertificate => Error::ApnsBadCertificate { - timestamp: this.timestamp.unwrap_or(OffsetDateTime::UNIX_EPOCH), - }, - Reason::BadCertificateEnvironment => Error::ApnsBadCertificateEnvironment { - timestamp: this.timestamp.unwrap_or(OffsetDateTime::UNIX_EPOCH), - }, - Reason::ExpiredProviderToken => Error::ApnsExpiredProviderToken, - Reason::Forbidden => Error::ApnsForbidden, - Reason::InvalidProviderToken => Error::ApnsInvalidProviderToken, - Reason::MissingProviderToken => Error::ApnsMissingProviderToken, - Reason::BadPath => Error::ApnsBadPath, - Reason::MethodNotAllowed => Error::ApnsMethodNotAllowed, - Reason::ExpiredToken => Error::ApnsExpiredToken, - Reason::Unregistered => Error::ApnsUnregistered, - Reason::PayloadTooLarge => Error::ApnsPayloadTooLarge, - Reason::TooManyProviderTokenUpdates => Error::ApnsTooManyProviderTokenUpdates, - Reason::TooManyRequests => Error::ApnsTooManyRequests, - Reason::InternalServerError => Error::ApnsInternalServerError, - Reason::ServiceUnavailable => Error::ApnsServiceUnavailable, - Reason::Shutdown => Error::ApnsShutdown, - Reason::Other(msg) => Error::ApnsOther(msg), - } - } else { - Error::Unknown - } - } -} - -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] -pub enum Reason { - BadCollapseId, - BadDeviceToken, - BadExpirationDate, - BadMessageId, - BadPriority, - BadTopic, - DeviceTokenNotForTopic, - DuplicateHeaders, - IdleTimeout, - InvalidPushType, - MissingDeviceToken, - MissingTopic, - PayloadEmpty, - TopicDisallowed, - BadCertificate, - BadCertificateEnvironment, - ExpiredProviderToken, - Forbidden, - InvalidProviderToken, - MissingProviderToken, - BadPath, - MethodNotAllowed, - ExpiredToken, - Unregistered, - PayloadTooLarge, - TooManyProviderTokenUpdates, - TooManyRequests, - InternalServerError, - ServiceUnavailable, - Shutdown, - Other(String), -} - -impl From for StatusCode { - fn from(this: Reason) -> Self { - match this { - Reason::BadCollapseId => StatusCode::BAD_REQUEST, - Reason::BadDeviceToken => StatusCode::BAD_REQUEST, - Reason::BadExpirationDate => StatusCode::BAD_REQUEST, - Reason::BadMessageId => StatusCode::BAD_REQUEST, - Reason::BadPriority => StatusCode::BAD_REQUEST, - Reason::BadTopic => StatusCode::BAD_REQUEST, - Reason::DeviceTokenNotForTopic => StatusCode::BAD_REQUEST, - Reason::DuplicateHeaders => StatusCode::BAD_REQUEST, - Reason::IdleTimeout => StatusCode::BAD_REQUEST, - Reason::InvalidPushType => StatusCode::BAD_REQUEST, - Reason::MissingDeviceToken => StatusCode::BAD_REQUEST, - Reason::MissingTopic => StatusCode::BAD_REQUEST, - Reason::PayloadEmpty => StatusCode::BAD_REQUEST, - Reason::TopicDisallowed => StatusCode::BAD_REQUEST, - Reason::BadCertificate => StatusCode::FORBIDDEN, - Reason::BadCertificateEnvironment => StatusCode::FORBIDDEN, - Reason::ExpiredProviderToken => StatusCode::FORBIDDEN, - Reason::Forbidden => StatusCode::FORBIDDEN, - Reason::InvalidProviderToken => StatusCode::FORBIDDEN, - Reason::MissingProviderToken => StatusCode::FORBIDDEN, - Reason::BadPath => StatusCode::NOT_FOUND, - Reason::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED, - Reason::ExpiredToken => StatusCode::GONE, - Reason::Unregistered => StatusCode::GONE, - Reason::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE, - Reason::TooManyProviderTokenUpdates => StatusCode::TOO_MANY_REQUESTS, - Reason::TooManyRequests => StatusCode::TOO_MANY_REQUESTS, - Reason::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, - Reason::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE, - Reason::Shutdown => StatusCode::SERVICE_UNAVAILABLE, - Reason::Other(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 7e42adca..35a80082 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ pub mod apns; pub mod result; pub use apns::header; +pub use apns::reason::*; pub use apns::request::*; -pub use apns::response::*; pub use apns::*; pub use result::*; diff --git a/src/result.rs b/src/result.rs index c7948ed3..44edfb75 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,101 +1,11 @@ -use time::OffsetDateTime; +use crate::apns::reason::Reason; pub type Result = std::result::Result; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("The collapse identifier exceeds the maximum allowed size.")] - ApnsBadCollapseId, - - #[error("The specified device token is invalid. Verify that the request contains a valid token and that the token matches the environment.")] - ApnsBadDeviceToken, - - #[error("The apns-expiration value is invalid.")] - ApnsBadExpirationDate, - - #[error("The apns-id value is invalid.")] - ApnsBadMessageId, - - #[error("The apns-priority value is invalid.")] - ApnsBadPriority, - - #[error("The apns-topic value is invalid.")] - ApnsBadTopic, - - #[error("The device token doesn’t match the specified topic.")] - ApnsDeviceTokenNotForTopic, - - #[error("One or more headers are repeated.")] - ApnsDuplicateHeaders, - - #[error("Idle timeout.")] - ApnsIdleTimeout, - - #[error("The apns-push-type value is invalid.")] - ApnsInvalidPushType, - - #[error("The device token isn’t specified in the request :path. Verify that the :path header contains the device token.")] - ApnsMissingDeviceToken, - - #[error("The apns-topic header of the request isn’t specified and is required. The apns-topic header is mandatory when the client is connected using a certificate that supports multiple topics.")] - ApnsMissingTopic, - - #[error("The message payload is empty.")] - ApnsPayloadEmpty, - - #[error("Pushing to this topic is not allowed.")] - ApnsTopicDisallowed, - - #[error("The certificate is invalid.")] - ApnsBadCertificate { timestamp: OffsetDateTime }, - - #[error("The client certificate is for the wrong environment.")] - ApnsBadCertificateEnvironment { timestamp: OffsetDateTime }, - - #[error("The provider token is stale and a new token should be generated.")] - ApnsExpiredProviderToken, - - #[error("The specified action is not allowed.")] - ApnsForbidden, - - #[error("The provider token is not valid, or the token signature can't be verified.")] - ApnsInvalidProviderToken, - - #[error("No provider certificate was used to connect to APNs, and the authorization header is missing or no provider token is specified.")] - ApnsMissingProviderToken, - - #[error("The request contained an invalid :path value.")] - ApnsBadPath, - - #[error("The specified :method value isn’t POST.")] - ApnsMethodNotAllowed, - - #[error("The device token has expired.")] - ApnsExpiredToken, - - #[error("The device token is inactive for the specified topic. There is no need to send further pushes to the same device token, unless your application retrieves the same device token, see Registering Your App with APNs")] - ApnsUnregistered, - - #[error("The message payload is too large. For information about the allowed payload size, see Create and Send a POST Request to APNs.")] - ApnsPayloadTooLarge, - - #[error("The provider’s authentication token is being updated too often. Update the authentication token no more than once every 20 minutes.")] - ApnsTooManyProviderTokenUpdates, - - #[error("Too many requests were made consecutively to the same device token.")] - ApnsTooManyRequests, - - #[error("An internal server error occurred.")] - ApnsInternalServerError, - - #[error("The service is unavailable.")] - ApnsServiceUnavailable, - - #[error("The APNs server is shutting down.")] - ApnsShutdown, - - #[error("{0}")] - ApnsOther(String), + #[error(transparent)] + Apns(#[from] Reason), #[error("interruption level does not match sound critical flag")] CriticalSound,