Skip to content

Commit 139e829

Browse files
committed
Update default route
1 parent c1f45ef commit 139e829

File tree

4 files changed

+175
-24
lines changed

4 files changed

+175
-24
lines changed

.cursor/rules/rust.mdc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: true
5+
---
6+
---
7+
description: This is helpful for working on Rust files
8+
globs: **.rs
9+
---
10+
11+
This Rust project uses the async-std runtime, the anyhow error handling crate, the Poem HTTP framework, and rustls-tls for secure TLS. Assume these dependencies are available and imported when generating code.

engine/src/middlewares/auth.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ impl UserAuth {
197197

198198
Ok(())
199199
},
200-
UserAuth::Key(key, _) => {
200+
UserAuth::Key(_key, _) => {
201201
Err(HttpError::Forbidden)
202202
},
203203
UserAuth::None(_) => Err(HttpError::Unauthorized),

engine/src/routes/site/keys/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl SiteKeysApi {
8181
) -> Result<Json<serde_json::Value>> {
8282
user.verify_access_to(&SiteId(&site_id.0)).await?;
8383

84-
let user = user.required_session()?;
84+
let _user = user.required_session()?;
8585

8686
let key = Key::get_by_id(&state.database, key_id.as_ref())
8787
.await

engine/src/server/mod.rs

Lines changed: 162 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
use std::sync::Arc;
22

3+
use crate::cache::ResolveResult;
4+
use futures::StreamExt;
35
use opentelemetry::global;
46
use poem::middleware::OpenTelemetryMetrics;
57
use poem::web::Data;
6-
use poem::{Body, EndpointExt, IntoResponse, Request, Response};
78
use poem::{get, handler, listener::TcpListener, middleware::Cors, Route, Server};
9+
use poem::{Body, EndpointExt, IntoResponse, Request, Response};
810
use reqwest::StatusCode;
911
use tracing::info;
10-
use futures::StreamExt;
11-
use bytes::Bytes;
12-
use crate::cache::ResolveResult;
1312

1413
use crate::middlewares::tracing::TraceId;
15-
use crate::models::deployment::{Deployment, DeploymentFile, DeploymentFileEntry};
14+
use crate::models::deployment::{Deployment, DeploymentFile};
1615
use crate::models::domain::Domain;
1716
use crate::models::site::Site;
1817
use crate::routes::error::HttpError;
@@ -40,7 +39,8 @@ pub async fn serve(state: State) {
4039
async fn resolve_http(request: &Request, state: Data<&State>) -> impl IntoResponse {
4140
// extract host and path
4241
let headers = request.headers();
43-
let host = headers.get("host")
42+
let host = headers
43+
.get("host")
4444
.and_then(|h| h.to_str().ok())
4545
.unwrap_or("localhost");
4646
let raw_path = request.uri().path();
@@ -60,10 +60,22 @@ async fn resolve_http(request: &Request, state: Data<&State>) -> impl IntoRespon
6060
.get_with(cache_key.clone(), async move {
6161
match get_last_deployment(&host_for_cache, &state_for_cache.clone()).await {
6262
Ok(deployment) => {
63-
let lookup_path = if path_for_cache.is_empty() { "index.html" } else { path_for_cache.as_str() };
64-
match DeploymentFile::get_file_by_path(&state_for_cache.clone().database, &deployment.deployment_id, lookup_path).await {
63+
let lookup_path = if path_for_cache.is_empty() {
64+
"index.html"
65+
} else {
66+
path_for_cache.as_str()
67+
};
68+
match DeploymentFile::get_file_by_path(
69+
&state_for_cache.clone().database,
70+
&deployment.deployment_id,
71+
lookup_path,
72+
)
73+
.await
74+
{
6575
Ok(entry) => ResolveResult::Success(entry),
66-
Err(_) => ResolveResult::NotFound(format!("File not found: {}", lookup_path)),
76+
Err(_) => {
77+
ResolveResult::NotFound(format!("File not found: {}", lookup_path))
78+
}
6779
}
6880
}
6981
Err(err) => {
@@ -81,10 +93,19 @@ async fn resolve_http(request: &Request, state: Data<&State>) -> impl IntoRespon
8193
// handle the resolved result
8294
match cached_resolve {
8395
ResolveResult::Success(deployment_file) => {
84-
info!("Serving file: {} (cached lookup)", deployment_file.file_hash);
96+
info!(
97+
"Serving file: {} (cached lookup)",
98+
deployment_file.file_hash
99+
);
85100
// if eligible for full caching
86101
if (deployment_file.deployment_file_mime_type == "text/html"
87-
|| HTML_CACHE_FILE_EXTENSIONS.contains(&deployment_file.deployment_file_file_path.split('.').last().unwrap_or("")))
102+
|| HTML_CACHE_FILE_EXTENSIONS.contains(
103+
&deployment_file
104+
.deployment_file_file_path
105+
.split('.')
106+
.last()
107+
.unwrap_or(""),
108+
))
88109
&& deployment_file.file_size.unwrap_or(0) <= HTML_CACHE_SIZE_LIMIT as i64
89110
{
90111
let file_key = deployment_file.file_hash.clone();
@@ -93,18 +114,27 @@ async fn resolve_http(request: &Request, state: Data<&State>) -> impl IntoRespon
93114
let body = Body::from_bytes(cached_bytes.clone());
94115
return Response::builder()
95116
.status(StatusCode::OK)
96-
.header("content-type", deployment_file.deployment_file_mime_type.clone())
117+
.header(
118+
"content-type",
119+
deployment_file.deployment_file_mime_type.clone(),
120+
)
97121
.body(body);
98122
}
99123
// fetch from S3 and cache
100124
match state.storage.bucket.get_object(&file_key).await {
101125
Ok(data) => {
102126
let bytes = data.into_bytes();
103-
state.cache.file_bytes.insert(file_key.clone(), bytes.clone());
127+
state
128+
.cache
129+
.file_bytes
130+
.insert(file_key.clone(), bytes.clone());
104131
let body = Body::from_bytes(bytes);
105132
return Response::builder()
106133
.status(StatusCode::OK)
107-
.header("content-type", deployment_file.deployment_file_mime_type.clone())
134+
.header(
135+
"content-type",
136+
deployment_file.deployment_file_mime_type.clone(),
137+
)
108138
.body(body);
109139
}
110140
Err(e) => {
@@ -128,7 +158,10 @@ async fn resolve_http(request: &Request, state: Data<&State>) -> impl IntoRespon
128158
let body = Body::from_bytes_stream(stream);
129159
return Response::builder()
130160
.status(StatusCode::OK)
131-
.header("content-type", deployment_file.deployment_file_mime_type.clone())
161+
.header(
162+
"content-type",
163+
deployment_file.deployment_file_mime_type.clone(),
164+
)
132165
.body(body);
133166
}
134167
Err(e) => {
@@ -140,10 +173,112 @@ async fn resolve_http(request: &Request, state: Data<&State>) -> impl IntoRespon
140173
}
141174
}
142175
ResolveResult::NotFound(reason) => {
143-
info!("Not found: {}", reason);
176+
// SPA fallback when the specific file isn't found
177+
info!("Not found: {}, attempting SPA fallback", reason);
178+
// Try index.html for SPA routing
179+
let spa_cache_key = format!("resolve:{}:index.html", host);
180+
let state_for_spa = state.clone();
181+
let host_for_spa = host.to_string();
182+
let spa_result: ResolveResult = state
183+
.cache
184+
.resolve
185+
.get_with(spa_cache_key.clone(), async move {
186+
match get_last_deployment(&host_for_spa, &state_for_spa.clone()).await {
187+
Ok(deployment) => {
188+
match DeploymentFile::get_file_by_path(
189+
&state_for_spa.clone().database,
190+
&deployment.deployment_id,
191+
"index.html",
192+
)
193+
.await
194+
{
195+
Ok(entry) => ResolveResult::Success(entry),
196+
Err(_) => {
197+
ResolveResult::NotFound("SPA index.html not found".to_string())
198+
}
199+
}
200+
}
201+
Err(err) => {
202+
let msg = err.to_string();
203+
if let HttpError::NotFound = err {
204+
ResolveResult::NotFound(msg)
205+
} else {
206+
ResolveResult::Error(msg)
207+
}
208+
}
209+
}
210+
})
211+
.await;
212+
// If SPA index.html found, serve it using same caching logic
213+
if let ResolveResult::Success(deployment_file) = spa_result {
214+
info!("Serving SPA index.html for {} {}", host, path);
215+
// full in-memory cache eligible?
216+
if (deployment_file.deployment_file_mime_type == "text/html"
217+
|| HTML_CACHE_FILE_EXTENSIONS.contains(
218+
&deployment_file
219+
.deployment_file_file_path
220+
.split('.')
221+
.last()
222+
.unwrap_or(""),
223+
))
224+
&& deployment_file.file_size.unwrap_or(0) <= HTML_CACHE_SIZE_LIMIT as i64
225+
{
226+
let file_key = deployment_file.file_hash.clone();
227+
// check in-memory cache
228+
if let Some(cached_bytes) = state.cache.file_bytes.get(&file_key).await {
229+
let body = Body::from_bytes(cached_bytes.clone());
230+
return Response::builder()
231+
.status(StatusCode::OK)
232+
.header(
233+
"content-type",
234+
deployment_file.deployment_file_mime_type.clone(),
235+
)
236+
.body(body);
237+
}
238+
// fetch and cache
239+
if let Ok(data) = state.storage.bucket.get_object(&file_key).await {
240+
let bytes = data.into_bytes();
241+
state
242+
.cache
243+
.file_bytes
244+
.insert(file_key.clone(), bytes.clone());
245+
let body = Body::from_bytes(bytes);
246+
return Response::builder()
247+
.status(StatusCode::OK)
248+
.header(
249+
"content-type",
250+
deployment_file.deployment_file_mime_type.clone(),
251+
)
252+
.body(body);
253+
}
254+
}
255+
// otherwise, stream from S3
256+
let s3_path = deployment_file.file_hash.clone();
257+
if let Ok(s3_data) = state.storage.bucket.get_object_stream(s3_path).await {
258+
let stream = s3_data.bytes.map(|chunk| {
259+
chunk.map_err(|e| {
260+
info!("Error streaming SPA index: {}", e);
261+
std::io::Error::new(std::io::ErrorKind::Other, e)
262+
})
263+
});
264+
let body = Body::from_bytes_stream(stream);
265+
return Response::builder()
266+
.status(StatusCode::OK)
267+
.header(
268+
"content-type",
269+
deployment_file.deployment_file_mime_type.clone(),
270+
)
271+
.body(body);
272+
}
273+
}
274+
// default 404 if SPA fallback failed
275+
info!(
276+
"SPA fallback failed for {} {}, returning default 404",
277+
host, path
278+
);
144279
return Response::builder()
145280
.status(StatusCode::NOT_FOUND)
146-
.body(Body::from_string(reason));
281+
.body(Body::from_string(include_str!("./404.html").to_string()));
147282
}
148283
ResolveResult::Error(err) => {
149284
info!("Resolution error: {}", err);
@@ -155,21 +290,26 @@ async fn resolve_http(request: &Request, state: Data<&State>) -> impl IntoRespon
155290
}
156291

157292
async fn get_last_deployment(host: &str, state: &State) -> Result<Deployment, HttpError> {
158-
// get domain
293+
// get domain
159294
// get site
160295
// get last deployment
161296
// get file at path in deployment
162297
// otherwise return default /index.html if exists
163298
let domain = Domain::existing_domain_by_name(host, &state.clone())
164-
.await.ok().flatten();
299+
.await
300+
.ok()
301+
.flatten();
165302

166303
if let Some(domain) = domain {
167304
let site = Site::get_by_id(&state.clone().database, &domain.site_id)
168-
.await.ok();
305+
.await
306+
.ok();
169307

170308
if let Some(site) = site {
171-
let deployment = Deployment::get_last_by_site_id(&state.clone().database, &site.site_id)
172-
.await.ok();
309+
let deployment =
310+
Deployment::get_last_by_site_id(&state.clone().database, &site.site_id)
311+
.await
312+
.ok();
173313

174314
if let Some(deployment) = deployment {
175315
return Ok(deployment);

0 commit comments

Comments
 (0)