Revision control
Copy as Markdown
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#![allow(unknown_lints)]
#![warn(rust_2018_idioms)]
use url::Url;
#[macro_use]
mod headers;
mod backend;
pub mod error;
pub mod settings;
pub use error::*;
pub use backend::{note_backend, set_backend, Backend};
pub use headers::{consts as header_names, Header, HeaderName, Headers, InvalidHeaderName};
pub use settings::GLOBAL_SETTINGS;
#[allow(clippy::derive_partial_eq_without_eq)]
pub(crate) mod msg_types {
include!("mozilla.appservices.httpconfig.protobuf.rs");
}
/// HTTP Methods.
///
/// The supported methods are the limited to what's supported by android-components.
#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[repr(u8)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
}
impl Method {
pub fn as_str(self) -> &'static str {
match self {
Method::Get => "GET",
Method::Head => "HEAD",
Method::Post => "POST",
Method::Put => "PUT",
Method::Delete => "DELETE",
Method::Connect => "CONNECT",
Method::Options => "OPTIONS",
Method::Trace => "TRACE",
Method::Patch => "PATCH",
}
}
}
impl std::fmt::Display for Method {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[must_use = "`Request`'s \"builder\" functions take by move, not by `&mut self`"]
#[derive(Clone, Debug)]
pub struct Request {
pub method: Method,
pub url: Url,
pub headers: Headers,
pub body: Option<Vec<u8>>,
}
impl Request {
/// Construct a new request to the given `url` using the given `method`.
/// Note that the request is not made until `send()` is called.
pub fn new(method: Method, url: Url) -> Self {
Self {
method,
url,
headers: Headers::new(),
body: None,
}
}
pub fn send(self) -> Result<Response, Error> {
crate::backend::send(self)
}
/// Alias for `Request::new(Method::Get, url)`, for convenience.
pub fn get(url: Url) -> Self {
Self::new(Method::Get, url)
}
/// Alias for `Request::new(Method::Patch, url)`, for convenience.
pub fn patch(url: Url) -> Self {
Self::new(Method::Patch, url)
}
/// Alias for `Request::new(Method::Post, url)`, for convenience.
pub fn post(url: Url) -> Self {
Self::new(Method::Post, url)
}
/// Alias for `Request::new(Method::Put, url)`, for convenience.
pub fn put(url: Url) -> Self {
Self::new(Method::Put, url)
}
/// Alias for `Request::new(Method::Delete, url)`, for convenience.
pub fn delete(url: Url) -> Self {
Self::new(Method::Delete, url)
}
/// Append the provided query parameters to the URL
///
/// ## Example
/// ```
/// # use viaduct::{Request, header_names};
/// # use url::Url;
///
/// let req = Request::post(some_url).query(&[("a", "1234"), ("b", "qwerty")]);
///
/// // This appends to the query query instead of replacing `a`.
/// let req = req.query(&[("a", "5678")]);
/// ```
pub fn query(mut self, pairs: &[(&str, &str)]) -> Self {
let mut append_to = self.url.query_pairs_mut();
for (k, v) in pairs {
append_to.append_pair(k, v);
}
drop(append_to);
self
}
/// Set the query string of the URL. Note that `req.set_query(None)` will
/// clear the query.
///
/// See also `Request::query` which appends a slice of query pairs, which is
/// typically more ergonomic when usable.
///
/// ## Example
/// ```
/// # use viaduct::{Request, header_names};
/// # use url::Url;
///
/// let req = Request::post(some_url).set_query("a=b&c=d");
///
/// let req = req.set_query(None);
/// ```
pub fn set_query<'a, Q: Into<Option<&'a str>>>(mut self, query: Q) -> Self {
self.url.set_query(query.into());
self
}
/// Add all the provided headers to the list of headers to send with this
/// request.
pub fn headers<I>(mut self, to_add: I) -> Self
where
I: IntoIterator<Item = Header>,
{
self.headers.extend(to_add);
self
}
/// Add the provided header to the list of headers to send with this request.
///
/// This returns `Err` if `val` contains characters that may not appear in
/// the body of a header.
///
/// ## Example
/// ```
/// # use viaduct::{Request, header_names};
/// # use url::Url;
/// # fn main() -> Result<(), viaduct::Error> {
/// Request::post(some_url)
/// .header(header_names::CONTENT_TYPE, "application/json")?
/// .header("My-Header", "Some special value")?;
/// // ...
/// # Ok(())
/// # }
/// ```
pub fn header<Name, Val>(mut self, name: Name, val: Val) -> Result<Self, crate::Error>
where
Name: Into<HeaderName> + PartialEq<HeaderName>,
Val: Into<String> + AsRef<str>,
{
self.headers.insert(name, val)?;
Ok(self)
}
/// Set this request's body.
pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = Some(body.into());
self
}
/// Set body to the result of serializing `val`, and, unless it has already
/// been set, set the Content-Type header to "application/json".
///
/// Note: This panics if serde_json::to_vec fails. This can only happen
/// in a couple cases:
///
/// 1. Trying to serialize a map with non-string keys.
/// 2. We wrote a custom serializer that fails.
///
/// Neither of these are things we do. If they happen, it seems better for
/// this to fail hard with an easy to track down panic, than for e.g. `sync`
/// to fail with a JSON parse error (which we'd probably attribute to
/// corrupt data on the server, or something).
pub fn json<T: ?Sized + serde::Serialize>(mut self, val: &T) -> Self {
self.body =
Some(serde_json::to_vec(val).expect("Rust component bug: serde_json::to_vec failure"));
self.headers
.insert_if_missing(header_names::CONTENT_TYPE, "application/json")
.unwrap(); // We know this has to be valid.
self
}
}
/// A response from the server.
#[derive(Clone, Debug)]
pub struct Response {
/// The method used to request this response.
pub request_method: Method,
/// The URL of this response.
pub url: Url,
/// The HTTP Status code of this response.
pub status: u16,
/// The headers returned with this response.
pub headers: Headers,
/// The body of the response.
pub body: Vec<u8>,
}
impl Response {
/// Parse the body as JSON.
pub fn json<'a, T>(&'a self) -> Result<T, serde_json::Error>
where
T: serde::Deserialize<'a>,
{
serde_json::from_slice(&self.body)
}
/// Get the body as a string. Assumes UTF-8 encoding. Any non-utf8 bytes
/// are replaced with the replacement character.
pub fn text(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(&self.body)
}
/// Returns true if the status code is in the interval `[200, 300)`.
#[inline]
pub fn is_success(&self) -> bool {
status_codes::is_success_code(self.status)
}
/// Returns true if the status code is in the interval `[500, 600)`.
#[inline]
pub fn is_server_error(&self) -> bool {
status_codes::is_server_error_code(self.status)
}
/// Returns true if the status code is in the interval `[400, 500)`.
#[inline]
pub fn is_client_error(&self) -> bool {
status_codes::is_client_error_code(self.status)
}
/// Returns an [`UnexpectedStatus`] error if `self.is_success()` is false,
/// otherwise returns `Ok(self)`.
#[inline]
pub fn require_success(self) -> Result<Self, UnexpectedStatus> {
if self.is_success() {
Ok(self)
} else {
Err(UnexpectedStatus {
method: self.request_method,
// XXX We probably should try and sanitize this. Replace the user id
// if it's a sync token server URL, for example.
url: self.url,
status: self.status,
})
}
}
}
/// A module containing constants for all HTTP status codes.
pub mod status_codes {
/// Is it a 2xx status?
#[inline]
pub fn is_success_code(c: u16) -> bool {
(200..300).contains(&c)
}
/// Is it a 4xx error?
#[inline]
pub fn is_client_error_code(c: u16) -> bool {
(400..500).contains(&c)
}
/// Is it a 5xx error?
#[inline]
pub fn is_server_error_code(c: u16) -> bool {
(500..600).contains(&c)
}
macro_rules! define_status_codes {
($(($val:expr, $NAME:ident)),* $(,)?) => {
$(pub const $NAME: u16 = $val;)*
};
}
define_status_codes![
(100, CONTINUE),
(101, SWITCHING_PROTOCOLS),
// 2xx
(200, OK),
(201, CREATED),
(202, ACCEPTED),
(203, NONAUTHORITATIVE_INFORMATION),
(204, NO_CONTENT),
(205, RESET_CONTENT),
(206, PARTIAL_CONTENT),
// 3xx
(300, MULTIPLE_CHOICES),
(301, MOVED_PERMANENTLY),
(302, FOUND),
(303, SEE_OTHER),
(304, NOT_MODIFIED),
(305, USE_PROXY),
// no 306
(307, TEMPORARY_REDIRECT),
// 4xx
(400, BAD_REQUEST),
(401, UNAUTHORIZED),
(402, PAYMENT_REQUIRED),
(403, FORBIDDEN),
(404, NOT_FOUND),
(405, METHOD_NOT_ALLOWED),
(406, NOT_ACCEPTABLE),
(407, PROXY_AUTHENTICATION_REQUIRED),
(408, REQUEST_TIMEOUT),
(409, CONFLICT),
(410, GONE),
(411, LENGTH_REQUIRED),
(412, PRECONDITION_FAILED),
(413, REQUEST_ENTITY_TOO_LARGE),
(414, REQUEST_URI_TOO_LONG),
(415, UNSUPPORTED_MEDIA_TYPE),
(416, REQUESTED_RANGE_NOT_SATISFIABLE),
(417, EXPECTATION_FAILED),
(429, TOO_MANY_REQUESTS),
// 5xx
(500, INTERNAL_SERVER_ERROR),
(501, NOT_IMPLEMENTED),
(502, BAD_GATEWAY),
(503, SERVICE_UNAVAILABLE),
(504, GATEWAY_TIMEOUT),
(505, HTTP_VERSION_NOT_SUPPORTED),
];
}