diff --git a/services/reports/src/http.rs b/services/reports/src/http.rs --- a/services/reports/src/http.rs +++ b/services/reports/src/http.rs @@ -1,10 +1,15 @@ -use actix_web::{web, App, HttpResponse, HttpServer}; +use actix_web::error::{ + ErrorBadRequest, ErrorInternalServerError, ErrorServiceUnavailable, + ErrorUnsupportedMediaType, +}; +use actix_web::{web, App, HttpResponse, HttpServer, ResponseError}; use anyhow::Result; -use tracing::info; +use http::StatusCode; +use tracing::{debug, error, info, trace, warn}; use crate::config::CONFIG; use crate::constants::REQUEST_BODY_JSON_SIZE_LIMIT; -use crate::service::ReportsService; +use crate::service::{ReportsService, ReportsServiceError}; pub async fn run_http_server(service: ReportsService) -> Result<()> { use actix_web::middleware::{Logger, NormalizePath}; @@ -34,3 +39,61 @@ Ok(()) } + +fn handle_reports_service_error(err: &ReportsServiceError) -> actix_web::Error { + use aws_sdk_dynamodb::Error as DynamoDBError; + use comm_services_lib::database::Error as DBError; + + trace!("Handling reports service error: {:?}", err); + match err { + ReportsServiceError::UnsupportedReportType => { + ErrorUnsupportedMediaType("unsupported report type") + } + ReportsServiceError::SerdeError(err) => { + error!("Serde error: {0:?} - {0}", err); + ErrorInternalServerError("internal error") + } + ReportsServiceError::ParseError(err) => { + debug!("Parse error: {0:?} - {0}", err); + ErrorBadRequest("invalid input format") + } + ReportsServiceError::BlobError(err) => { + error!("Blob Service error: {0:?} - {0}", err); + ErrorInternalServerError("internal error") + } + ReportsServiceError::DatabaseError(db_err) => match db_err { + // retriable errors + DBError::MaxRetriesExceeded + | DBError::AwsSdk( + DynamoDBError::InternalServerError(_) + | DynamoDBError::ProvisionedThroughputExceededException(_) + | DynamoDBError::RequestLimitExceeded(_), + ) => { + warn!("AWS transient error occurred"); + ErrorServiceUnavailable("please retry") + } + err => { + error!("Unexpected database error: {0:?} - {0}", err); + ErrorInternalServerError("internal error") + } + }, + #[allow(unreachable_patterns)] + err => { + error!("Received an unexpected error: {0:?} - {0}", err); + ErrorInternalServerError("server error") + } + } +} + +/// This allow us to `await?` blob service calls in HTTP handlers +impl ResponseError for ReportsServiceError { + fn error_response(&self) -> HttpResponse { + handle_reports_service_error(self).error_response() + } + + fn status_code(&self) -> StatusCode { + handle_reports_service_error(self) + .as_response_error() + .status_code() + } +} diff --git a/services/reports/src/service.rs b/services/reports/src/service.rs --- a/services/reports/src/service.rs +++ b/services/reports/src/service.rs @@ -1,8 +1,36 @@ use actix_web::FromRequest; -use comm_services_lib::{auth::UserIdentity, blob::client::BlobServiceClient}; +use comm_services_lib::{ + auth::UserIdentity, + blob::client::{BlobServiceClient, BlobServiceError}, + database, +}; +use derive_more::{Display, Error, From}; use std::future::{ready, Ready}; use crate::database::client::DatabaseClient; + +#[derive(Debug, Display, Error, From)] +pub enum ReportsServiceError { + DatabaseError(database::Error), + BlobError(BlobServiceError), + /// Error during parsing user input + /// Usually this indicates user error + #[from(ignore)] + ParseError(serde_json::Error), + /// Error during serializing/deserializing internal data + /// This is usually a service bug / data inconsistency + #[from(ignore)] + SerdeError(serde_json::Error), + /// Unsupported report type + /// Returned when trying to perform an operation on an incompatible report type + /// e.g. create a Redux Devtools import from a media mission report + UnsupportedReportType, + /// Unexpected error + Unexpected, +} + +type ServiceResult = Result; + #[derive(Clone)] pub struct ReportsService { db: DatabaseClient,