viaduct/
lib.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5#![allow(unknown_lints)]
6#![warn(rust_2018_idioms)]
7
8use url::Url;
9#[macro_use]
10mod headers;
11
12mod backend;
13pub mod error;
14pub mod settings;
15pub use error::*;
16// reexport logging helpers.
17pub use error_support::{debug, error, info, trace, warn};
18
19pub use backend::{note_backend, set_backend, Backend};
20pub use headers::{consts as header_names, Header, HeaderName, Headers, InvalidHeaderName};
21pub use settings::GLOBAL_SETTINGS;
22
23#[allow(clippy::derive_partial_eq_without_eq)]
24pub(crate) mod msg_types {
25    include!("mozilla.appservices.httpconfig.protobuf.rs");
26}
27
28/// HTTP Methods.
29///
30/// The supported methods are the limited to what's supported by android-components.
31#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
32#[repr(u8)]
33pub enum Method {
34    Get,
35    Head,
36    Post,
37    Put,
38    Delete,
39    Connect,
40    Options,
41    Trace,
42    Patch,
43}
44
45impl Method {
46    pub fn as_str(self) -> &'static str {
47        match self {
48            Method::Get => "GET",
49            Method::Head => "HEAD",
50            Method::Post => "POST",
51            Method::Put => "PUT",
52            Method::Delete => "DELETE",
53            Method::Connect => "CONNECT",
54            Method::Options => "OPTIONS",
55            Method::Trace => "TRACE",
56            Method::Patch => "PATCH",
57        }
58    }
59}
60
61impl std::fmt::Display for Method {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.write_str(self.as_str())
64    }
65}
66
67#[must_use = "`Request`'s \"builder\" functions take by move, not by `&mut self`"]
68#[derive(Clone, Debug)]
69pub struct Request {
70    pub method: Method,
71    pub url: Url,
72    pub headers: Headers,
73    pub body: Option<Vec<u8>>,
74}
75
76impl Request {
77    /// Construct a new request to the given `url` using the given `method`.
78    /// Note that the request is not made until `send()` is called.
79    pub fn new(method: Method, url: Url) -> Self {
80        Self {
81            method,
82            url,
83            headers: Headers::new(),
84            body: None,
85        }
86    }
87
88    pub fn send(self) -> Result<Response, Error> {
89        crate::backend::send(self)
90    }
91
92    /// Alias for `Request::new(Method::Get, url)`, for convenience.
93    pub fn get(url: Url) -> Self {
94        Self::new(Method::Get, url)
95    }
96
97    /// Alias for `Request::new(Method::Patch, url)`, for convenience.
98    pub fn patch(url: Url) -> Self {
99        Self::new(Method::Patch, url)
100    }
101
102    /// Alias for `Request::new(Method::Post, url)`, for convenience.
103    pub fn post(url: Url) -> Self {
104        Self::new(Method::Post, url)
105    }
106
107    /// Alias for `Request::new(Method::Put, url)`, for convenience.
108    pub fn put(url: Url) -> Self {
109        Self::new(Method::Put, url)
110    }
111
112    /// Alias for `Request::new(Method::Delete, url)`, for convenience.
113    pub fn delete(url: Url) -> Self {
114        Self::new(Method::Delete, url)
115    }
116
117    /// Append the provided query parameters to the URL
118    ///
119    /// ## Example
120    /// ```
121    /// # use viaduct::{Request, header_names};
122    /// # use url::Url;
123    /// let some_url = url::Url::parse("https://www.example.com/xyz").unwrap();
124    ///
125    /// let req = Request::post(some_url).query(&[("a", "1234"), ("b", "qwerty")]);
126    /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=1234&b=qwerty");
127    ///
128    /// // This appends to the query query instead of replacing `a`.
129    /// let req = req.query(&[("a", "5678")]);
130    /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=1234&b=qwerty&a=5678");
131    /// ```
132    pub fn query(mut self, pairs: &[(&str, &str)]) -> Self {
133        let mut append_to = self.url.query_pairs_mut();
134        for (k, v) in pairs {
135            append_to.append_pair(k, v);
136        }
137        drop(append_to);
138        self
139    }
140
141    /// Set the query string of the URL. Note that `req.set_query(None)` will
142    /// clear the query.
143    ///
144    /// See also `Request::query` which appends a slice of query pairs, which is
145    /// typically more ergonomic when usable.
146    ///
147    /// ## Example
148    /// ```
149    /// # use viaduct::{Request, header_names};
150    /// # use url::Url;
151    /// let some_url = url::Url::parse("https://www.example.com/xyz").unwrap();
152    ///
153    /// let req = Request::post(some_url).set_query("a=b&c=d");
154    /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=b&c=d");
155    ///
156    /// let req = req.set_query(None);
157    /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz");
158    /// ```
159    pub fn set_query<'a, Q: Into<Option<&'a str>>>(mut self, query: Q) -> Self {
160        self.url.set_query(query.into());
161        self
162    }
163
164    /// Add all the provided headers to the list of headers to send with this
165    /// request.
166    pub fn headers<I>(mut self, to_add: I) -> Self
167    where
168        I: IntoIterator<Item = Header>,
169    {
170        self.headers.extend(to_add);
171        self
172    }
173
174    /// Add the provided header to the list of headers to send with this request.
175    ///
176    /// This returns `Err` if `val` contains characters that may not appear in
177    /// the body of a header.
178    ///
179    /// ## Example
180    /// ```
181    /// # use viaduct::{Request, header_names};
182    /// # use url::Url;
183    /// # fn main() -> Result<(), viaduct::Error> {
184    /// # let some_url = url::Url::parse("https://www.example.com").unwrap();
185    /// Request::post(some_url)
186    ///     .header(header_names::CONTENT_TYPE, "application/json")?
187    ///     .header("My-Header", "Some special value")?;
188    /// // ...
189    /// # Ok(())
190    /// # }
191    /// ```
192    pub fn header<Name, Val>(mut self, name: Name, val: Val) -> Result<Self, crate::Error>
193    where
194        Name: Into<HeaderName> + PartialEq<HeaderName>,
195        Val: Into<String> + AsRef<str>,
196    {
197        self.headers.insert(name, val)?;
198        Ok(self)
199    }
200
201    /// Set this request's body.
202    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
203        self.body = Some(body.into());
204        self
205    }
206
207    /// Set body to the result of serializing `val`, and, unless it has already
208    /// been set, set the Content-Type header to "application/json".
209    ///
210    /// Note: This panics if serde_json::to_vec fails. This can only happen
211    /// in a couple cases:
212    ///
213    /// 1. Trying to serialize a map with non-string keys.
214    /// 2. We wrote a custom serializer that fails.
215    ///
216    /// Neither of these are things we do. If they happen, it seems better for
217    /// this to fail hard with an easy to track down panic, than for e.g. `sync`
218    /// to fail with a JSON parse error (which we'd probably attribute to
219    /// corrupt data on the server, or something).
220    pub fn json<T: ?Sized + serde::Serialize>(mut self, val: &T) -> Self {
221        self.body =
222            Some(serde_json::to_vec(val).expect("Rust component bug: serde_json::to_vec failure"));
223        self.headers
224            .insert_if_missing(header_names::CONTENT_TYPE, "application/json")
225            .unwrap(); // We know this has to be valid.
226        self
227    }
228}
229
230/// A response from the server.
231#[derive(Clone, Debug)]
232pub struct Response {
233    /// The method used to request this response.
234    pub request_method: Method,
235    /// The URL of this response.
236    pub url: Url,
237    /// The HTTP Status code of this response.
238    pub status: u16,
239    /// The headers returned with this response.
240    pub headers: Headers,
241    /// The body of the response.
242    pub body: Vec<u8>,
243}
244
245impl Response {
246    /// Parse the body as JSON.
247    pub fn json<'a, T>(&'a self) -> Result<T, serde_json::Error>
248    where
249        T: serde::Deserialize<'a>,
250    {
251        serde_json::from_slice(&self.body)
252    }
253
254    /// Get the body as a string. Assumes UTF-8 encoding. Any non-utf8 bytes
255    /// are replaced with the replacement character.
256    pub fn text(&self) -> std::borrow::Cow<'_, str> {
257        String::from_utf8_lossy(&self.body)
258    }
259
260    /// Returns true if the status code is in the interval `[200, 300)`.
261    #[inline]
262    pub fn is_success(&self) -> bool {
263        status_codes::is_success_code(self.status)
264    }
265
266    /// Returns true if the status code is in the interval `[500, 600)`.
267    #[inline]
268    pub fn is_server_error(&self) -> bool {
269        status_codes::is_server_error_code(self.status)
270    }
271
272    /// Returns true if the status code is in the interval `[400, 500)`.
273    #[inline]
274    pub fn is_client_error(&self) -> bool {
275        status_codes::is_client_error_code(self.status)
276    }
277
278    /// Returns an [`UnexpectedStatus`] error if `self.is_success()` is false,
279    /// otherwise returns `Ok(self)`.
280    #[inline]
281    pub fn require_success(self) -> Result<Self, UnexpectedStatus> {
282        if self.is_success() {
283            Ok(self)
284        } else {
285            Err(UnexpectedStatus {
286                method: self.request_method,
287                // XXX We probably should try and sanitize this. Replace the user id
288                // if it's a sync token server URL, for example.
289                url: self.url,
290                status: self.status,
291            })
292        }
293    }
294}
295
296/// A module containing constants for all HTTP status codes.
297pub mod status_codes {
298
299    /// Is it a 2xx status?
300    #[inline]
301    pub fn is_success_code(c: u16) -> bool {
302        (200..300).contains(&c)
303    }
304
305    /// Is it a 4xx error?
306    #[inline]
307    pub fn is_client_error_code(c: u16) -> bool {
308        (400..500).contains(&c)
309    }
310
311    /// Is it a 5xx error?
312    #[inline]
313    pub fn is_server_error_code(c: u16) -> bool {
314        (500..600).contains(&c)
315    }
316
317    macro_rules! define_status_codes {
318        ($(($val:expr, $NAME:ident)),* $(,)?) => {
319            $(pub const $NAME: u16 = $val;)*
320        };
321    }
322    // From https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
323    define_status_codes![
324        (100, CONTINUE),
325        (101, SWITCHING_PROTOCOLS),
326        // 2xx
327        (200, OK),
328        (201, CREATED),
329        (202, ACCEPTED),
330        (203, NONAUTHORITATIVE_INFORMATION),
331        (204, NO_CONTENT),
332        (205, RESET_CONTENT),
333        (206, PARTIAL_CONTENT),
334        // 3xx
335        (300, MULTIPLE_CHOICES),
336        (301, MOVED_PERMANENTLY),
337        (302, FOUND),
338        (303, SEE_OTHER),
339        (304, NOT_MODIFIED),
340        (305, USE_PROXY),
341        // no 306
342        (307, TEMPORARY_REDIRECT),
343        // 4xx
344        (400, BAD_REQUEST),
345        (401, UNAUTHORIZED),
346        (402, PAYMENT_REQUIRED),
347        (403, FORBIDDEN),
348        (404, NOT_FOUND),
349        (405, METHOD_NOT_ALLOWED),
350        (406, NOT_ACCEPTABLE),
351        (407, PROXY_AUTHENTICATION_REQUIRED),
352        (408, REQUEST_TIMEOUT),
353        (409, CONFLICT),
354        (410, GONE),
355        (411, LENGTH_REQUIRED),
356        (412, PRECONDITION_FAILED),
357        (413, REQUEST_ENTITY_TOO_LARGE),
358        (414, REQUEST_URI_TOO_LONG),
359        (415, UNSUPPORTED_MEDIA_TYPE),
360        (416, REQUESTED_RANGE_NOT_SATISFIABLE),
361        (417, EXPECTATION_FAILED),
362        (429, TOO_MANY_REQUESTS),
363        // 5xx
364        (500, INTERNAL_SERVER_ERROR),
365        (501, NOT_IMPLEMENTED),
366        (502, BAD_GATEWAY),
367        (503, SERVICE_UNAVAILABLE),
368        (504, GATEWAY_TIMEOUT),
369        (505, HTTP_VERSION_NOT_SUPPORTED),
370    ];
371}