From 9f5e1ede9b2036b2217d27f4eeb297c472ed55cd Mon Sep 17 00:00:00 2001 From: Ruediger Birkner <ruediger.birkner@dfinity.org> Date: Tue, 19 Nov 2024 15:21:33 +0100 Subject: [PATCH 1/5] client-facing errors --- src/routing/error_cause.rs | 193 ++++++++++++++++++++++--------------- src/routing/mod.rs | 2 +- src/routing/proxy.rs | 6 +- 3 files changed, 118 insertions(+), 83 deletions(-) diff --git a/src/routing/error_cause.rs b/src/routing/error_cause.rs index 18c0b59..f917f30 100644 --- a/src/routing/error_cause.rs +++ b/src/routing/error_cause.rs @@ -1,14 +1,11 @@ -use std::{ - error::Error as StdError, - fmt::{self}, -}; +use std::error::Error as StdError; use axum::response::{IntoResponse, Response}; use hickory_resolver::error::ResolveError; use http::{header::CONTENT_TYPE, StatusCode}; use ic_agent::AgentError; use ic_bn_lib::http::{headers::CONTENT_TYPE_HTML, Error as IcBnError}; -use strum_macros::{Display, IntoStaticStr}; +use strum::{Display, IntoStaticStr}; // Process error chain trying to find given error type pub fn error_infer<E: StdError + Send + Sync + 'static>(error: &anyhow::Error) -> Option<&E> { @@ -28,7 +25,8 @@ pub enum RateLimitCause { // Categorized possible causes for request processing failures // Not using Error as inner type since it's not cloneable -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Display, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] pub enum ErrorCause { ClientBodyTooLarge, ClientBodyTimeout, @@ -50,38 +48,13 @@ pub enum ErrorCause { BackendBodyError(String), BackendTLSErrorOther(String), BackendTLSErrorCert(String), + #[strum(serialize = "rate_limited_{0}")] RateLimited(RateLimitCause), + #[strum(serialize = "internal_server_error")] Other(String), } impl ErrorCause { - pub const fn status_code(&self) -> StatusCode { - match self { - Self::Other(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::ClientBodyTooLarge => StatusCode::PAYLOAD_TOO_LARGE, - Self::ClientBodyTimeout => StatusCode::REQUEST_TIMEOUT, - Self::ClientBodyError(_) => StatusCode::BAD_REQUEST, - Self::LoadShed => StatusCode::TOO_MANY_REQUESTS, - Self::IncorrectPrincipal => StatusCode::BAD_REQUEST, - Self::MalformedRequest(_) => StatusCode::BAD_REQUEST, - Self::NoAuthority => StatusCode::BAD_REQUEST, - Self::UnknownDomain => StatusCode::BAD_REQUEST, - Self::CanisterIdNotFound => StatusCode::BAD_REQUEST, - Self::AgentError(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::DomainCanisterMismatch => StatusCode::FORBIDDEN, - Self::Denylisted => StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS, - Self::BackendError(_) => StatusCode::SERVICE_UNAVAILABLE, - Self::BackendErrorDNS(_) => StatusCode::SERVICE_UNAVAILABLE, - Self::BackendErrorConnect => StatusCode::SERVICE_UNAVAILABLE, - Self::BackendTimeout => StatusCode::INTERNAL_SERVER_ERROR, - Self::BackendBodyTimeout => StatusCode::INTERNAL_SERVER_ERROR, - Self::BackendBodyError(_) => StatusCode::SERVICE_UNAVAILABLE, - Self::BackendTLSErrorOther(_) => StatusCode::SERVICE_UNAVAILABLE, - Self::BackendTLSErrorCert(_) => StatusCode::SERVICE_UNAVAILABLE, - Self::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS, - } - } - pub fn details(&self) -> Option<String> { match self { Self::Other(x) => Some(x.clone()), @@ -99,13 +72,6 @@ impl ErrorCause { } } - pub const fn html(&self) -> Option<&str> { - match self { - Self::Denylisted => Some(include_str!("error_pages/451.html")), - _ => None, - } - } - // Convert from client-side error pub fn from_client_error(e: IcBnError) -> Self { match e { @@ -125,33 +91,31 @@ impl ErrorCause { _ => Self::Other(e.to_string()), } } -} -impl fmt::Display for ErrorCause { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + pub fn to_client_facing_error(&self) -> ErrorClientFacing { match self { - Self::Other(_) => write!(f, "general_error"), - Self::ClientBodyTooLarge => write!(f, "client_body_too_large"), - Self::ClientBodyTimeout => write!(f, "client_body_timeout"), - Self::ClientBodyError(_) => write!(f, "client_body_error"), - Self::LoadShed => write!(f, "load_shed"), - Self::IncorrectPrincipal => write!(f, "incorrect_principal"), - Self::MalformedRequest(_) => write!(f, "malformed_request"), - Self::UnknownDomain => write!(f, "unknown_domain"), - Self::CanisterIdNotFound => write!(f, "canister_id_not_found"), - Self::DomainCanisterMismatch => write!(f, "domain_canister_mismatch"), - Self::Denylisted => write!(f, "denylisted"), - Self::NoAuthority => write!(f, "no_authority"), - Self::AgentError(_) => write!(f, "agent_error"), - Self::BackendError(_) => write!(f, "backend_error"), - Self::BackendErrorDNS(_) => write!(f, "backend_error_dns"), - Self::BackendErrorConnect => write!(f, "backend_error_connect"), - Self::BackendTimeout => write!(f, "backend_timeout"), - Self::BackendBodyTimeout => write!(f, "backend_body_timeout"), - Self::BackendBodyError(_) => write!(f, "backend_body_error"), - Self::BackendTLSErrorOther(_) => write!(f, "backend_tls_error"), - Self::BackendTLSErrorCert(_) => write!(f, "backend_tls_error_cert"), - Self::RateLimited(x) => write!(f, "rate_limited_{x}"), + Self::Other(_) => ErrorClientFacing::Other, + Self::ClientBodyTooLarge => ErrorClientFacing::PayloadTooLarge, + Self::ClientBodyTimeout => ErrorClientFacing::BodyTimedOut, + Self::ClientBodyError(x) => ErrorClientFacing::MalformedRequest(x.clone()), + Self::LoadShed => ErrorClientFacing::LoadShed, + Self::IncorrectPrincipal => ErrorClientFacing::IncorrectPrincipal, + Self::MalformedRequest(x) => ErrorClientFacing::MalformedRequest(x.clone()), + Self::UnknownDomain => ErrorClientFacing::UnknownDomain, + Self::CanisterIdNotFound => ErrorClientFacing::CanisterIdNotFound, + Self::DomainCanisterMismatch => ErrorClientFacing::DomainCanisterMismatch, + Self::Denylisted => ErrorClientFacing::Denylisted, + Self::NoAuthority => ErrorClientFacing::NoAuthority, + Self::AgentError(_) => ErrorClientFacing::UpstreamError, + Self::BackendError(_) => ErrorClientFacing::UpstreamError, + Self::BackendErrorDNS(_) => ErrorClientFacing::UpstreamError, + Self::BackendErrorConnect => ErrorClientFacing::UpstreamError, + Self::BackendTimeout => ErrorClientFacing::UpstreamError, + Self::BackendBodyTimeout => ErrorClientFacing::UpstreamError, + Self::BackendBodyError(_) => ErrorClientFacing::UpstreamError, + Self::BackendTLSErrorOther(_) => ErrorClientFacing::UpstreamError, + Self::BackendTLSErrorCert(_) => ErrorClientFacing::UpstreamError, + Self::RateLimited(_) => ErrorClientFacing::RateLimited, } } } @@ -159,20 +123,8 @@ impl fmt::Display for ErrorCause { // Creates the response from ErrorCause and injects itself into extensions to be visible by middleware impl IntoResponse for ErrorCause { fn into_response(self) -> Response { - // Return the HTML reply if it exists, otherwise textual - let body = self.html().map_or_else( - || { - self.details() - .map_or_else(|| self.to_string(), |x| format!("{self}: {x}\n")) - }, - |x| format!("{x}\n"), - ); - - let mut resp = (self.status_code(), body).into_response(); - if self.html().is_some() { - resp.headers_mut().insert(CONTENT_TYPE, CONTENT_TYPE_HTML); - } - + let client_facing_error = self.to_client_facing_error(); + let mut resp = client_facing_error.into_response(); resp.extensions_mut().insert(self); resp } @@ -234,6 +186,89 @@ impl From<anyhow::Error> for ErrorCause { } } +#[derive(Debug, Clone, Display, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum ErrorClientFacing { + BodyTimedOut, + CanisterIdNotFound, + Denylisted, + DomainCanisterMismatch, + IncorrectPrincipal, + LoadShed, + MalformedRequest(String), + NoAuthority, + #[strum(serialize = "internal_server_error")] + Other, + PayloadTooLarge, + RateLimited, + UnknownDomain, + UpstreamError, +} + +impl ErrorClientFacing { + pub const fn status_code(&self) -> StatusCode { + match self { + Self::BodyTimedOut => StatusCode::REQUEST_TIMEOUT, + Self::CanisterIdNotFound => StatusCode::BAD_REQUEST, + Self::Denylisted => StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS, + Self::DomainCanisterMismatch => StatusCode::BAD_REQUEST, + Self::IncorrectPrincipal => StatusCode::BAD_REQUEST, + Self::LoadShed => StatusCode::TOO_MANY_REQUESTS, + Self::MalformedRequest(_) => StatusCode::BAD_REQUEST, + Self::NoAuthority => StatusCode::BAD_REQUEST, + Self::Other => StatusCode::INTERNAL_SERVER_ERROR, + Self::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE, + Self::RateLimited => StatusCode::TOO_MANY_REQUESTS, + Self::UnknownDomain => StatusCode::BAD_REQUEST, + Self::UpstreamError => StatusCode::SERVICE_UNAVAILABLE, + } + } + + pub fn details(&self) -> String { + match self { + Self::BodyTimedOut => "Reading the request body timed out due to data arriving too slowly.".to_string(), + Self::CanisterIdNotFound => "The canister ID could not be resolved from the provided authority.".to_string(), + Self::Denylisted => "Access to this resource is denied due to a violation of the code of conduct.".to_string(), + Self::DomainCanisterMismatch => "Access to the canister is forbidden through the current gateway domain. Try accessing it through an allowed gateway domain.".to_string(), + Self::IncorrectPrincipal => "The principal in the request is incorrectly formatted.".to_string(), + Self::LoadShed => "The HTTP gateway is temporarily unable to handle the request due to high load. Please try again later.".to_string(), + Self::MalformedRequest(x) => x.to_string(), + Self::NoAuthority => "The request is missing the required authority information (e.g., Host header).".to_string(), + Self::Other => "Internal Server Error".to_string(), + Self::PayloadTooLarge => "The payload is too large.".to_string(), + Self::RateLimited => "Rate limit exceeded. Please slow down requests and try again later.".to_string(), + Self::UnknownDomain => "The requested domain is not served by this HTTP gateway.".to_string(), + Self::UpstreamError => "The HTTP gateway is temporarily unable to process the request. Please try again later.".to_string(), + } + } + + pub const fn html(&self) -> Option<&str> { + match self { + Self::Denylisted => Some(include_str!("error_pages/451.html")), + _ => None, + } + } +} + +// Creates the response from ErrorCause and injects itself into extensions to be visible by middleware +impl IntoResponse for ErrorClientFacing { + fn into_response(self) -> Response { + let error_cause = self.to_string(); + + // Return the HTML reply if it exists, otherwise textual + let body = self.html().map_or_else( + || format!("error: {}\ndetails: {}", error_cause, self.details()), + |x| format!("{x}\n"), + ); + + let mut resp = (self.status_code(), body).into_response(); + if self.html().is_some() { + resp.headers_mut().insert(CONTENT_TYPE, CONTENT_TYPE_HTML); + } + resp + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/routing/mod.rs b/src/routing/mod.rs index 84061f0..21e6cca 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -152,7 +152,7 @@ pub async fn redirect_to_https( .authority(host) .path_and_query(pq) .build() - .map_err(|_| ErrorCause::MalformedRequest("incorrect url".into()))? + .map_err(|_| ErrorCause::MalformedRequest("Incorrect URL".into()))? .to_string(), )) } diff --git a/src/routing/proxy.rs b/src/routing/proxy.rs index 60a9358..402e2e0 100644 --- a/src/routing/proxy.rs +++ b/src/routing/proxy.rs @@ -48,12 +48,12 @@ pub async fn api_proxy( let url = state .route_provider .route() - .map_err(|e| ErrorCause::Other(format!("unable to obtain route: {e:#}")))?; + .map_err(|e| ErrorCause::Other(format!("Unable to obtain route: {e:#}")))?; // Append the query URL to the IC url let url = url .join(original_uri.path()) - .map_err(|e| ErrorCause::MalformedRequest(format!("incorrect URL: {e:#}")))?; + .map_err(|e| ErrorCause::MalformedRequest(format!("Incorrect URL: {e:#}")))?; // Proxy the request let mut response = proxy(url, request, &state.http_client) @@ -111,7 +111,7 @@ pub async fn issuer_proxy( let url = state.issuers[next] .clone() .join(original_uri.path()) - .map_err(|_| ErrorCause::MalformedRequest("unable to parse path as URL part".into()))?; + .map_err(|_| ErrorCause::Other("Unable to parse path as URL part".into()))?; let mut response = proxy(url, request, &state.http_client) .await From 4f263b011a7659f1c5bf632211c2ab8194af9214 Mon Sep 17 00:00:00 2001 From: Ruediger Birkner <ruediger.birkner@dfinity.org> Date: Tue, 19 Nov 2024 18:21:24 +0100 Subject: [PATCH 2/5] fix tests --- src/routing/middleware/rate_limiter.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/routing/middleware/rate_limiter.rs b/src/routing/middleware/rate_limiter.rs index f9ffabb..6b64086 100644 --- a/src/routing/middleware/rate_limiter.rs +++ b/src/routing/middleware/rate_limiter.rs @@ -131,7 +131,7 @@ mod tests { let result = send_request(&mut app).await.unwrap(); assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); - assert_eq!(body, b"rate_limited_normal: normal\n"); + assert!(body.starts_with(b"error: rate_limited\n")); // Wait so that requests can be accepted again. sleep(Duration::from_secs(1)).await; @@ -166,7 +166,7 @@ mod tests { let result = send_request(&mut app).await.unwrap(); assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); - assert_eq!(body, b"rate_limited_normal: normal\n"); + assert!(body.starts_with(b"error: rate_limited\n")); // Wait so that requests can be accepted again. sleep(delay).await; @@ -193,6 +193,9 @@ mod tests { assert_eq!(result.status(), StatusCode::INTERNAL_SERVER_ERROR); let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); - assert_eq!(body, b"general_error: UnableToExtractIpAddress\n"); + assert_eq!( + body, + b"error: internal_server_error\ndetails: Internal Server Error" + ); } } From fb9b2c4b5d8479f3357812b0946618b24231e977 Mon Sep 17 00:00:00 2001 From: Ruediger Birkner <ruediger.birkner@dfinity.org> Date: Mon, 9 Dec 2024 12:01:19 +0100 Subject: [PATCH 3/5] provide HTML error --- src/routing/error_cause.rs | 108 ++++++++++++++++++++++--- src/routing/error_pages/template.html | 107 ++++++++++++++++++++++++ src/routing/ic/handler.rs | 5 +- src/routing/middleware/rate_limiter.rs | 97 ++++++++++++---------- src/routing/middleware/validate.rs | 26 ++++-- 5 files changed, 280 insertions(+), 63 deletions(-) create mode 100644 src/routing/error_pages/template.html diff --git a/src/routing/error_cause.rs b/src/routing/error_cause.rs index f917f30..3a3964b 100644 --- a/src/routing/error_cause.rs +++ b/src/routing/error_cause.rs @@ -1,11 +1,20 @@ use std::error::Error as StdError; +use crate::routing::RequestType; use axum::response::{IntoResponse, Response}; use hickory_resolver::error::ResolveError; use http::{header::CONTENT_TYPE, StatusCode}; use ic_agent::AgentError; use ic_bn_lib::http::{headers::CONTENT_TYPE_HTML, Error as IcBnError}; +use ic_http_gateway::HttpGatewayError; use strum::{Display, IntoStaticStr}; +use tokio::task_local; + +task_local! { + pub static ERROR_CONTEXT: RequestType; +} + +const ERROR_PAGE_TEMPLATE: &str = include_str!("error_pages/template.html"); // Process error chain trying to find given error type pub fn error_infer<E: StdError + Send + Sync + 'static>(error: &anyhow::Error) -> Option<&E> { @@ -52,6 +61,7 @@ pub enum ErrorCause { RateLimited(RateLimitCause), #[strum(serialize = "internal_server_error")] Other(String), + HttpGatewayError(HttpGatewayError), } impl ErrorCause { @@ -68,6 +78,7 @@ impl ErrorCause { Self::BackendTLSErrorCert(x) => Some(x.clone()), Self::AgentError(x) => Some(x.clone()), Self::RateLimited(x) => Some(x.to_string()), + Self::HttpGatewayError(x) => Some(x.to_string()), _ => None, } } @@ -116,6 +127,26 @@ impl ErrorCause { Self::BackendTLSErrorOther(_) => ErrorClientFacing::UpstreamError, Self::BackendTLSErrorCert(_) => ErrorClientFacing::UpstreamError, Self::RateLimited(_) => ErrorClientFacing::RateLimited, + Self::HttpGatewayError(x) => match x { + HttpGatewayError::AgentError(y) => { + let error_string = y.to_string(); + if error_string.contains("no_healthy_nodes") { + return ErrorClientFacing::SubnetUnavailable; + } else if error_string.contains("canister_not_found") { + return ErrorClientFacing::CanisterIdNotFound; + } + ErrorClientFacing::UpstreamError + } + HttpGatewayError::HttpError(y) => { + if y.contains("no_healthy_nodes") { + return ErrorClientFacing::SubnetUnavailable; + } else if y.contains("canister_not_found") { + return ErrorClientFacing::CanisterIdNotFound; + } + ErrorClientFacing::UpstreamError + } + _ => ErrorClientFacing::UpstreamError, + }, } } } @@ -201,6 +232,7 @@ pub enum ErrorClientFacing { Other, PayloadTooLarge, RateLimited, + SubnetUnavailable, UnknownDomain, UpstreamError, } @@ -219,6 +251,7 @@ impl ErrorClientFacing { Self::Other => StatusCode::INTERNAL_SERVER_ERROR, Self::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE, Self::RateLimited => StatusCode::TOO_MANY_REQUESTS, + Self::SubnetUnavailable => StatusCode::SERVICE_UNAVAILABLE, Self::UnknownDomain => StatusCode::BAD_REQUEST, Self::UpstreamError => StatusCode::SERVICE_UNAVAILABLE, } @@ -237,32 +270,37 @@ impl ErrorClientFacing { Self::Other => "Internal Server Error".to_string(), Self::PayloadTooLarge => "The payload is too large.".to_string(), Self::RateLimited => "Rate limit exceeded. Please slow down requests and try again later.".to_string(), + Self::SubnetUnavailable => "The subnet is temporarily unavailable. This may be due to an ongoing upgrade of the replica software. Please try again later.".to_string(), Self::UnknownDomain => "The requested domain is not served by this HTTP gateway.".to_string(), Self::UpstreamError => "The HTTP gateway is temporarily unable to process the request. Please try again later.".to_string(), } } - pub const fn html(&self) -> Option<&str> { + pub fn html(&self) -> String { match self { - Self::Denylisted => Some(include_str!("error_pages/451.html")), - _ => None, + Self::Denylisted => include_str!("error_pages/451.html").to_string(), + _ => { + let template = ERROR_PAGE_TEMPLATE; + let template = template.replace("{status_code}", self.status_code().as_str()); + let template = template.replace("{reason}", self.to_string().as_str()); + let template = template.replace("{details}", self.details().as_str()); + template + } } } } -// Creates the response from ErrorCause and injects itself into extensions to be visible by middleware +// Creates the response from ErrorClientFacing impl IntoResponse for ErrorClientFacing { fn into_response(self) -> Response { - let error_cause = self.to_string(); - - // Return the HTML reply if it exists, otherwise textual - let body = self.html().map_or_else( - || format!("error: {}\ndetails: {}", error_cause, self.details()), - |x| format!("{x}\n"), - ); + // Return an HTML error page if it was an HTTP request + let body = match ERROR_CONTEXT.get() { + RequestType::Http => format!("{}\n", self.html()), + _ => format!("error: {}\ndetails: {}", self.to_string(), self.details()), + }; let mut resp = (self.status_code(), body).into_response(); - if self.html().is_some() { + if ERROR_CONTEXT.get() == RequestType::Http { resp.headers_mut().insert(CONTENT_TYPE, CONTENT_TYPE_HTML); } resp @@ -272,6 +310,8 @@ impl IntoResponse for ErrorClientFacing { #[cfg(test)] mod test { use super::*; + use ic_agent::{agent_error::HttpErrorPayload, AgentError}; + use std::sync::Arc; #[test] fn test_error_cause() { @@ -294,5 +334,49 @@ mod test { ErrorCause::from(err), ErrorCause::BackendTLSErrorOther(_) )); + + // test that no_healthy_nodes from upstream is mapped to ErrorClientFacing::NoHealthyNodes + let err_payload = HttpErrorPayload { + status: 503, + content_type: Some("text/plain".to_string()), + content: "error: no_healthy_nodes\ndetails: There are currently no healthy replica nodes available to handle the request. This may be due to an ongoing upgrade of the replica software in the subnet. Please try again later.".as_bytes().to_vec(), + }; + let err: HttpGatewayError = + HttpGatewayError::AgentError(Arc::new(AgentError::HttpError(err_payload))); + let err_cause = ErrorCause::HttpGatewayError(err); + let err_client_facing = err_cause.to_client_facing_error(); + assert!(matches!( + err_client_facing, + ErrorClientFacing::SubnetUnavailable + )); + + // test that canister_not_found from upstream is mapped to ErrorClientFacing::CanisterIdNotFound + let err_payload = HttpErrorPayload { + status: 400, + content_type: Some("text/plain".to_string()), + content: "error: canister_not_found\ndetails: The specified canister does not exist." + .as_bytes() + .to_vec(), + }; + let err: HttpGatewayError = + HttpGatewayError::AgentError(Arc::new(AgentError::HttpError(err_payload))); + let err_cause = ErrorCause::HttpGatewayError(err); + let err_client_facing = err_cause.to_client_facing_error(); + assert!(matches!( + err_client_facing, + ErrorClientFacing::CanisterIdNotFound + )); + + // test that canister_not_found from upstream is mapped to ErrorClientFacing::CanisterIdNotFound + let err: HttpGatewayError = HttpGatewayError::HeaderValueParsingError { + header_name: "Test".to_string(), + header_value: "Test".to_string(), + }; + let err_cause = ErrorCause::HttpGatewayError(err); + let err_client_facing = err_cause.to_client_facing_error(); + assert!(matches!( + err_client_facing, + ErrorClientFacing::UpstreamError + )); } } diff --git a/src/routing/error_pages/template.html b/src/routing/error_pages/template.html new file mode 100644 index 0000000..affd721 --- /dev/null +++ b/src/routing/error_pages/template.html @@ -0,0 +1,107 @@ +<html> + +<head> + <meta charset="utf8" /> + <title>Internet Computer - Error: {reason}</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="icon" type="image/x-icon" href="/favicon.ico" /> + <style> + html { + min-height: 100%; + } + + html, + body { + padding: 0; + margin: 0; + } + + body { + text-align: center; + font-size: 16px; + padding: 5em 1em 1em; + box-sizing: border-box; + font-family: CircularXX, sans-serif; + font-style: normal; + font-weight: 700; + color: #1c1e21; + display: flex; + flex-flow: column nowrap; + justify-content: space-between; + background: rgb(241, 238, 245); + background: linear-gradient(180deg, + rgba(241, 238, 245, 1) 68%, + rgba(60, 1, 186, 0.17) 100%); + } + + h1, + p { + font-size: 24px; + line-height: 32px; + } + + h1 { + font-size: 24px; + line-height: 32px; + } + + p { + color: #181818; + font-weight: 400; + font-size: 16px; + line-height: 17px; + } + + .transparent { + opacity: 0.6; + } + </style> +</head> + +<body> + <main> + <!-- Logo --> + <div> + <h3 class="transparent">This app is powered by</h3> + <div id="logo"> + <svg xmlns="http://www.w3.org/2000/svg" width="86.33" height="74.33" fill="none" viewBox="0 0 259 223"> + <g clip-path="url(#a)"> + <path fill="url(#b)" + d="M188.498 0c-12.854 0-26.892 6.604-41.721 19.623-7.022 6.167-13.11 12.766-17.678 18.06.008.009.016.017.024.029a.245.245 0 0 1 .02-.024s7.206 7.86 15.136 16.274c4.269-5.082 10.436-12.018 17.527-18.24 13.182-11.578 21.792-14.005 26.692-14.005 18.49 0 33.531 14.693 33.531 32.75 0 17.957-15.049 32.638-33.555 32.75-.843 0-1.927-.104-3.262-.4 5.392 2.338 11.188 4.02 16.708 4.02 33.89 0 40.513-22.158 40.965-23.727a52.622 52.622 0 0 0 1.535-12.639C244.416 24.432 219.331 0 188.498 0Z" /> + <path fill="url(#c)" + d="M70.107 108.926c12.854 0 26.892-6.604 41.721-19.623 7.022-6.167 13.11-12.767 17.678-18.06a.174.174 0 0 1-.024-.029c-.012.016-.02.024-.02.024s-7.206-7.86-15.136-16.274c-4.269 5.081-10.436 12.017-17.527 18.24C83.617 84.783 75.007 87.21 70.107 87.21c-18.49 0-33.53-14.693-33.53-32.75 0-17.957 15.048-32.638 33.554-32.75.843 0 1.927.104 3.261.4-5.391-2.338-11.187-4.02-16.707-4.02-33.89 0-40.513 22.157-40.965 23.727a52.612 52.612 0 0 0-1.535 12.639c.004 30.039 25.09 54.471 55.922 54.471Z" /> + <path fill="#29ABE2" + d="M201.856 90.284c-17.351-.428-35.385-14.136-39.067-17.544-9.504-8.806-31.44-32.65-33.163-34.52C113.546 20.183 91.759 0 70.107 0h-.056c-26.32.132-48.44 17.989-54.323 41.824.452-1.57 9.097-24.148 40.937-23.363 17.351.429 35.473 14.32 39.15 17.729 9.506 8.806 31.449 32.654 33.164 34.524 16.08 18.033 37.867 38.212 59.519 38.212h.056c26.32-.132 48.44-17.989 54.323-41.824-.448 1.57-9.181 23.967-41.021 23.182Z" /> + <path fill="#000" + d="M7.405 170a1.5 1.5 0 0 0 1.5-1.5v-25.36a1.5 1.5 0 0 0-1.5-1.5h-2.6a1.5 1.5 0 0 0-1.5 1.5v25.36a1.5 1.5 0 0 0 1.5 1.5h2.6ZM47.432 170a1.5 1.5 0 0 0 1.5-1.5v-25.36a1.5 1.5 0 0 0-1.5-1.5h-2.52a1.5 1.5 0 0 0-1.5 1.5v17.26L32.2 142.575a2 2 0 0 0-1.693-.935h-4.275a1.5 1.5 0 0 0-1.5 1.5v25.36a1.5 1.5 0 0 0 1.5 1.5h2.52a1.5 1.5 0 0 0 1.5-1.5v-18.62l12.29 19.198a2.001 2.001 0 0 0 1.684.922h3.206ZM84.09 146.88a1.5 1.5 0 0 0 1.5-1.5v-2.24a1.5 1.5 0 0 0-1.5-1.5H63.61a1.5 1.5 0 0 0-1.5 1.5v2.24a1.5 1.5 0 0 0 1.5 1.5h7.46v21.62a1.5 1.5 0 0 0 1.5 1.5h2.56a1.5 1.5 0 0 0 1.5-1.5v-21.62h7.46ZM115.017 170a1.5 1.5 0 0 0 1.5-1.5v-2.2a1.5 1.5 0 0 0-1.5-1.5h-10.74v-6.56h9.58a1.5 1.5 0 0 0 1.5-1.5v-1.92a1.5 1.5 0 0 0-1.5-1.5h-9.58v-6.48h10.74a1.5 1.5 0 0 0 1.5-1.5v-2.2a1.5 1.5 0 0 0-1.5-1.5h-14.76a1.5 1.5 0 0 0-1.5 1.5v25.36a1.5 1.5 0 0 0 1.5 1.5h14.76ZM144.703 169.18a1.5 1.5 0 0 0 1.337.82h2.761a1.5 1.5 0 0 0 1.328-2.196l-4.928-9.404c3.72-1.08 6.04-4.08 6.04-8.04 0-4.92-3.52-8.72-9.04-8.72h-9.58a1.5 1.5 0 0 0-1.5 1.5v25.36a1.5 1.5 0 0 0 1.5 1.5h2.56a1.5 1.5 0 0 0 1.5-1.5v-9.42h2.88l5.142 10.1Zm-8.022-14.86v-7.88h4.48c2.8 0 4.44 1.56 4.44 3.96 0 2.32-1.64 3.92-4.44 3.92h-4.48ZM188.216 170a1.5 1.5 0 0 0 1.5-1.5v-25.36a1.5 1.5 0 0 0-1.5-1.5h-2.52a1.5 1.5 0 0 0-1.5 1.5v17.26l-11.212-17.825a2 2 0 0 0-1.693-.935h-4.275a1.5 1.5 0 0 0-1.5 1.5v25.36a1.5 1.5 0 0 0 1.5 1.5h2.52a1.5 1.5 0 0 0 1.5-1.5v-18.62l12.29 19.198a2 2 0 0 0 1.684.922h3.206ZM221.835 170a1.5 1.5 0 0 0 1.5-1.5v-2.2a1.5 1.5 0 0 0-1.5-1.5h-10.74v-6.56h9.58a1.5 1.5 0 0 0 1.5-1.5v-1.92a1.5 1.5 0 0 0-1.5-1.5h-9.58v-6.48h10.74a1.5 1.5 0 0 0 1.5-1.5v-2.2a1.5 1.5 0 0 0-1.5-1.5h-14.76a1.5 1.5 0 0 0-1.5 1.5v25.36a1.5 1.5 0 0 0 1.5 1.5h14.76ZM257.239 146.88a1.5 1.5 0 0 0 1.5-1.5v-2.24a1.5 1.5 0 0 0-1.5-1.5h-20.48a1.5 1.5 0 0 0-1.5 1.5v2.24a1.5 1.5 0 0 0 1.5 1.5h7.46v21.62a1.5 1.5 0 0 0 1.5 1.5h2.56a1.5 1.5 0 0 0 1.5-1.5v-21.62h7.46ZM14.52 222.6c7.146 0 11.166-4.256 12.694-8.281.284-.747-.174-1.541-.939-1.772l-2.34-.707c-.8-.242-1.633.225-2.006.974-1.111 2.235-3.416 4.386-7.409 4.386-4.56 0-8.8-3.32-8.8-9.36 0-6.44 4.48-9.48 8.72-9.48 4.02 0 6.225 2.004 7.268 4.221.367.782 1.228 1.279 2.052 1.02l2.342-.739c.753-.238 1.202-1.022.928-1.763-1.53-4.146-5.511-8.059-12.59-8.059-7.6 0-14.44 5.76-14.44 14.8 0 9.04 6.6 14.76 14.52 14.76ZM41.154 207.8c0-6.4 4.48-9.44 8.84-9.44 4.4 0 8.88 3.04 8.88 9.44 0 6.4-4.48 9.44-8.88 9.44-4.36 0-8.84-3.04-8.84-9.44Zm-5.72.04c0 9.12 6.88 14.76 14.56 14.76 7.72 0 14.6-5.64 14.6-14.76 0-9.16-6.88-14.8-14.6-14.8-7.68 0-14.56 5.64-14.56 14.8ZM104.876 222a1.5 1.5 0 0 0 1.5-1.5v-24.86a2 2 0 0 0-2-2h-4.174a2 2 0 0 0-1.852 1.247l-7.814 19.233-8.007-19.248a2 2 0 0 0-1.847-1.232h-3.946a2 2 0 0 0-2 2v24.86a1.5 1.5 0 0 0 1.5 1.5h2.28a1.5 1.5 0 0 0 1.5-1.5v-18.22l7.777 18.793a1.5 1.5 0 0 0 1.386.927h2.591a1.5 1.5 0 0 0 1.388-.931l7.778-18.949v18.38a1.5 1.5 0 0 0 1.5 1.5h2.44ZM123.777 206.56v-8.12h4.36c2.76 0 4.44 1.56 4.44 4.08 0 2.44-1.68 4.04-4.44 4.04h-4.36Zm5.041 4.76c5.6 0 9.319-3.68 9.319-8.84 0-5.12-3.719-8.84-9.319-8.84h-9.101a1.5 1.5 0 0 0-1.5 1.5v25.36a1.5 1.5 0 0 0 1.5 1.5h2.521a1.5 1.5 0 0 0 1.5-1.5v-9.18h5.08ZM158.276 222.64c6.08 0 10.92-3.72 10.92-10.68v-16.82a1.5 1.5 0 0 0-1.5-1.5h-2.52a1.5 1.5 0 0 0-1.5 1.5v16.42c0 3.72-2.04 5.68-5.4 5.68-3.28 0-5.36-1.96-5.36-5.68v-16.42a1.5 1.5 0 0 0-1.5-1.5h-2.52a1.5 1.5 0 0 0-1.5 1.5v16.82c0 6.96 4.84 10.68 10.88 10.68ZM200.105 198.88a1.5 1.5 0 0 0 1.5-1.5v-2.24a1.5 1.5 0 0 0-1.5-1.5h-20.48a1.5 1.5 0 0 0-1.5 1.5v2.24a1.5 1.5 0 0 0 1.5 1.5h7.46v21.62a1.5 1.5 0 0 0 1.5 1.5h2.56a1.5 1.5 0 0 0 1.5-1.5v-21.62h7.46ZM227.031 222a1.5 1.5 0 0 0 1.5-1.5v-2.2a1.5 1.5 0 0 0-1.5-1.5h-10.74v-6.56h9.58a1.5 1.5 0 0 0 1.5-1.5v-1.92a1.5 1.5 0 0 0-1.5-1.5h-9.58v-6.48h10.74a1.5 1.5 0 0 0 1.5-1.5v-2.2a1.5 1.5 0 0 0-1.5-1.5h-14.76a1.5 1.5 0 0 0-1.5 1.5v25.36a1.5 1.5 0 0 0 1.5 1.5h14.76ZM252.717 221.18a1.5 1.5 0 0 0 1.337.82h2.761a1.5 1.5 0 0 0 1.328-2.196l-4.928-9.404c3.72-1.08 6.04-4.08 6.04-8.04 0-4.92-3.52-8.72-9.04-8.72h-11.08v26.86a1.5 1.5 0 0 0 1.5 1.5h2.56a1.5 1.5 0 0 0 1.5-1.5v-9.42h2.88l5.142 10.1Zm-8.022-14.86v-7.88h4.48c2.8 0 4.44 1.56 4.44 3.96 0 2.32-1.64 3.92-4.44 3.92h-4.48Z" /> + </g> + <defs> + <linearGradient id="b" x1="159.39" x2="235.567" y1="7.182" y2="85.915" + gradientUnits="userSpaceOnUse"> + <stop offset=".21" stop-color="#F15A24" /> + <stop offset=".684" stop-color="#FBB03B" /> + </linearGradient> + <linearGradient id="c" x1="99.215" x2="23.038" y1="101.744" y2="23.01" + gradientUnits="userSpaceOnUse"> + <stop offset=".21" stop-color="#ED1E79" /> + <stop offset=".893" stop-color="#522785" /> + </linearGradient> + <clipPath id="a"> + <path fill="#fff" d="M0 0h259v223H0z" /> + </clipPath> + </defs> + </svg> + </div> + </div> + + <!-- Error Notice --> + <div> + <h1>{status_code} - {reason}</h1> + <p> + {details} + </p> + </div> + </main> +</body> + +</html> diff --git a/src/routing/ic/handler.rs b/src/routing/ic/handler.rs index f36f74f..02626d9 100644 --- a/src/routing/ic/handler.rs +++ b/src/routing/ic/handler.rs @@ -97,7 +97,10 @@ pub async fn handler( let ic_status = IcResponseStatus::from(&resp); // Convert it into Axum response - let mut response = resp.canister_response.into_response(); + let mut response = match resp.metadata.internal_error { + None => resp.canister_response.into_response(), + Some(e) => return Err(ErrorCause::HttpGatewayError(e)), + }; response.extensions_mut().insert(ic_status); response.extensions_mut().insert(bn_req_meta); response.extensions_mut().insert(bn_resp_meta); diff --git a/src/routing/middleware/rate_limiter.rs b/src/routing/middleware/rate_limiter.rs index 6b64086..b602762 100644 --- a/src/routing/middleware/rate_limiter.rs +++ b/src/routing/middleware/rate_limiter.rs @@ -85,8 +85,9 @@ mod tests { use uuid::Uuid; use crate::routing::{ - error_cause::{ErrorCause, RateLimitCause}, + error_cause::{ErrorCause, RateLimitCause, ERROR_CONTEXT}, middleware::rate_limiter::{layer, IpKeyExtractor}, + RequestType, }; async fn handler(_request: Request<Body>) -> Result<impl IntoResponse, ErrorCause> { @@ -121,23 +122,27 @@ mod tests { .route("/", post(handler)) .layer(rate_limiter_mw); - // All requests filling the burst capacity should succeed - for _ in 0..burst_size { - let result = send_request(&mut app).await.unwrap(); - assert_eq!(result.status(), StatusCode::OK); - } + ERROR_CONTEXT + .scope(RequestType::Unknown, async { + // All requests filling the burst capacity should succeed + for _ in 0..burst_size { + let result = send_request(&mut app).await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + } - // Once capacity is reached, request should fail with 429 - let result = send_request(&mut app).await.unwrap(); - assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); - let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); - assert!(body.starts_with(b"error: rate_limited\n")); + // Once capacity is reached, request should fail with 429 + let result = send_request(&mut app).await.unwrap(); + assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); + let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); + assert!(body.starts_with(b"error: rate_limited\n")); - // Wait so that requests can be accepted again. - sleep(Duration::from_secs(1)).await; + // Wait so that requests can be accepted again. + sleep(Duration::from_secs(1)).await; - let result = send_request(&mut app).await.unwrap(); - assert_eq!(result.status(), StatusCode::OK); + let result = send_request(&mut app).await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + }) + .await; } #[tokio::test] @@ -152,27 +157,31 @@ mod tests { .route("/", post(handler)) .layer(rate_limiter_mw); - let total_requests = 20; - let delay = Duration::from_millis((1000.0 / rps as f64) as u64); + ERROR_CONTEXT + .scope(RequestType::Unknown, async { + let total_requests = 20; + let delay = Duration::from_millis((1000.0 / rps as f64) as u64); - // All requests submitted at the max rps rate should succeed. - for _ in 0..total_requests { - sleep(delay).await; - let result = send_request(&mut app).await.unwrap(); - assert_eq!(result.status(), StatusCode::OK); - } + // All requests submitted at the max rps rate should succeed. + for _ in 0..total_requests { + sleep(delay).await; + let result = send_request(&mut app).await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + } - // This request is submitted without delay, thus 429. - let result = send_request(&mut app).await.unwrap(); - assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); - let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); - assert!(body.starts_with(b"error: rate_limited\n")); + // This request is submitted without delay, thus 429. + let result = send_request(&mut app).await.unwrap(); + assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); + let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); + assert!(body.starts_with(b"error: rate_limited\n")); - // Wait so that requests can be accepted again. - sleep(delay).await; + // Wait so that requests can be accepted again. + sleep(delay).await; - let result = send_request(&mut app).await.unwrap(); - assert_eq!(result.status(), StatusCode::OK); + let result = send_request(&mut app).await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + }) + .await; } #[tokio::test] @@ -187,15 +196,19 @@ mod tests { .route("/", post(handler)) .layer(rate_limiter_mw); - // Send request without connection info, i.e. without ip address. - let request = Request::post("/").body(Body::from("".to_string())).unwrap(); - let result = app.call(request).await.unwrap(); - - assert_eq!(result.status(), StatusCode::INTERNAL_SERVER_ERROR); - let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); - assert_eq!( - body, - b"error: internal_server_error\ndetails: Internal Server Error" - ); + ERROR_CONTEXT + .scope(RequestType::Unknown, async { + // Send request without connection info, i.e. without ip address. + let request = Request::post("/").body(Body::from("".to_string())).unwrap(); + let result = app.call(request).await.unwrap(); + + assert_eq!(result.status(), StatusCode::INTERNAL_SERVER_ERROR); + let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); + assert_eq!( + body, + b"error: internal_server_error\ndetails: Internal Server Error" + ); + }) + .await; } } diff --git a/src/routing/middleware/validate.rs b/src/routing/middleware/validate.rs index 77878f4..39e5980 100644 --- a/src/routing/middleware/validate.rs +++ b/src/routing/middleware/validate.rs @@ -7,7 +7,10 @@ use axum::{ }; use super::extract_authority; -use crate::routing::{domain::ResolvesDomain, CanisterId, ErrorCause, RequestCtx, RequestType}; +use crate::routing::{ + domain::ResolvesDomain, error_cause::ERROR_CONTEXT, CanisterId, ErrorCause, RequestCtx, + RequestType, +}; pub async fn middleware( State(resolver): State<Arc<dyn ResolvesDomain>>, @@ -42,14 +45,21 @@ pub async fn middleware( request.extensions_mut().insert(ctx.clone()); - // Execute the request - let mut response = next.run(request).await; + // Set error context + let response = ERROR_CONTEXT + .scope(request_type, async move { + // Execute the request + let mut response = next.run(request).await; - // Inject the same into the response - response.extensions_mut().insert(ctx); - if let Some(v) = lookup.canister_id { - response.extensions_mut().insert(CanisterId(v)); - } + // Inject the same into the response + response.extensions_mut().insert(ctx); + if let Some(v) = lookup.canister_id { + response.extensions_mut().insert(CanisterId(v)); + } + + response + }) + .await; Ok(response) } From 8dbb31b417443df94ea1eefee65742528714a054 Mon Sep 17 00:00:00 2001 From: Ruediger Birkner <ruediger.birkner@dfinity.org> Date: Fri, 13 Dec 2024 11:47:37 +0100 Subject: [PATCH 4/5] update test --- src/routing/middleware/rate_limiter.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/routing/middleware/rate_limiter.rs b/src/routing/middleware/rate_limiter.rs index b602762..544e0b1 100644 --- a/src/routing/middleware/rate_limiter.rs +++ b/src/routing/middleware/rate_limiter.rs @@ -204,10 +204,7 @@ mod tests { assert_eq!(result.status(), StatusCode::INTERNAL_SERVER_ERROR); let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); - assert_eq!( - body, - b"error: internal_server_error\ndetails: Internal Server Error" - ); + assert!(body.starts_with(b"error: internal_server_error\n")); }) .await; } From d43134f6673c98f193dec9911488272c3ac1790e Mon Sep 17 00:00:00 2001 From: Ruediger Birkner <ruediger.birkner@dfinity.org> Date: Fri, 13 Dec 2024 14:29:35 +0100 Subject: [PATCH 5/5] addressing comments --- src/routing/error_cause.rs | 9 ++++++--- src/routing/error_pages/451.html | 4 ++-- src/routing/ic/handler.rs | 12 +++++++----- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/routing/error_cause.rs b/src/routing/error_cause.rs index 3a3964b..d8ab554 100644 --- a/src/routing/error_cause.rs +++ b/src/routing/error_cause.rs @@ -282,7 +282,8 @@ impl ErrorClientFacing { _ => { let template = ERROR_PAGE_TEMPLATE; let template = template.replace("{status_code}", self.status_code().as_str()); - let template = template.replace("{reason}", self.to_string().as_str()); + let template = + template.replace("{reason}", self.to_string().replace("_", " ").as_str()); let template = template.replace("{details}", self.details().as_str()); template } @@ -293,14 +294,16 @@ impl ErrorClientFacing { // Creates the response from ErrorClientFacing impl IntoResponse for ErrorClientFacing { fn into_response(self) -> Response { + let error_context = ERROR_CONTEXT.get(); + // Return an HTML error page if it was an HTTP request - let body = match ERROR_CONTEXT.get() { + let body = match error_context { RequestType::Http => format!("{}\n", self.html()), _ => format!("error: {}\ndetails: {}", self.to_string(), self.details()), }; let mut resp = (self.status_code(), body).into_response(); - if ERROR_CONTEXT.get() == RequestType::Http { + if error_context == RequestType::Http { resp.headers_mut().insert(CONTENT_TYPE, CONTENT_TYPE_HTML); } resp diff --git a/src/routing/error_pages/451.html b/src/routing/error_pages/451.html index 1ccdd51..7acb297 100644 --- a/src/routing/error_pages/451.html +++ b/src/routing/error_pages/451.html @@ -96,7 +96,7 @@ <h3 class="transparent">This app is powered by</h3> <!-- 451 Notice --> <div> - <h1>451 (Unavailable for policy reasons)</h1> + <h1>451 - unavailable for policy reasons)</h1> <h3> <strong> The page you are looking for is currently being blocked. @@ -245,4 +245,4 @@ <h3> </main> </body> -</html> \ No newline at end of file +</html> diff --git a/src/routing/ic/handler.rs b/src/routing/ic/handler.rs index 02626d9..5d275a0 100644 --- a/src/routing/ic/handler.rs +++ b/src/routing/ic/handler.rs @@ -96,11 +96,13 @@ pub async fn handler( let ic_status = IcResponseStatus::from(&resp); - // Convert it into Axum response - let mut response = match resp.metadata.internal_error { - None => resp.canister_response.into_response(), - Some(e) => return Err(ErrorCause::HttpGatewayError(e)), - }; + // Check if an error occured in the HTTP gateway library + if let Some(e) = resp.metadata.internal_error { + return Err(ErrorCause::HttpGatewayError(e)); + } + + // Convert the HTTP gateway library response into an Axum response + let mut response = resp.canister_response.into_response(); response.extensions_mut().insert(ic_status); response.extensions_mut().insert(bn_req_meta); response.extensions_mut().insert(bn_resp_meta);