web_push/clients/
request_builder.rs1use http::{
5 header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE},
6 Request, StatusCode,
7};
8
9use crate::{
10 error::{ErrorInfo, WebPushError},
11 message::WebPushMessage,
12};
13
14pub fn build_request<T>(message: WebPushMessage) -> Request<T>
38where
39 T: From<Vec<u8>> + From<&'static str>, {
41 let mut builder = Request::builder()
42 .method("POST")
43 .uri(message.endpoint)
44 .header("TTL", format!("{}", message.ttl).as_bytes());
45
46 if let Some(urgency) = message.urgency {
47 builder = builder.header("Urgency", urgency.to_string());
48 }
49
50 if let Some(topic) = message.topic {
51 builder = builder.header("Topic", topic);
52 }
53
54 if let Some(payload) = message.payload {
55 builder = builder
56 .header(CONTENT_ENCODING, payload.content_encoding.to_str())
57 .header(CONTENT_LENGTH, format!("{}", payload.content.len() as u64).as_bytes())
58 .header(CONTENT_TYPE, "application/octet-stream");
59
60 for (k, v) in payload.crypto_headers.into_iter() {
61 let v: &str = v.as_ref();
62 builder = builder.header(k, v);
63 }
64
65 builder.body(payload.content.into()).unwrap()
66 } else {
67 builder.body("".into()).unwrap()
68 }
69}
70
71pub fn parse_response(response_status: StatusCode, body: Vec<u8>) -> Result<(), WebPushError> {
73 if response_status.is_success() {
74 return Ok(());
75 }
76
77 let info: ErrorInfo = serde_json::from_slice(&body).unwrap_or_else(|_| ErrorInfo {
78 code: response_status.as_u16(),
79 errno: 999,
80 error: "unknown error".into(),
81 message: String::from_utf8(body).unwrap_or_else(|_| "-".into()),
82 });
83
84 match response_status {
85 StatusCode::UNAUTHORIZED => Err(WebPushError::Unauthorized(info)),
86 StatusCode::GONE => Err(WebPushError::EndpointNotValid(info)),
87 StatusCode::NOT_FOUND => Err(WebPushError::EndpointNotFound(info)),
88 StatusCode::PAYLOAD_TOO_LARGE => Err(WebPushError::PayloadTooLarge),
89 StatusCode::BAD_REQUEST => Err(WebPushError::BadRequest(info)),
90 status if status.is_server_error() => Err(WebPushError::ServerError {
91 retry_after: None,
92 info,
93 }),
94 _ => Err(WebPushError::Other(info)),
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use http::Uri;
101
102 use crate::{
103 clients::request_builder::*, error::WebPushError, http_ece::ContentEncoding, message::WebPushMessageBuilder,
104 Urgency,
105 };
106
107 #[cfg(feature = "isahc-client")]
108 #[test]
109 fn builds_a_correct_request_with_empty_payload() {
110 let sub = serde_json::json!({"endpoint":"https://fcm.googleapis.com/fcm/send/eKClHsXFm9E:APA91bH2x3gNOMv4dF1lQfCgIfOet8EngqKCAUS5DncLOd5hzfSUxcjigIjw9ws-bqa-KmohqiTOcgepAIVO03N39dQfkEkopubML_m3fyvF03pV9_JCB7SxpUjcFmBSVhCaWS6m8l7x",
112 "expirationTime":null,
113 "keys":{"p256dh":
114 "BGa4N1PI79lboMR_YrwCiCsgp35DRvedt7opHcf0yM3iOBTSoQYqQLwWxAfRKE6tsDnReWmhsImkhDF_DBdkNSU",
115 "auth":"EvcWjEgzr4rbvhfi3yds0A"}
116 });
117
118 let info = serde_json::from_value(sub).unwrap();
119
120 let mut builder = WebPushMessageBuilder::new(&info);
121
122 builder.set_ttl(420);
123 builder.set_urgency(Urgency::VeryLow);
124 builder.set_topic("some-topic".into());
125
126 let request = build_request::<isahc::Body>(builder.build().unwrap());
127 let ttl = request.headers().get("TTL").unwrap().to_str().unwrap();
128 let urgency = request.headers().get("Urgency").unwrap().to_str().unwrap();
129 let topic = request.headers().get("Topic").unwrap().to_str().unwrap();
130 let expected_uri: Uri = "fcm.googleapis.com".parse().unwrap();
131
132 assert_eq!("420", ttl);
133 assert_eq!("very-low", urgency);
134 assert_eq!("some-topic", topic);
135 assert_eq!(expected_uri.host(), request.uri().host());
136 }
137
138 #[cfg(feature = "isahc-client")]
139 #[test]
140 fn builds_a_correct_request_with_payload() {
141 let sub = serde_json::json!({"endpoint":"https://fcm.googleapis.com/fcm/send/eKClHsXFm9E:APA91bH2x3gNOMv4dF1lQfCgIfOet8EngqKCAUS5DncLOd5hzfSUxcjigIjw9ws-bqa-KmohqiTOcgepAIVO03N39dQfkEkopubML_m3fyvF03pV9_JCB7SxpUjcFmBSVhCaWS6m8l7x",
143 "expirationTime":null,
144 "keys":{"p256dh":
145 "BGa4N1PI79lboMR_YrwCiCsgp35DRvedt7opHcf0yM3iOBTSoQYqQLwWxAfRKE6tsDnReWmhsImkhDF_DBdkNSU",
146 "auth":"EvcWjEgzr4rbvhfi3yds0A"}
147 });
148
149 let info = serde_json::from_value(sub).unwrap();
150
151 let mut builder = WebPushMessageBuilder::new(&info);
152
153 builder.set_payload(ContentEncoding::Aes128Gcm, "test".as_bytes());
154
155 let request = build_request::<isahc::Body>(builder.build().unwrap());
156
157 let encoding = request.headers().get("Content-Encoding").unwrap().to_str().unwrap();
158
159 let length = request.headers().get("Content-Length").unwrap();
160 let expected_uri: Uri = "fcm.googleapis.com".parse().unwrap();
161
162 assert_eq!("230", length);
163 assert_eq!("aes128gcm", encoding);
164 assert_eq!(expected_uri.host(), request.uri().host());
165 }
166
167 #[test]
168 fn parses_a_successful_response_correctly() {
169 assert!(matches!(parse_response(StatusCode::OK, vec![]), Ok(())));
170 }
171
172 #[test]
173 fn parses_an_unauthorized_response_correctly() {
174 assert!(matches!(
175 parse_response(StatusCode::UNAUTHORIZED, vec![]),
176 Err(WebPushError::Unauthorized(_))
177 ));
178 }
179
180 #[test]
181 fn parses_a_gone_response_correctly() {
182 assert!(matches!(
183 parse_response(StatusCode::GONE, vec![]),
184 Err(WebPushError::EndpointNotValid(_))
185 ));
186 }
187
188 #[test]
189 fn parses_a_not_found_response_correctly() {
190 assert!(matches!(
191 parse_response(StatusCode::NOT_FOUND, vec![]),
192 Err(WebPushError::EndpointNotFound(_))
193 ));
194 }
195
196 #[test]
197 fn parses_a_payload_too_large_response_correctly() {
198 assert!(matches!(
199 parse_response(StatusCode::PAYLOAD_TOO_LARGE, vec![]),
200 Err(WebPushError::PayloadTooLarge)
201 ));
202 }
203
204 #[test]
205 fn parses_a_server_error_response_correctly() {
206 assert!(matches!(
207 parse_response(StatusCode::INTERNAL_SERVER_ERROR, vec![]),
208 Err(WebPushError::ServerError { .. })
209 ));
210 }
211
212 #[test]
213 fn parses_a_bad_request_response_with_no_body_correctly() {
214 assert!(matches!(
215 parse_response(StatusCode::BAD_REQUEST, vec![]),
216 Err(WebPushError::BadRequest(_))
217 ));
218 }
219
220 #[test]
221 fn parses_a_bad_request_response_with_body_correctly() {
222 let json = r#"
223 {
224 "code": 400,
225 "errno": 103,
226 "error": "FooBar",
227 "message": "No message found"
228 }
229 "#;
230
231 assert!(matches!(
232 parse_response(StatusCode::BAD_REQUEST, json.as_bytes().to_vec()),
233 Err(WebPushError::BadRequest(ErrorInfo {
234 code: 400,
235 errno: 103,
236 error: _,
237 message: _,
238 })),
239 ));
240 }
241}