1
0
Fork 0
website/src/handlers/mod.rs

182 lines
5.3 KiB
Rust
Raw Normal View History

2022-08-31 23:25:17 +02:00
use axum::{
2024-04-17 19:12:59 +02:00
body::Body,
extract::{Request, State},
http::{header, HeaderMap, StatusCode},
2023-03-29 18:20:51 +02:00
middleware::Next,
2023-04-02 15:27:06 +02:00
response::{Html, IntoResponse, Response},
2023-03-29 18:20:51 +02:00
routing::get,
Router,
2022-08-31 23:25:17 +02:00
};
2023-07-29 20:22:13 +02:00
use cached::once_cell::sync::Lazy;
2023-07-29 11:51:04 +02:00
use chrono::{DateTime, FixedOffset};
2023-03-29 18:20:51 +02:00
use prometheus::{opts, Encoder, IntCounterVec, TextEncoder};
2023-03-25 22:12:49 +01:00
use std::sync::Arc;
2023-07-29 20:22:13 +02:00
use tower_http::services::ServeDir;
2023-07-29 12:12:46 +02:00
use tracing::{
instrument,
2024-04-17 19:12:59 +02:00
log::{debug, error, info},
2023-07-29 12:12:46 +02:00
};
2022-08-31 23:20:59 +02:00
use crate::{AppState, WebsiteError};
2022-08-31 23:20:59 +02:00
2023-03-25 22:12:49 +01:00
pub mod posts;
2023-03-29 21:48:27 +02:00
pub mod tags;
2023-03-25 22:12:49 +01:00
2024-04-17 19:12:59 +02:00
pub static HIT_COUNTER: Lazy<IntCounterVec> = Lazy::new(|| {
prometheus::register_int_counter_vec!(
opts!(
"http_requests_total",
"Total amount of http requests received"
),
&["route", "method", "status"]
)
.unwrap()
});
2023-03-25 22:12:49 +01:00
2023-03-29 18:19:39 +02:00
pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
Router::new()
.route("/", get(index))
2023-04-02 15:26:20 +02:00
.merge(posts::router())
.merge(tags::router())
2023-03-29 18:19:39 +02:00
.merge(posts::alias_router(state.posts.values()))
.route("/healthcheck", get(healthcheck))
2024-04-17 19:12:59 +02:00
.route_service("/posts/:slug/*path", ServeDir::new("./"))
.route_service("/static/*path", ServeDir::new("./"))
2023-11-10 23:09:00 +01:00
.layer(axum::middleware::from_fn(metrics_middleware))
.route("/metrics", get(metrics))
}
2023-03-25 16:14:53 +01:00
#[instrument(skip(state))]
2023-03-25 22:12:49 +01:00
pub async fn index(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
2023-07-29 11:51:04 +02:00
) -> std::result::Result<Response, WebsiteError> {
if should_return_304(&headers, Some(state.startup_time.into())) {
return Ok(StatusCode::NOT_MODIFIED.into_response());
}
2023-07-29 20:22:13 +02:00
let mut ctx = tera::Context::new();
ctx.insert("base_url", &state.base_url.to_string());
2022-08-31 23:25:17 +02:00
let res = state.tera.render("index.html", &ctx).map_err(|e| {
error!("Failed rendering index: {}", e);
2023-07-29 20:22:13 +02:00
WebsiteError::InternalError(e.into())
2022-08-31 23:25:17 +02:00
})?;
2023-07-29 12:12:46 +02:00
Ok((
StatusCode::OK,
[(header::LAST_MODIFIED, state.startup_time.to_rfc2822())],
Html(res),
2023-07-29 11:51:04 +02:00
)
2023-07-29 12:12:46 +02:00
.into_response())
}
async fn healthcheck() -> &'static str {
"OK"
}
2023-11-10 23:09:00 +01:00
#[instrument]
async fn metrics() -> impl IntoResponse {
let encoder = TextEncoder::new();
let metric_families = prometheus::gather();
let mut buffer = vec![];
encoder.encode(&metric_families, &mut buffer).unwrap();
Response::builder()
.status(200)
.header(header::CONTENT_TYPE, encoder.format_type())
2024-04-17 19:12:59 +02:00
.body(Body::from(buffer))
.unwrap()
2022-08-31 23:20:59 +02:00
}
2023-03-29 21:48:27 +02:00
pub async fn not_found() -> impl IntoResponse {
(StatusCode::NOT_FOUND, ())
2023-03-26 12:40:25 +02:00
}
2023-11-10 23:09:00 +01:00
#[instrument(skip(request, next))]
2024-04-17 19:12:59 +02:00
pub async fn metrics_middleware(request: Request, next: Next) -> Response {
let path = request.uri().path().to_string();
2023-07-29 20:22:13 +02:00
let method = request.method().clone();
2023-03-29 21:48:27 +02:00
let response = next.run(request).await;
2023-03-29 21:48:27 +02:00
if !response.status().is_client_error() {
HIT_COUNTER
2023-07-29 20:22:13 +02:00
.with_label_values(&[&path, method.as_str(), response.status().as_str()])
2023-03-29 21:48:27 +02:00
.inc();
2023-04-10 00:46:58 +02:00
} else if response.status() == StatusCode::NOT_FOUND {
HIT_COUNTER
2023-07-29 20:22:13 +02:00
.with_label_values(&["not found", method.as_str(), response.status().as_str()])
2023-04-10 00:46:58 +02:00
.inc();
2023-03-29 21:48:27 +02:00
}
response
}
2023-07-29 12:04:37 +02:00
fn should_return_304(headers: &HeaderMap, last_changed: Option<DateTime<FixedOffset>>) -> bool {
2023-07-29 11:51:04 +02:00
let Some(date) = last_changed else {
2023-07-29 20:22:13 +02:00
debug!("no last modified date");
2023-07-29 11:51:04 +02:00
return false;
};
let Some(since) = headers.get(header::IF_MODIFIED_SINCE) else {
2023-07-29 20:22:13 +02:00
debug!("no IF_MODIFIED_SINCE header");
2023-07-29 11:51:04 +02:00
return false;
};
let Ok(parsed) = DateTime::<FixedOffset>::parse_from_rfc2822(since.to_str().unwrap()) else {
2023-07-29 20:22:13 +02:00
debug!("failed to parse IF_MODIFIED_SINCE header");
2023-07-29 11:51:04 +02:00
return false;
};
date > parsed
}
2023-03-25 21:38:16 +01:00
impl IntoResponse for WebsiteError {
2022-08-31 23:20:59 +02:00
fn into_response(self) -> Response {
match self {
2023-03-25 21:38:16 +01:00
WebsiteError::NotFound => {
2023-03-25 16:14:53 +01:00
info!("not found");
(StatusCode::NOT_FOUND, ()).into_response()
}
2023-03-25 21:38:16 +01:00
WebsiteError::InternalError(e) => {
2023-04-10 00:46:58 +02:00
if let Some(s) = e.source() {
error!("internal error: {}: {}", e, s);
} else {
error!("internal error: {}", e);
}
2023-03-25 16:14:53 +01:00
(StatusCode::INTERNAL_SERVER_ERROR, ()).into_response()
}
2022-08-31 23:20:59 +02:00
}
}
}
#[cfg(test)]
mod tests {
2023-03-29 18:31:20 +02:00
use std::sync::Arc;
use crate::AppState;
#[test]
fn render_index() {
let tera = tera::Tera::new("templates/**/*").unwrap();
2023-07-29 20:22:13 +02:00
let mut ctx = tera::Context::new();
ctx.insert("base_url", "http://localhost/");
let _res = tera.render("index.html", &ctx).unwrap();
}
2023-03-29 18:31:20 +02:00
#[tokio::test]
async fn setup_routes() {
let mut state = AppState {
2023-07-29 11:51:04 +02:00
startup_time: chrono::offset::Utc::now(),
base_url: "http://localhost:8180".parse().unwrap(),
2023-03-29 18:31:20 +02:00
tera: tera::Tera::new("templates/**/*").unwrap(),
..Default::default()
};
// Load the actual posts, just to make this test fail if
// aliases overlap with themselves or other routes
let posts = crate::post::load_all(&state).await.unwrap();
state.tags = crate::tag::get_tags(posts.values());
state.posts = posts;
let state = Arc::new(state);
2023-03-29 18:31:20 +02:00
2023-04-02 15:26:20 +02:00
super::routes(&state).with_state(state).into_make_service();
2023-03-29 18:31:20 +02:00
}
}