Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(errors): create client-facing errors #50

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 201 additions & 79 deletions src/routing/error_cause.rs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/routing/error_pages/451.html
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -245,4 +245,4 @@ <h3>
</main>
</body>

</html>
</html>
107 changes: 107 additions & 0 deletions src/routing/error_pages/template.html
Original file line number Diff line number Diff line change
@@ -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>
7 changes: 6 additions & 1 deletion src/routing/ic/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
89 changes: 51 additions & 38 deletions src/routing/middleware/rate_limiter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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;
}
}
26 changes: 18 additions & 8 deletions src/routing/middleware/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>>,
Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion src/routing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
))
}
Expand Down
6 changes: 3 additions & 3 deletions src/routing/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading