Skip to content

Commit 105fbd3

Browse files
committed
refactor header error handling in render.rs
1 parent 2eaa17f commit 105fbd3

File tree

2 files changed

+34
-47
lines changed

2 files changed

+34
-47
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# CHANGELOG.md
22

3+
## v0.39.1 (unreleased)
4+
- More precise server timing tracking to debug performance issues
5+
- Fix missing server timing header in some cases
6+
- Implement nice error messages for some header-related errors such as invalid header values.
7+
38
## v0.39.0 (2025-10-28)
49
- 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
510
- 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.

src/render.rs

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ use crate::webserver::http::RequestContext;
4646
use crate::webserver::response_writer::{AsyncResponseWriter, ResponseWriter};
4747
use crate::webserver::ErrorWithStatus;
4848
use crate::AppState;
49+
use actix_web::body::MessageBody;
4950
use actix_web::cookie::time::format_description::well_known::Rfc3339;
5051
use actix_web::cookie::time::OffsetDateTime;
52+
use actix_web::http::header::TryIntoHeaderPair;
5153
use actix_web::http::{header, StatusCode};
5254
use actix_web::{HttpResponse, HttpResponseBuilder};
5355
use anyhow::{bail, format_err, Context as AnyhowContext};
@@ -116,7 +118,7 @@ impl HeaderContext {
116118
Some(HeaderComponent::HttpHeader) => {
117119
self.add_http_header(&data).map(PageContext::Header)
118120
}
119-
Some(HeaderComponent::Redirect) => self.redirect(&data).map(PageContext::Close),
121+
Some(HeaderComponent::Redirect) => self.redirect(&data),
120122
Some(HeaderComponent::Json) => self.json(&data),
121123
Some(HeaderComponent::Csv) => self.csv(&data).await,
122124
Some(HeaderComponent::Cookie) => self.add_cookie(&data).map(PageContext::Header),
@@ -167,9 +169,9 @@ impl HeaderContext {
167169
self.response.status(StatusCode::FOUND);
168170
self.has_status = true;
169171
}
170-
let sanitized_value = sanitize_header_value(value_str);
171-
self.response
172-
.insert_header((name.as_str(), sanitized_value.as_ref()));
172+
let header = TryIntoHeaderPair::try_into_pair((name.as_str(), value_str))
173+
.map_err(|e| anyhow::anyhow!("Invalid header: {name}:{value_str}: {e:#?}"))?;
174+
self.response.insert_header(header);
173175
}
174176
Ok(self)
175177
}
@@ -239,15 +241,13 @@ impl HeaderContext {
239241
Ok(self)
240242
}
241243

