diff --git a/services/reports/src/http.rs b/services/reports/src/http.rs index abc87eab7..f09d2e2bf 100644 --- a/services/reports/src/http.rs +++ b/services/reports/src/http.rs @@ -1,36 +1,99 @@ -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}; use comm_services_lib::http::cors_config; use tracing_actix_web::TracingLogger; info!( "Starting HTTP server listening at port {}", CONFIG.http_port ); HttpServer::new(move || { let json_cfg = web::JsonConfig::default().limit(REQUEST_BODY_JSON_SIZE_LIMIT); App::new() .app_data(json_cfg) .app_data(service.to_owned()) .wrap(Logger::default()) .wrap(TracingLogger::default()) .wrap(NormalizePath::trim()) .wrap(cors_config(CONFIG.is_dev())) // Health endpoint for load balancers checks .route("/health", web::get().to(HttpResponse::Ok)) }) .bind(("0.0.0.0", CONFIG.http_port))? .run() .await?; 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 index 87231784b..32fbff1a8 100644 --- a/services/reports/src/service.rs +++ b/services/reports/src/service.rs @@ -1,64 +1,92 @@ 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, blob_client: BlobServiceClient, requesting_user_id: Option, } impl ReportsService { pub fn new(db: DatabaseClient, blob_client: BlobServiceClient) -> Self { Self { db, blob_client, requesting_user_id: None, } } pub fn authenticated(&self, user: UserIdentity) -> Self { let user_id = user.user_id.to_string(); Self { db: self.db.clone(), blob_client: self.blob_client.with_user_identity(user), requesting_user_id: Some(user_id), } } } impl FromRequest for ReportsService { type Error = actix_web::Error; type Future = Ready>; #[inline] fn from_request( req: &actix_web::HttpRequest, _payload: &mut actix_web::dev::Payload, ) -> Self::Future { use actix_web::HttpMessage; let Some(service) = req.app_data::() else { tracing::error!( "FATAL! Failed to extract ReportsService from actix app_data. \ Check HTTP server configuration" ); return ready(Err(actix_web::error::ErrorInternalServerError("Internal server error"))); }; let auth_service = if let Some(user_identity) = req.extensions().get::() { tracing::trace!("Found user identity. Creating authenticated service"); service.authenticated(user_identity.clone()) } else { tracing::trace!( "No user identity found. Leaving unauthenticated service" ); service.clone() }; ready(Ok(auth_service)) } }