refactor(errors): use failure crate to handle errors (#94) r=@vladikoff,@pjenvey

Fixes #48
Fixes #77 

I had to change basically everything since my last PR (#85) about this, so I thought it would be best to just create a new PR.

To get the failure errors working I needed to get at least providers, bounces and db to also work with the failure errors, not just the Rocket and HTTP errors. That's what I'm doing in this PR. Since this is kind of a big change, I thought it would be best to send this in before adapting the tests to the new error type, so, right now, many tests are commented.

Let me know what you guys think. If you think this is a good change I still would need to adapt all tests and also there are some error types specially for the queues bin that need to be changed as well.

I personally think this is a good change. It is abstracting all the errors to one single place in the code. While working on this refactoring, I saw a bunch of repeated code for error handling. This ends that, which means it will be easier to maintain and create new error types with this way of doing things. Also, we are now getting much more information about each error, not just the HTTP status and message and it's very easy to customize this even further.
This commit is contained in:
Beatriz Rizental 2018-06-26 14:29:21 -07:00 коммит произвёл Vlad Filippov
Родитель 59280fac1d
Коммит 1797317231
15 изменённых файлов: 476 добавлений и 330 удалений

Просмотреть файл

@ -2,65 +2,284 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
use std::collections::HashMap;
use std::{fmt, result};
use failure::{Backtrace, Context, Fail};
use rocket::{
self,
http::Status,
response::{self, Responder, Response},
Request,
};
use rocket_contrib::Json;
use serde_json::{map::Map, ser::to_string, Value};
use auth_db::BounceRecord;
use logging::MozlogLogger;
#[cfg(test)]
mod test;
pub type AppResult<T> = result::Result<T, AppError>;
#[derive(Debug)]
pub struct AppError {
inner: Context<AppErrorKind>,
}
impl AppError {
pub fn json(&self) -> Value {
let kind = self.kind();
let status = kind.http_status();
let mut fields = Map::new();
fields.insert(
String::from("code"),
Value::String(format!("{}", status.code)),
);
fields.insert(
String::from("error"),
Value::String(format!("{}", status.reason)),
);
let errno = kind.errno();
if let Some(ref errno) = errno {
fields.insert(String::from("errno"), Value::String(format!("{}", errno)));
fields.insert(String::from("message"), Value::String(format!("{}", self)));
};
let additional_fields = kind.additional_fields();
for (field, value) in additional_fields.iter() {
fields.insert(field.clone(), value.clone());
}
json!(fields)
}
pub fn kind(&self) -> &AppErrorKind {
self.inner.get_context()
}
}
impl Fail for AppError {
fn cause(&self) -> Option<&Fail> {
self.inner.cause()
}
fn backtrace(&self) -> Option<&Backtrace> {
self.inner.backtrace()
}
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.inner.fmt(f)
}
}
/// The specific kind of error that can occur.
#[derive(Clone, Debug, Eq, Fail, PartialEq)]
pub enum AppErrorKind {
/// 400 Bad Request
#[fail(display = "Bad Request")]
BadRequest,
/// 404 Not Found
#[fail(display = "Not Found")]
NotFound,
/// 405 Method Not Allowed
#[fail(display = "Method Not Allowed")]
MethodNotAllowed,
/// 422 Unprocessable Entity
#[fail(display = "Unprocessable Entity")]
UnprocessableEntity,
/// 429 Too Many Requests,
#[fail(display = "Too Many Requests")]
TooManyRequests,
/// 500 Internal Server Error
#[fail(display = "Internal Server Error")]
InternalServerError,
// Unexpected rocket error
#[fail(display = "{:?}", _0)]
RocketError(rocket::Error),
/// An error for invalid email params in the /send handler.
#[fail(display = "Error validating email params.")]
InvalidEmailParams,
/// An error for missing email params in the /send handler.
#[fail(display = "Missing email params.")]
MissingEmailParams(String),
/// An error for invalid provider names.
#[fail(display = "Invalid provider name: {}", _0)]
InvalidProvider(String),
/// An error for when we get an error from a provider.
#[fail(display = "{}", _description)]
ProviderError { _name: String, _description: String },
/// An error for when we have trouble parsing the email message.
#[fail(display = "{:?}", _0)]
EmailParsingError(String),
/// An error for when a bounce violation happens.
#[fail(display = "Email account sent complaint.")]
BounceComplaintError {
_address: String,
_bounce: Option<BounceRecord>,
},
#[fail(display = "Email account soft bounced.")]
BounceSoftError {
_address: String,
_bounce: Option<BounceRecord>,
},
#[fail(display = "Email account hard bounced.")]
BounceHardError {
_address: String,
_bounce: Option<BounceRecord>,
},
/// An error for when an error happens on a request to the db.
#[fail(display = "{:?}", _0)]
DbError(String),
/// An error for when we try to access functionality that is not
/// implemented.
#[fail(display = "Feature not implemented.")]
NotImplemented,
}
impl AppErrorKind {
/// Return a rocket response Status to be rendered for an error
pub fn http_status(&self) -> Status {
match self {
AppErrorKind::NotFound => Status::NotFound,
AppErrorKind::MethodNotAllowed => Status::MethodNotAllowed,
AppErrorKind::UnprocessableEntity => Status::UnprocessableEntity,
AppErrorKind::TooManyRequests => Status::TooManyRequests,
AppErrorKind::BounceComplaintError { .. }
| AppErrorKind::BounceSoftError { .. }
| AppErrorKind::BounceHardError { .. } => Status::TooManyRequests,
AppErrorKind::BadRequest | AppErrorKind::InvalidEmailParams => Status::BadRequest,
AppErrorKind::MissingEmailParams(_) => Status::BadRequest,
AppErrorKind::InternalServerError | _ => Status::InternalServerError,
}
}
pub fn errno(&self) -> Option<i32> {
match self {
AppErrorKind::RocketError(_) => Some(100),
AppErrorKind::MissingEmailParams(_) => Some(101),
AppErrorKind::InvalidEmailParams => Some(102),
AppErrorKind::InvalidProvider(_) => Some(103),
AppErrorKind::ProviderError { .. } => Some(104),
AppErrorKind::EmailParsingError(_) => Some(105),
AppErrorKind::BounceComplaintError { .. } => Some(106),
AppErrorKind::BounceSoftError { .. } => Some(107),
AppErrorKind::BounceHardError { .. } => Some(108),
AppErrorKind::DbError(_) => Some(109),
AppErrorKind::NotImplemented => Some(110),
AppErrorKind::BadRequest
| AppErrorKind::NotFound
| AppErrorKind::MethodNotAllowed
| AppErrorKind::UnprocessableEntity
| AppErrorKind::TooManyRequests
| AppErrorKind::InternalServerError => None,
}
}
pub fn additional_fields(&self) -> Map<String, Value> {
let mut fields = Map::new();
match self {
AppErrorKind::ProviderError {
ref _name,
ref _description,
} => {
fields.insert(String::from("name"), Value::String(format!("{}", _name)));
}
AppErrorKind::BounceComplaintError {
ref _address,
ref _bounce,
}
| AppErrorKind::BounceSoftError {
ref _address,
ref _bounce,
}
| AppErrorKind::BounceHardError {
ref _address,
ref _bounce,
} => {
fields.insert(
String::from("address"),
Value::String(format!("{}", _address)),
);
if let Some(_bounce) = _bounce {
fields.insert(
String::from("bounce"),
Value::String(to_string(_bounce).unwrap_or(String::from("{}"))),
);
}
}
_ => (),
}
fields
}
}
impl From<AppErrorKind> for AppError {
fn from(kind: AppErrorKind) -> AppError {
Context::new(kind).into()
}
}
impl From<Context<AppErrorKind>> for AppError {
fn from(inner: Context<AppErrorKind>) -> AppError {
AppError { inner }
}
}
/// Generate HTTP error responses for AppErrors
impl<'r> Responder<'r> for AppError {
fn respond_to(self, request: &Request) -> response::Result<'r> {
let status = self.kind().http_status();
let json = Json(self.json());
let log = MozlogLogger::with_request(request).map_err(|_| Status::InternalServerError)?;
slog_error!(log, "{}", json.to_string());
let mut builder = Response::build_from(json.respond_to(request)?);
builder.status(status).ok()
}
}
#[error(400)]
pub fn bad_request() -> Json<ApplicationError> {
Json(ApplicationError::new(400, "Bad Request"))
pub fn bad_request() -> AppResult<()> {
Err(AppErrorKind::BadRequest)?
}
#[error(404)]
pub fn not_found() -> Json<ApplicationError> {
Json(ApplicationError::new(404, "Not Found"))
pub fn not_found() -> AppResult<()> {
Err(AppErrorKind::NotFound)?
}
#[error(405)]
pub fn method_not_allowed() -> Json<ApplicationError> {
Json(ApplicationError::new(405, "Method Not Allowed"))
pub fn method_not_allowed() -> AppResult<()> {
Err(AppErrorKind::MethodNotAllowed)?
}
#[error(422)]
pub fn unprocessable_entity() -> Json<ApplicationError> {
Json(ApplicationError::new(422, "Unprocessable Entity"))
pub fn unprocessable_entity() -> AppResult<()> {
Err(AppErrorKind::UnprocessableEntity)?
}
#[error(429)]
pub fn too_many_requests() -> Json<ApplicationError> {
Json(ApplicationError::new(429, "Too Many Requests"))
pub fn too_many_requests() -> AppResult<()> {
Err(AppErrorKind::TooManyRequests)?
}
#[error(500)]
pub fn internal_server_error() -> Json<ApplicationError> {
Json(ApplicationError::new(500, "Internal Server Error"))
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct ApplicationError {
pub status: u16,
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub errno: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<HashMap<String, String>>,
}
impl ApplicationError {
pub fn new(status: u16, error: &str) -> ApplicationError {
// TODO: Set errno, message and data when rocket#596 is resolved
// (https://github.com/SergioBenitez/Rocket/issues/596)
ApplicationError {
status,
error: error.to_string(),
errno: None,
message: None,
data: None,
}
}
pub fn internal_server_error() -> AppResult<()> {
Err(AppErrorKind::InternalServerError)?
}

Просмотреть файл

@ -2,52 +2,52 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
use super::ApplicationError;
use super::AppErrorKind;
#[test]
fn bad_request() {
assert_eq!(
super::bad_request().into_inner(),
ApplicationError::new(400, "Bad Request")
format!("{}", super::bad_request().unwrap_err().kind()),
format!("{}", AppErrorKind::BadRequest)
);
}
#[test]
fn not_found() {
assert_eq!(
super::not_found().into_inner(),
ApplicationError::new(404, "Not Found")
format!("{}", super::not_found().unwrap_err().kind()),
format!("{}", AppErrorKind::NotFound)
);
}
#[test]
fn method_not_allowed() {
assert_eq!(
super::method_not_allowed().into_inner(),
ApplicationError::new(405, "Method Not Allowed")
format!("{}", super::method_not_allowed().unwrap_err().kind()),
format!("{}", AppErrorKind::MethodNotAllowed)
);
}
#[test]
fn unprocessable_entity() {
assert_eq!(
super::unprocessable_entity().into_inner(),
ApplicationError::new(422, "Unprocessable Entity")
format!("{}", super::unprocessable_entity().unwrap_err().kind()),
format!("{}", AppErrorKind::UnprocessableEntity)
);
}
#[test]
fn too_many_requests() {
assert_eq!(
super::too_many_requests().into_inner(),
ApplicationError::new(429, "Too Many Requests")
format!("{}", super::too_many_requests().unwrap_err().kind()),
format!("{}", AppErrorKind::TooManyRequests)
);
}
#[test]
fn internal_server_error() {
assert_eq!(
super::internal_server_error().into_inner(),
ApplicationError::new(500, "Internal Server Error")
format!("{}", super::internal_server_error().unwrap_err().kind()),
format!("{}", AppErrorKind::InternalServerError)
);
}

Просмотреть файл

@ -2,10 +2,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
use std::{
error::Error,
fmt::{self, Debug, Display, Formatter},
};
use std::fmt::Debug;
use hex;
use reqwest::{Client as RequestClient, Error as RequestError, StatusCode, Url, UrlError};
@ -14,6 +11,7 @@ use serde::{
ser::{Serialize, Serializer},
};
use app_errors::{AppError, AppErrorKind, AppResult};
use settings::Settings;
#[cfg(test)]
@ -63,7 +61,7 @@ impl Serialize for BounceType {
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BounceSubtype {
// Set by the auth db if an input string is not recognised
Unmapped,
@ -141,7 +139,7 @@ impl Serialize for BounceSubtype {
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
pub struct BounceRecord {
#[serde(rename = "email")]
pub address: String,
@ -153,38 +151,15 @@ pub struct BounceRecord {
pub created_at: u64,
}
#[derive(Debug)]
pub struct DbError {
description: String,
}
impl DbError {
pub fn new(description: String) -> DbError {
DbError { description }
impl From<UrlError> for AppError {
fn from(error: UrlError) -> AppError {
AppErrorKind::DbError(format!("{}", error)).into()
}
}
impl Error for DbError {
fn description(&self) -> &str {
&self.description
}
}
impl Display for DbError {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "{}", self.description)
}
}
impl From<UrlError> for DbError {
fn from(error: UrlError) -> DbError {
DbError::new(format!("URL error: {:?}", error))
}
}
impl From<RequestError> for DbError {
fn from(error: RequestError) -> DbError {
DbError::new(format!("request error: {:?}", error))
impl From<RequestError> for AppError {
fn from(error: RequestError) -> AppError {
AppErrorKind::DbError(format!("{}", error)).into()
}
}
@ -213,15 +188,15 @@ impl DbUrls {
}
pub trait Db: Debug + Sync {
fn get_bounces(&self, address: &str) -> Result<Vec<BounceRecord>, DbError>;
fn get_bounces(&self, address: &str) -> AppResult<Vec<BounceRecord>>;
fn create_bounce(
&self,
_address: &str,
_bounce_type: BounceType,
_bounce_subtype: BounceSubtype,
) -> Result<(), DbError> {
Err(DbError::new(String::from("Not implemented")))
) -> AppResult<()> {
Err(AppErrorKind::NotImplemented.into())
}
}
@ -241,14 +216,14 @@ impl DbClient {
}
impl Db for DbClient {
fn get_bounces(&self, address: &str) -> Result<Vec<BounceRecord>, DbError> {
fn get_bounces(&self, address: &str) -> AppResult<Vec<BounceRecord>> {
let mut response = self
.request_client
.get(self.urls.get_bounces(address)?)
.send()?;
match response.status() {
StatusCode::Ok => response.json::<Vec<BounceRecord>>().map_err(From::from),
status => Err(DbError::new(format!("auth db response: {}", status))),
status => Err(AppErrorKind::DbError(format!("{}", status)).into()),
}
}
@ -257,7 +232,7 @@ impl Db for DbClient {
address: &str,
bounce_type: BounceType,
bounce_subtype: BounceSubtype,
) -> Result<(), DbError> {
) -> AppResult<()> {
let response = self
.request_client
.post(self.urls.create_bounce())
@ -270,7 +245,7 @@ impl Db for DbClient {
.send()?;
match response.status() {
StatusCode::Ok => Ok(()),
status => Err(DbError::new(format!("auth db response: {}", status))),
status => Err(AppErrorKind::DbError(format!("{}", status)).into()),
}
}
}

Просмотреть файл

@ -24,11 +24,17 @@ fn deserialize_bounce_type() {
fn deserialize_invalid_bounce_type() {
match serde_json::from_value::<BounceType>(From::from(4)) {
Ok(_) => assert!(false, "serde_json::from_value should have failed"),
Err(error) => assert_eq!(error.description(), "JSON error"),
Err(error) => assert_eq!(
format!("{}", error),
"invalid value: integer `4`, expected bounce type"
),
}
match serde_json::from_value::<BounceType>(From::from(-1)) {
Ok(_) => assert!(false, "serde_json::from_value should have failed"),
Err(error) => assert_eq!(error.description(), "JSON error"),
Err(error) => assert_eq!(
format!("{}", error),
"invalid value: integer `-1`, expected u8"
),
}
}
@ -80,11 +86,17 @@ fn deserialize_bounce_subtype() {
fn deserialize_invalid_bounce_subtype() {
match serde_json::from_value::<BounceSubtype>(From::from(15)) {
Ok(_) => assert!(false, "serde_json::from_value should have failed"),
Err(error) => assert_eq!(error.description(), "JSON error"),
Err(error) => assert_eq!(
format!("{}", error),
"invalid value: integer `15`, expected bounce subtype"
),
}
match serde_json::from_value::<BounceSubtype>(From::from(-1)) {
Ok(_) => assert!(false, "serde_json::from_value should have failed"),
Err(error) => assert_eq!(error.description(), "JSON error"),
Err(error) => assert_eq!(
format!("{}", error),
"invalid value: integer `-1`, expected u8"
),
}
}
@ -127,7 +139,7 @@ fn get_bounces() {
let settings = Settings::new().expect("config error");
let db = DbClient::new(&settings);
if let Err(error) = db.get_bounces("foo@example.com") {
assert!(false, error.description().to_string());
assert!(false, format!("{}", error));
}
}
@ -137,7 +149,7 @@ fn get_bounces_invalid_address() {
let db = DbClient::new(&settings);
match db.get_bounces("") {
Ok(_) => assert!(false, "DbClient::get_bounces should have failed"),
Err(error) => assert_eq!(error.description(), "auth db response: 400 Bad Request"),
Err(error) => assert_eq!(format!("{}", error), "\"400 Bad Request\""),
}
}

Просмотреть файл

@ -7,7 +7,7 @@
extern crate fxa_email_service;
extern crate rocket;
#[macro_use(slog_b, slog_error, slog_info, slog_kv, slog_log, slog_record, slog_record_static)]
#[macro_use(slog_b, slog_info, slog_kv, slog_log, slog_record, slog_record_static)]
extern crate slog;
use fxa_email_service::{
@ -48,9 +48,6 @@ fn main() {
if response.status().code == 200 {
slog_info!(log, "{}", "Request finished succesfully.";
"status_code" => response.status().code, "status_msg" => response.status().reason);
} else {
slog_error!(log, "{}", "Request errored.";
"status_code" => response.status().code, "status_msg" => response.status().reason);
}
}))
.launch();

Просмотреть файл

@ -2,84 +2,16 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
use std::{
collections::HashMap,
error::Error,
fmt::{self, Display, Formatter},
time::SystemTime,
};
use std::{collections::HashMap, time::SystemTime};
use rocket::{http::Status, response::Failure};
use auth_db::{
BounceRecord, BounceSubtype as DbBounceSubtype, BounceType as DbBounceType, Db, DbError,
};
use app_errors::{AppErrorKind, AppResult};
use auth_db::{BounceSubtype as DbBounceSubtype, BounceType as DbBounceType, Db};
use queues::notification::{BounceSubtype, BounceType, ComplaintFeedbackType};
use settings::{BounceLimit, BounceLimits, Settings};
#[cfg(test)]
mod test;
#[derive(Debug)]
pub struct BounceError {
pub address: String,
pub bounce: Option<BounceRecord>,
description: String,
}
impl BounceError {
pub fn new(address: &str, bounce: &BounceRecord) -> BounceError {
let description = format!(
"email address violated {} limit",
match bounce.bounce_type {
DbBounceType::Hard => "hard bounce",
DbBounceType::Soft => "soft bounce",
DbBounceType::Complaint => "complaint",
}
);
BounceError {
address: address.to_string(),
bounce: Some(bounce.clone()),
description,
}
}
}
impl Error for BounceError {
fn description(&self) -> &str {
&self.description
}
}
impl Display for BounceError {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "{}", self.description)
}
}
impl From<DbError> for BounceError {
fn from(error: DbError) -> BounceError {
BounceError {
address: String::from(""),
bounce: None,
description: format!("database error: {}", error.description()),
}
}
}
impl From<BounceError> for Failure {
fn from(error: BounceError) -> Failure {
// Eventually we should be able to do something richer than this,
// as per https://github.com/SergioBenitez/Rocket/issues/586.
Failure(
error
.bounce
.map_or(Status::InternalServerError, |_| Status::TooManyRequests),
)
}
}
#[derive(Debug)]
pub struct Bounces<D: Db> {
db: D,
@ -97,7 +29,7 @@ where
}
}
pub fn check(&self, address: &str) -> Result<(), BounceError> {
pub fn check(&self, address: &str) -> AppResult<()> {
let bounces = self.db.get_bounces(address)?;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
@ -115,7 +47,20 @@ where
DbBounceType::Complaint => &self.limits.complaint,
};
if is_bounce_violation(*count, bounce.created_at, now, limits) {
return Err(BounceError::new(address, bounce));
return match bounce.bounce_type {
DbBounceType::Hard => Err(AppErrorKind::BounceHardError {
_address: address.to_string(),
_bounce: Some(bounce.clone()),
}.into()),
DbBounceType::Soft => Err(AppErrorKind::BounceSoftError {
_address: address.to_string(),
_bounce: Some(bounce.clone()),
}.into()),
DbBounceType::Complaint => Err(AppErrorKind::BounceComplaintError {
_address: address.to_string(),
_bounce: Some(bounce.clone()),
}.into()),
};
}
}
@ -129,7 +74,7 @@ where
address: &str,
bounce_type: BounceType,
bounce_subtype: BounceSubtype,
) -> Result<(), BounceError> {
) -> AppResult<()> {
self.db
.create_bounce(address, From::from(bounce_type), From::from(bounce_subtype))?;
Ok(())
@ -139,7 +84,7 @@ where
&self,
address: &str,
complaint_type: Option<ComplaintFeedbackType>,
) -> Result<(), BounceError> {
) -> AppResult<()> {
let bounce_subtype = complaint_type.map_or(DbBounceSubtype::Unmapped, |ct| From::from(ct));
self.db
.create_bounce(address, DbBounceType::Complaint, bounce_subtype)?;

Просмотреть файл

@ -6,8 +6,9 @@ use rocket::http::Status;
use serde_json::{self, Value as Json};
use super::*;
use app_errors::{AppErrorKind, AppResult};
use auth_db::{
BounceSubtype as DbBounceSubtype, BounceType as DbBounceType, Db, DbClient, DbError,
BounceRecord, BounceSubtype as DbBounceSubtype, BounceType as DbBounceType, Db, DbClient,
};
use queues::notification::{BounceSubtype, BounceType, ComplaintFeedbackType};
use settings::Settings;
@ -36,7 +37,7 @@ fn check_no_bounces() {
let db = DbMockNoBounce;
let bounces = Bounces::new(&settings, db);
if let Err(error) = bounces.check("foo@example.com") {
assert!(false, error.description().to_string());
assert!(false, format!("{}", error));
}
}
@ -50,7 +51,7 @@ fn create_settings(bounce_limits: Json) -> Settings {
pub struct DbMockNoBounce;
impl Db for DbMockNoBounce {
fn get_bounces(&self, _address: &str) -> Result<Vec<BounceRecord>, DbError> {
fn get_bounces(&self, _address: &str) -> AppResult<Vec<BounceRecord>> {
let now = now_as_milliseconds();
Ok(vec![
BounceRecord {
@ -97,18 +98,22 @@ fn check_soft_bounce() {
match bounces.check("foo@example.com") {
Ok(_) => assert!(false, "Bounces::check should have failed"),
Err(error) => {
assert_eq!(
error.description(),
"email address violated soft bounce limit"
);
assert_eq!(error.address, "foo@example.com");
if let Some(ref bounce) = error.bounce {
assert_eq!(bounce.bounce_type, DbBounceType::Soft);
assert_eq!(format!("{}", error), "Email account soft bounced.");
let err_data = error.kind().additional_fields();
let address = err_data.get("address");
if let Some(ref address) = address {
assert_eq!("foo@example.com", address.as_str().unwrap());
} else {
assert!(false, "Error::address should be set");
}
let bounce = err_data.get("bounce");
if let Some(ref bounce) = bounce {
let record: Json = serde_json::from_str(bounce.as_str().unwrap()).unwrap();
assert_eq!(record["bounceType"], 2);
} else {
assert!(false, "Error::bounce should be set");
}
let failure: Failure = From::from(error);
assert_eq!(failure.0, Status::TooManyRequests);
assert_eq!(error.kind().http_status(), Status::TooManyRequests);
}
}
}
@ -117,7 +122,7 @@ fn check_soft_bounce() {
pub struct DbMockBounceSoft;
impl Db for DbMockBounceSoft {
fn get_bounces(&self, _address: &str) -> Result<Vec<BounceRecord>, DbError> {
fn get_bounces(&self, _address: &str) -> AppResult<Vec<BounceRecord>> {
let now = now_as_milliseconds();
Ok(vec![BounceRecord {
address: String::from("foo@example.com"),
@ -143,18 +148,22 @@ fn check_hard_bounce() {
match bounces.check("bar@example.com") {
Ok(_) => assert!(false, "Bounces::check should have failed"),
Err(error) => {
assert_eq!(
error.description(),
"email address violated hard bounce limit"
);
assert_eq!(error.address, "bar@example.com");
if let Some(ref bounce) = error.bounce {
assert_eq!(bounce.bounce_type, DbBounceType::Hard);
assert_eq!(format!("{}", error), "Email account hard bounced.");
let err_data = error.kind().additional_fields();
let address = err_data.get("address");
if let Some(ref address) = address {
assert_eq!("bar@example.com", address.as_str().unwrap());
} else {
assert!(false, "Error::address should be set");
}
let bounce = err_data.get("bounce");
if let Some(ref bounce) = bounce {
let record: Json = serde_json::from_str(bounce.as_str().unwrap()).unwrap();
assert_eq!(record["bounceType"], 1);
} else {
assert!(false, "Error::bounce should be set");
}
let failure: Failure = From::from(error);
assert_eq!(failure.0, Status::TooManyRequests);
assert_eq!(error.kind().http_status(), Status::TooManyRequests);
}
}
}
@ -163,7 +172,7 @@ fn check_hard_bounce() {
pub struct DbMockBounceHard;
impl Db for DbMockBounceHard {
fn get_bounces(&self, _address: &str) -> Result<Vec<BounceRecord>, DbError> {
fn get_bounces(&self, _address: &str) -> AppResult<Vec<BounceRecord>> {
let now = now_as_milliseconds();
Ok(vec![BounceRecord {
address: String::from("bar@example.com"),
@ -189,18 +198,22 @@ fn check_complaint() {
match bounces.check("baz@example.com") {
Ok(_) => assert!(false, "Bounces::check should have failed"),
Err(error) => {
assert_eq!(
error.description(),
"email address violated complaint limit"
);
assert_eq!(error.address, "baz@example.com");
if let Some(ref bounce) = error.bounce {
assert_eq!(bounce.bounce_type, DbBounceType::Complaint);
assert_eq!(format!("{}", error), "Email account sent complaint.");
let err_data = error.kind().additional_fields();
let address = err_data.get("address");
if let Some(ref address) = address {
assert_eq!("baz@example.com", address.as_str().unwrap());
} else {
assert!(false, "Error::address should be set");
}
let bounce = err_data.get("bounce");
if let Some(ref bounce) = bounce {
let record: Json = serde_json::from_str(bounce.as_str().unwrap()).unwrap();
assert_eq!(record["bounceType"], 3);
} else {
assert!(false, "Error::bounce should be set");
}
let failure: Failure = From::from(error);
assert_eq!(failure.0, Status::TooManyRequests);
assert_eq!(error.kind().http_status(), Status::TooManyRequests);
}
}
}
@ -209,7 +222,7 @@ fn check_complaint() {
pub struct DbMockComplaint;
impl Db for DbMockComplaint {
fn get_bounces(&self, _address: &str) -> Result<Vec<BounceRecord>, DbError> {
fn get_bounces(&self, _address: &str) -> AppResult<Vec<BounceRecord>> {
let now = now_as_milliseconds();
Ok(vec![BounceRecord {
address: String::from("baz@example.com"),
@ -239,13 +252,8 @@ fn check_db_error() {
match bounces.check("foo@example.com") {
Ok(_) => assert!(false, "Bounces::check should have failed"),
Err(error) => {
assert_eq!(error.description(), "database error: wibble blee");
assert_eq!(error.address, "");
if let Some(_) = error.bounce {
assert!(false, "Error::bounce should not be set");
}
let failure: Failure = From::from(error);
assert_eq!(failure.0, Status::InternalServerError);
assert_eq!(format!("{}", error), "\"wibble blee\"");
assert_eq!(error.kind().http_status(), Status::InternalServerError);
}
}
}
@ -254,8 +262,8 @@ fn check_db_error() {
pub struct DbMockError;
impl Db for DbMockError {
fn get_bounces(&self, _address: &str) -> Result<Vec<BounceRecord>, DbError> {
Err(DbError::new(String::from("wibble blee")))
fn get_bounces(&self, _address: &str) -> AppResult<Vec<BounceRecord>> {
Err(AppErrorKind::DbError(String::from("wibble blee")).into())
}
}
@ -276,7 +284,7 @@ fn check_no_bounces_with_nonzero_limits() {
let db = DbMockNoBounceWithNonZeroLimits;
let bounces = Bounces::new(&settings, db);
if let Err(error) = bounces.check("foo@example.com") {
assert!(false, error.description().to_string());
assert!(false, format!("{}", error));
}
}
@ -284,7 +292,7 @@ fn check_no_bounces_with_nonzero_limits() {
pub struct DbMockNoBounceWithNonZeroLimits;
impl Db for DbMockNoBounceWithNonZeroLimits {
fn get_bounces(&self, _address: &str) -> Result<Vec<BounceRecord>, DbError> {
fn get_bounces(&self, _address: &str) -> AppResult<Vec<BounceRecord>> {
let now = now_as_milliseconds();
Ok(vec![
BounceRecord {
@ -362,13 +370,18 @@ fn check_bounce_with_multiple_limits() {
match bounces.check("foo@example.com") {
Ok(_) => assert!(false, "Bounces::check should have failed"),
Err(error) => {
assert_eq!(
error.description(),
"email address violated soft bounce limit"
);
assert_eq!(error.address, "foo@example.com");
if let Some(bounce) = error.bounce {
assert_eq!(bounce.bounce_type, DbBounceType::Soft);
assert_eq!(format!("{}", error), "Email account soft bounced.");
let err_data = error.kind().additional_fields();
let address = err_data.get("address");
if let Some(ref address) = address {
assert_eq!("foo@example.com", address.as_str().unwrap());
} else {
assert!(false, "Error::address should be set");
}
let bounce = err_data.get("bounce");
if let Some(ref bounce) = bounce {
let record: Json = serde_json::from_str(bounce.as_str().unwrap()).unwrap();
assert_eq!(record["bounceType"], 2);
} else {
assert!(false, "Error::bounce should be set");
}
@ -380,7 +393,7 @@ fn check_bounce_with_multiple_limits() {
pub struct DbMockBounceWithMultipleLimits;
impl Db for DbMockBounceWithMultipleLimits {
fn get_bounces(&self, _address: &str) -> Result<Vec<BounceRecord>, DbError> {
fn get_bounces(&self, _address: &str) -> AppResult<Vec<BounceRecord>> {
let now = now_as_milliseconds();
Ok(vec![
BounceRecord {

Просмотреть файл

@ -12,6 +12,7 @@ extern crate base64;
extern crate chrono;
extern crate config;
extern crate emailmessage;
#[macro_use]
extern crate failure;
extern crate futures;
extern crate hex;
@ -38,7 +39,9 @@ extern crate serde_derive;
extern crate serde_json;
extern crate serde_test;
extern crate sha2;
#[macro_use(slog_b, slog_info, slog_kv, slog_log, slog_o, slog_record, slog_record_static)]
#[macro_use(
slog_b, slog_error, slog_info, slog_kv, slog_log, slog_o, slog_record, slog_record_static
)]
extern crate slog;
extern crate slog_async;
extern crate slog_mozlog_json;

Просмотреть файл

@ -2,7 +2,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
use super::{Headers, Provider, ProviderError};
use super::{Headers, Provider};
use app_errors::AppResult;
pub struct MockProvider;
@ -15,7 +16,7 @@ impl Provider for MockProvider {
_subject: &str,
_body_text: &str,
_body_html: Option<&str>,
) -> Result<String, ProviderError> {
) -> AppResult<String> {
Ok(String::from("deadbeef"))
}
}

Просмотреть файл

@ -2,16 +2,12 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
use std::{
boxed::Box,
collections::HashMap,
error::Error,
fmt::{self, Display, Formatter},
};
use std::{boxed::Box, collections::HashMap};
use self::{
mock::MockProvider as Mock, sendgrid::SendgridProvider as Sendgrid, ses::SesProvider as Ses,
};
use app_errors::{AppErrorKind, AppResult};
use settings::Settings;
mod mock;
@ -29,36 +25,7 @@ trait Provider {
subject: &str,
body_text: &str,
body_html: Option<&str>,
) -> Result<String, ProviderError>;
}
#[derive(Debug)]
pub struct ProviderError {
description: String,
}
impl ProviderError {
pub fn new(description: String) -> ProviderError {
ProviderError { description }
}
}
impl From<String> for ProviderError {
fn from(error: String) -> Self {
ProviderError::new(error)
}
}
impl Error for ProviderError {
fn description(&self) -> &str {
&self.description
}
}
impl Display for ProviderError {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "{}", self.description)
}
) -> AppResult<String>;
}
pub struct Providers {
@ -95,14 +62,12 @@ impl Providers {
body_text: &str,
body_html: Option<&str>,
provider_id: Option<&str>,
) -> Result<String, ProviderError> {
) -> AppResult<String> {
let resolved_provider_id = provider_id.unwrap_or(&self.default_provider);
self.providers
.get(resolved_provider_id)
.ok_or_else(|| {
ProviderError::new(format!("Invalid provider `{}`", resolved_provider_id))
})
.ok_or_else(|| AppErrorKind::InvalidProvider(String::from(resolved_provider_id)).into())
.and_then(|provider| provider.send(to, cc, headers, subject, body_text, body_html))
.map(|message_id| format!("{}:{}", resolved_provider_id, message_id))
}

Просмотреть файл

@ -12,7 +12,8 @@ use sendgrid::{
},
};
use super::{Headers, Provider, ProviderError};
use super::{Headers, Provider};
use app_errors::{AppError, AppErrorKind, AppResult};
use settings::{Sender, Sendgrid as SendgridSettings, Settings};
pub struct SendgridProvider {
@ -38,7 +39,7 @@ impl Provider for SendgridProvider {
subject: &str,
body_text: &str,
body_html: Option<&str>,
) -> Result<String, ProviderError> {
) -> AppResult<String> {
let mut message = Message::new();
let mut from_address = EmailAddress::new();
from_address.set_email(&self.sender.address);
@ -82,34 +83,40 @@ impl Provider for SendgridProvider {
.headers()
.get_raw("X-Message-Id")
.and_then(|raw_header| raw_header.one())
.ok_or(ProviderError {
description: String::from(
"Missing or duplicate X-Message-Id header in Sendgrid response",
),
})
.ok_or(
AppErrorKind::ProviderError {
_name: String::from("Sendgrid"),
_description: String::from(
"Missing or duplicate X-Message-Id header in Sendgrid response",
),
}.into(),
)
.and_then(|message_id| from_utf8(message_id).map_err(From::from))
.map(|message_id| message_id.to_string())
} else {
Err(ProviderError {
description: format!("Sendgrid response: {}", status),
})
Err(AppErrorKind::ProviderError {
_name: String::from("Sendgrid"),
_description: format!("Unsuccesful response status: {}", status),
}.into())
}
})
}
}
impl From<SendgridError> for ProviderError {
fn from(error: SendgridError) -> ProviderError {
ProviderError {
description: format!("Sendgrid error: {:?}", error),
}
impl From<SendgridError> for AppError {
fn from(error: SendgridError) -> AppError {
AppErrorKind::ProviderError {
_name: String::from("Sendgrid"),
_description: format!("{:?}", error),
}.into()
}
}
impl From<Utf8Error> for ProviderError {
fn from(error: Utf8Error) -> ProviderError {
ProviderError {
description: format!("Failed to decode string as UTF-8: {:?}", error),
}
impl From<Utf8Error> for AppError {
fn from(error: Utf8Error) -> AppError {
AppErrorKind::ProviderError {
_name: String::from("Sendgrid"),
_description: format!("Failed to decode string as UTF-8: {:?}", error),
}.into()
}
}

Просмотреть файл

@ -10,7 +10,8 @@ use rusoto_core::{reactor::RequestDispatcher, Region};
use rusoto_credential::StaticProvider;
use rusoto_ses::{RawMessage, SendRawEmailError, SendRawEmailRequest, Ses, SesClient};
use super::{Headers, Provider, ProviderError};
use super::{Headers, Provider};
use app_errors::{AppError, AppErrorKind, AppResult};
use settings::Settings;
pub struct SesProvider {
@ -50,7 +51,7 @@ impl Provider for SesProvider {
subject: &str,
body_text: &str,
body_html: Option<&str>,
) -> Result<String, ProviderError> {
) -> AppResult<String> {
let mut all_headers = header::Headers::new();
all_headers.set(header::From(vec![self.sender.parse::<Mailbox>()?]));
all_headers.set(header::To(vec![to.parse::<Mailbox>()?]));
@ -108,10 +109,17 @@ impl Provider for SesProvider {
}
}
impl From<SendRawEmailError> for ProviderError {
fn from(error: SendRawEmailError) -> ProviderError {
ProviderError {
description: format!("SES error: {:?}", error),
}
impl From<String> for AppError {
fn from(error: String) -> Self {
AppErrorKind::EmailParsingError(format!("{:?}", error)).into()
}
}
impl From<SendRawEmailError> for AppError {
fn from(error: SendRawEmailError) -> AppError {
AppErrorKind::ProviderError {
_name: String::from("SES"),
_description: format!("{:?}", error),
}.into()
}
}

Просмотреть файл

@ -12,8 +12,9 @@ use futures::future::{self, Future};
use self::notification::{Notification, NotificationType};
pub use self::sqs::Queue as Sqs;
use app_errors::AppError;
use auth_db::DbClient;
use bounces::{BounceError, Bounces};
use bounces::Bounces;
use message_data::MessageData;
use settings::Settings;
@ -203,8 +204,8 @@ impl Display for QueueError {
}
}
impl From<BounceError> for QueueError {
fn from(error: BounceError) -> QueueError {
impl From<AppError> for QueueError {
fn from(error: AppError) -> QueueError {
QueueError::new(format!("bounce error: {:?}", error))
}
}

Просмотреть файл

@ -5,11 +5,11 @@
use rocket::{
data::{self, FromData},
http::Status,
response::Failure,
Data, Outcome, Request, State,
};
use rocket_contrib::{Json, Value};
use app_errors::{AppError, AppErrorKind, AppResult};
use auth_db::DbClient;
use bounces::Bounces;
use deserialize;
@ -39,7 +39,7 @@ struct Email {
}
impl FromData for Email {
type Error = ();
type Error = AppError;
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
let result = Json::<Email>::from_data(request, data);
@ -49,10 +49,13 @@ impl FromData for Email {
if validate(&email) {
Outcome::Success(email)
} else {
fail()
Outcome::Failure((Status::BadRequest, AppErrorKind::InvalidEmailParams.into()))
}
}
Outcome::Failure(_error) => fail(),
Outcome::Failure((_status, error)) => Outcome::Failure((
Status::BadRequest,
AppErrorKind::MissingEmailParams(error.to_string()).into(),
)),
Outcome::Forward(forward) => Outcome::Forward(forward),
}
}
@ -76,17 +79,15 @@ fn validate(email: &Email) -> bool {
true
}
fn fail() -> data::Outcome<Email, ()> {
Outcome::Failure((Status::BadRequest, ()))
}
#[post("/send", format = "application/json", data = "<email>")]
fn handler(
email: Email,
email: AppResult<Email>,
bounces: State<Bounces<DbClient>>,
message_data: State<MessageData>,
providers: State<Providers>,
) -> Result<Json<Value>, Failure> {
) -> AppResult<Json<Value>> {
let email = email?;
let to = email.to.as_ref();
bounces.check(to)?;
@ -119,8 +120,5 @@ fn handler(
.map(|error| println!("{}", error));
Json(json!({ "messageId": message_id }))
})
.map_err(|error| {
println!("{}", error);
Failure(Status::InternalServerError)
})
.map_err(|error| error)
}

Просмотреть файл

@ -7,11 +7,11 @@ use rocket::{
http::{ContentType, Status},
local::Client,
};
use serde_json;
use app_errors::{self, ApplicationError};
use app_errors::{self, AppError, AppErrorKind};
use auth_db::DbClient;
use bounces::Bounces;
use logging::MozlogLogger;
use message_data::MessageData;
use providers::Providers;
use settings::Settings;
@ -20,10 +20,12 @@ fn setup() -> Client {
let settings = Settings::new().unwrap();
let db = DbClient::new(&settings);
let bounces = Bounces::new(&settings, db);
let logger = MozlogLogger::new(&settings).expect("MozlogLogger::init error");
let message_data = MessageData::new(&settings);
let providers = Providers::new(&settings);
let server = rocket::ignite()
.manage(bounces)
.manage(logger)
.manage(message_data)
.manage(providers)
.mount("/", routes![super::handler])
@ -139,8 +141,8 @@ fn missing_to_field() {
assert_eq!(response.status(), Status::BadRequest);
let body = response.body().unwrap().into_string().unwrap();
let error: ApplicationError = serde_json::from_str(&body).unwrap();
assert_eq!(error, ApplicationError::new(400, "Bad Request"));
let error: AppError = AppErrorKind::MissingEmailParams(String::from("")).into();
assert_eq!(body, error.json().to_string());
}
#[test]
@ -164,8 +166,8 @@ fn missing_subject_field() {
assert_eq!(response.status(), Status::BadRequest);
let body = response.body().unwrap().into_string().unwrap();
let error: ApplicationError = serde_json::from_str(&body).unwrap();
assert_eq!(error, ApplicationError::new(400, "Bad Request"));
let error: AppError = AppErrorKind::MissingEmailParams(String::from("")).into();
assert_eq!(body, error.json().to_string());
}
#[test]
@ -190,8 +192,8 @@ fn missing_body_text_field() {
assert_eq!(response.status(), Status::BadRequest);
let body = response.body().unwrap().into_string().unwrap();
let error: ApplicationError = serde_json::from_str(&body).unwrap();
assert_eq!(error, ApplicationError::new(400, "Bad Request"));
let error: AppError = AppErrorKind::MissingEmailParams(String::from("")).into();
assert_eq!(body, error.json().to_string());
}
#[test]
@ -216,8 +218,8 @@ fn invalid_to_field() {
assert_eq!(response.status(), Status::BadRequest);
let body = response.body().unwrap().into_string().unwrap();
let error: ApplicationError = serde_json::from_str(&body).unwrap();
assert_eq!(error, ApplicationError::new(400, "Bad Request"));
let error: AppError = AppErrorKind::MissingEmailParams(String::from("")).into();
assert_eq!(body, error.json().to_string());
}
#[test]
@ -243,6 +245,6 @@ fn invalid_cc_field() {
assert_eq!(response.status(), Status::BadRequest);
let body = response.body().unwrap().into_string().unwrap();
let error: ApplicationError = serde_json::from_str(&body).unwrap();
assert_eq!(error, ApplicationError::new(400, "Bad Request"));
let error: AppError = AppErrorKind::MissingEmailParams(String::from("")).into();
assert_eq!(body, error.json().to_string());
}