242-
fn redirect(mut self, data: &JsonValue) -> anyhow::Result<HttpResponse> {
244+
fn redirect(mut self, data: &JsonValue) -> anyhow::Result<PageContext> {
243245
self.response.status(StatusCode::FOUND);
244246
self.has_status = true;
245247
let link = get_object_str(data, "link")
246248
.with_context(|| "The redirect component requires a 'link' property")?;
247-
let sanitized_link = sanitize_header_value(link);
248-
self.response
249-
.insert_header((header::LOCATION, sanitized_link.as_ref()));
250-
Ok(self.into_response_builder().body(()))
249+
self.response.insert_header((header::LOCATION, link));
250+
self.close_with_body(())
251251
}
252252

253253
/// Answers to the HTTP request with a single json object
@@ -260,9 +260,7 @@ impl HeaderContext {
260260
} else {
261261
serde_json::to_vec(contents)?
262262
};
263-
Ok(PageContext::Close(
264-
self.into_response_builder().body(json_response),
265-
))
263+
self.close_with_body(json_response)
266264
} else {
267265
let body_type = get_object_str(data, "type");
268266
let json_renderer = match body_type {
@@ -293,10 +291,9 @@ impl HeaderContext {
293291
get_object_str(options, "filename").or_else(|| get_object_str(options, "title"))
294292
{
295293
let extension = if filename.contains('.') { "" } else { ".csv" };
296-
let sanitized_filename = sanitize_header_value(filename);
297294
self.response.insert_header((
298295
header::CONTENT_DISPOSITION,
299-
format!("attachment; filename={sanitized_filename}{extension}"),
296+
format!("attachment; filename={filename}{extension}"),
300297
));
301298
}
302299
let csv_renderer = CsvBodyRenderer::new(self.writer, options).await?;
@@ -321,15 +318,15 @@ impl HeaderContext {
321318
log::debug!("Authentication failed");
322319
// The authentication failed
323320
if let Some(link) = get_object_str(&data, "link") {
324-
let sanitized_link = sanitize_header_value(link);
325321
self.response
326322
.status(StatusCode::FOUND)
327-
.insert_header((header::LOCATION, sanitized_link.as_ref()));
323+
.insert_header((header::LOCATION, link));
328324
self.has_status = true;
329-
Ok(PageContext::Close(self.into_response_builder().body(
325+
let response = self.into_response(
330326
"Sorry, but you are not authorized to access this page. \
331327
Redirecting to the login page...",
332-
)))
328+
)?;
329+
Ok(PageContext::Close(response))
333330
} else {
334331
anyhow::bail!(ErrorWithStatus {
335332
status: StatusCode::UNAUTHORIZED
@@ -339,10 +336,9 @@ impl HeaderContext {
339336

340337
fn download(mut self, options: &JsonValue) -> anyhow::Result<PageContext> {
341338
if let Some(filename) = get_object_str(options, "filename") {
342-
let sanitized_filename = sanitize_header_value(filename);
343339
self.response.insert_header((
344340
header::CONTENT_DISPOSITION,
345-
format!("attachment; filename=\"{sanitized_filename}\""),
341+
format!("attachment; filename=\"{filename}\""),
346342
));
347343
}
348344
let data_url = get_object_str(options, "data_url")
@@ -365,9 +361,7 @@ impl HeaderContext {
365361
self.response
366362
.insert_header((header::CONTENT_TYPE, content_type));
367363
}
368-
Ok(PageContext::Close(
369-
self.into_response_builder().body(body_bytes.into_owned()),
370-
))
364+
self.close_with_body(body_bytes.into_owned())
371365
}
372366

373367
fn log(self, data: &JsonValue) -> anyhow::Result<PageContext> {
@@ -381,9 +375,18 @@ impl HeaderContext {
381375
}
382376
}
383377

384-
fn into_response_builder(mut self) -> HttpResponseBuilder {
378+
fn into_response<B: MessageBody + 'static>(mut self, body: B) -> anyhow::Result<HttpResponse> {
385379
self.add_server_timing_header();
386-
self.response
380+
match self.response.message_body(body) {
381+
Ok(response) => Ok(response.map_into_boxed_body()),
382+
Err(e) => Err(anyhow::anyhow!(
383+
"An error occured while generating the request headers: {e:#}"
384+
)),
385+
}
386+
}
387+
388+
fn close_with_body<B: MessageBody + 'static>(self, body: B) -> anyhow::Result<PageContext> {
389+
Ok(PageContext::Close(self.into_response(body)?))
387390
}
388391

389392
async fn start_body(mut self, data: JsonValue) -> anyhow::Result<PageContext> {
@@ -401,7 +404,7 @@ impl HeaderContext {
401404
}
402405

403406
pub fn close(self) -> HttpResponse {
404-
self.into_response_builder().finish()
407+
self.into_response(()).unwrap()
405408
}
406409
}
407410

@@ -418,27 +421,6 @@ async fn verify_password_async(
418421
.await?
419422
}
420423

421-
fn sanitize_header_value(value: &str) -> Cow<'_, str> {
422-
if value.bytes().all(|b| b >= 0x20 && b != 0x7F) {
423-
return Cow::Borrowed(value);
424-
}
425-
426-
log::warn!(
427-
"Sanitized header value by removing control characters. Original length: {}",
428-
value.len()
429-
);
430-
431-
Cow::Owned(
432-
value
433-
.chars()
434-
.filter(|&c| {
435-
let byte = c as u32;
436-
byte >= 0x20 && byte != 0x7F
437-
})
438-
.collect(),
439-
)
440-
}
441-
442424
fn get_object_str<'a>(json: &'a JsonValue, key: &str) -> Option<&'a str> {
443425
json.as_object()
444426
.and_then(|obj| obj.get(key))

0 commit comments

Comments
 (0)