diff --git a/src/routing/error_cause.rs b/src/routing/error_cause.rs index 18c0b59..d8ab554 100644 --- a/src/routing/error_cause.rs +++ b/src/routing/error_cause.rs @@ -1,14 +1,20 @@ -use std::{ - error::Error as StdError, - fmt::{self}, -}; +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 strum_macros::{Display, IntoStaticStr}; +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(error: &anyhow::Error) -> Option<&E> { @@ -28,7 +34,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 +57,14 @@ pub enum ErrorCause { BackendBodyError(String), BackendTLSErrorOther(String), BackendTLSErrorCert(String), + #[strum(serialize = "rate_limited_{0}")] RateLimited(RateLimitCause), + #[strum(serialize = "internal_server_error")] Other(String), + HttpGatewayError(HttpGatewayError), } 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 { match self { Self::Other(x) => Some(x.clone()), @@ -95,13 +78,7 @@ impl ErrorCause { Self::BackendTLSErrorCert(x) => Some(x.clone()), Self::AgentError(x) => Some(x.clone()), Self::RateLimited(x) => Some(x.to_string()), - _ => None, - } - } - - pub const fn html(&self) -> Option<&str> { - match self { - Self::Denylisted => Some(include_str!("error_pages/451.html")), + Self::HttpGatewayError(x) => Some(x.to_string()), _ => None, } } @@ -125,33 +102,51 @@ 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, + 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, + }, } } } @@ -159,20 +154,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,9 +217,104 @@ impl From 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, + SubnetUnavailable, + 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::SubnetUnavailable => StatusCode::SERVICE_UNAVAILABLE, + 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::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 fn html(&self) -> String { + match self { + 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().replace("_", " ").as_str()); + let template = template.replace("{details}", self.details().as_str()); + template + } + } + } +} + +// 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 { + 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 == RequestType::Http { + resp.headers_mut().insert(CONTENT_TYPE, CONTENT_TYPE_HTML); + } + resp + } +} + #[cfg(test)] mod test { use super::*; + use ic_agent::{agent_error::HttpErrorPayload, AgentError}; + use std::sync::Arc; #[test] fn test_error_cause() { @@ -259,5 +337,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/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 @@

This app is powered by

-

451 (Unavailable for policy reasons)

+

451 - unavailable for policy reasons)

The page you are looking for is currently being blocked. @@ -245,4 +245,4 @@

- \ No newline at end of file + 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 @@ + + + + + Internet Computer - Error: {reason} + + + + + + +
+ +
+

This app is powered by

+ +
+ + +
+

{status_code} - {reason}

+

+ {details} +

+
+
+ + + diff --git a/src/routing/ic/handler.rs b/src/routing/ic/handler.rs index f36f74f..5d275a0 100644 --- a/src/routing/ic/handler.rs +++ b/src/routing/ic/handler.rs @@ -96,7 +96,12 @@ pub async fn handler( let ic_status = IcResponseStatus::from(&resp); - // Convert it into Axum response + // 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); diff --git a/src/routing/middleware/rate_limiter.rs b/src/routing/middleware/rate_limiter.rs index f9ffabb..544e0b1 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) -> Result { @@ -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_eq!(body, b"rate_limited_normal: normal\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_eq!(body, b"rate_limited_normal: normal\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,12 +196,16 @@ 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(); + 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"general_error: UnableToExtractIpAddress\n"); + assert_eq!(result.status(), StatusCode::INTERNAL_SERVER_ERROR); + let body = to_bytes(result.into_body(), 100).await.unwrap().to_vec(); + assert!(body.starts_with(b"error: internal_server_error\n")); + }) + .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>, @@ -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) } 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