Skip to content
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG.md

## v0.39.1 (unreleased)
- More precise server timing tracking to debug performance issues
- Fix missing server timing header in some cases
- Implement nice error messages for some header-related errors such as invalid header values.

## v0.39.0 (2025-10-28)
- Ability to execute sql for URL paths with another extension. If you create sitemap.xml.sql, it will be executed for example.com/sitemap.xml
- Display source line info in errors even when the database does not return a precise error position. In this case, the entire problematic SQL statement is referenced.
Expand Down
40 changes: 25 additions & 15 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ use crate::webserver::http::RequestContext;
use crate::webserver::response_writer::{AsyncResponseWriter, ResponseWriter};
use crate::webserver::ErrorWithStatus;
use crate::AppState;
use actix_web::body::MessageBody;
use actix_web::cookie::time::format_description::well_known::Rfc3339;
use actix_web::cookie::time::OffsetDateTime;
use actix_web::http::header::TryIntoHeaderPair;
use actix_web::http::{header, StatusCode};
use actix_web::{HttpResponse, HttpResponseBuilder};
use anyhow::{bail, format_err, Context as AnyhowContext};
Expand Down Expand Up @@ -116,7 +118,7 @@ impl HeaderContext {
Some(HeaderComponent::HttpHeader) => {
self.add_http_header(&data).map(PageContext::Header)
}
Some(HeaderComponent::Redirect) => self.redirect(&data).map(PageContext::Close),
Some(HeaderComponent::Redirect) => self.redirect(&data),
Some(HeaderComponent::Json) => self.json(&data),
Some(HeaderComponent::Csv) => self.csv(&data).await,
Some(HeaderComponent::Cookie) => self.add_cookie(&data).map(PageContext::Header),
Expand Down Expand Up @@ -167,7 +169,9 @@ impl HeaderContext {
self.response.status(StatusCode::FOUND);
self.has_status = true;
}
self.response.insert_header((name.as_str(), value_str));
let header = TryIntoHeaderPair::try_into_pair((name.as_str(), value_str))
.map_err(|e| anyhow::anyhow!("Invalid header: {name}:{value_str}: {e:#?}"))?;
self.response.insert_header(header);
}
Ok(self)
}
Expand Down Expand Up @@ -237,13 +241,13 @@ impl HeaderContext {
Ok(self)
}

fn redirect(mut self, data: &JsonValue) -> anyhow::Result<HttpResponse> {
fn redirect(mut self, data: &JsonValue) -> anyhow::Result<PageContext> {
self.response.status(StatusCode::FOUND);
self.has_status = true;
let link = get_object_str(data, "link")
.with_context(|| "The redirect component requires a 'link' property")?;
self.response.insert_header((header::LOCATION, link));
Ok(self.into_response_builder().body(()))
self.close_with_body(())
}

/// Answers to the HTTP request with a single json object
Expand All @@ -256,9 +260,7 @@ impl HeaderContext {
} else {
serde_json::to_vec(contents)?
};
Ok(PageContext::Close(
self.into_response_builder().body(json_response),
))
self.close_with_body(json_response)
} else {
let body_type = get_object_str(data, "type");
let json_renderer = match body_type {
Expand Down Expand Up @@ -320,10 +322,11 @@ impl HeaderContext {
.status(StatusCode::FOUND)
.insert_header((header::LOCATION, link));
self.has_status = true;
Ok(PageContext::Close(self.into_response_builder().body(
let response = self.into_response(
"Sorry, but you are not authorized to access this page. \
Redirecting to the login page...",
)))
)?;
Ok(PageContext::Close(response))
} else {
anyhow::bail!(ErrorWithStatus {
status: StatusCode::UNAUTHORIZED
Expand Down Expand Up @@ -358,9 +361,7 @@ impl HeaderContext {
self.response
.insert_header((header::CONTENT_TYPE, content_type));
}
Ok(PageContext::Close(
self.into_response_builder().body(body_bytes.into_owned()),
))
self.close_with_body(body_bytes.into_owned())
}

fn log(self, data: &JsonValue) -> anyhow::Result<PageContext> {
Expand All @@ -374,9 +375,18 @@ impl HeaderContext {
}
}

fn into_response_builder(mut self) -> HttpResponseBuilder {
fn into_response<B: MessageBody + 'static>(mut self, body: B) -> anyhow::Result<HttpResponse> {
self.add_server_timing_header();
self.response
match self.response.message_body(body) {
Ok(response) => Ok(response.map_into_boxed_body()),
Err(e) => Err(anyhow::anyhow!(
"An error occured while generating the request headers: {e:#}"
)),
}
}

fn close_with_body<B: MessageBody + 'static>(self, body: B) -> anyhow::Result<PageContext> {
Ok(PageContext::Close(self.into_response(body)?))
}

async fn start_body(mut self, data: JsonValue) -> anyhow::Result<PageContext> {
Expand All @@ -394,7 +404,7 @@ impl HeaderContext {
}

pub fn close(self) -> HttpResponse {
self.into_response_builder().finish()
self.into_response(()).unwrap()
}
}

Expand Down