Skip to content

Commit a6a872e

Browse files
committed
Improve the error message
when a header component (e.g. status_code, json, cookie) is used in the wrong place (after data has already been sent to the client).
1 parent 0bef7e1 commit a6a872e

File tree

2 files changed

+61
-37
lines changed

2 files changed

+61
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
- Format monetary values using the `money` property to specify columns and `currency` to set the currency.
3535
- Control decimal places with `number_format_digits` property.
3636
- Add a new `description_md` row-level property to the form component to allow displaying markdown in a form field description.
37+
- Improve the error message when a header component (e.g. status_code, json, cookie) is used in the wrong place (after data has already been sent to the client).
3738

3839
## 0.32.1 (2025-01-03)
3940

src/render.rs

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ use actix_web::cookie::time::OffsetDateTime;
5151
use actix_web::http::{header, StatusCode};
5252
use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
5353
use anyhow::{bail, format_err, Context as AnyhowContext};
54-
use async_recursion::async_recursion;
5554
use awc::cookie::time::Duration;
5655
use handlebars::{BlockContext, Context, JsonValue, RenderError, Renderable};
5756
use serde::Serialize;
5857
use serde_json::{json, Value};
5958
use std::borrow::Cow;
59+
use std::convert::TryFrom;
6060
use std::io::Write;
6161
use std::sync::Arc;
6262

@@ -105,15 +105,19 @@ impl HeaderContext {
105105
}
106106
pub async fn handle_row(self, data: JsonValue) -> anyhow::Result<PageContext> {
107107
log::debug!("Handling header row: {data}");
108-
match get_object_str(&data, "component") {
109-
Some("status_code") => self.status_code(&data).map(PageContext::Header),
110-
Some("http_header") => self.add_http_header(&data).map(PageContext::Header),
111-
Some("redirect") => self.redirect(&data).map(PageContext::Close),
112-
Some("json") => self.json(&data),
113-
Some("csv") => self.csv(&data).await,
114-
Some("cookie") => self.add_cookie(&data).map(PageContext::Header),
115-
Some("authentication") => self.authentication(data).await,
116-
_ => self.start_body(data).await,
108+
let comp_opt =
109+
get_object_str(&data, "component").and_then(|s| HeaderComponent::try_from(s).ok());
110+
match comp_opt {
111+
Some(HeaderComponent::StatusCode) => self.status_code(&data).map(PageContext::Header),
112+
Some(HeaderComponent::HttpHeader) => {
113+
self.add_http_header(&data).map(PageContext::Header)
114+
}
115+
Some(HeaderComponent::Redirect) => self.redirect(&data).map(PageContext::Close),
116+
Some(HeaderComponent::Json) => self.json(&data),
117+
Some(HeaderComponent::Csv) => self.csv(&data).await,
118+
Some(HeaderComponent::Cookie) => self.add_cookie(&data).map(PageContext::Header),
119+
Some(HeaderComponent::Authentication) => self.authentication(data).await,
120+
None => self.start_body(data).await,
117121
}
118122
}
119123

@@ -692,41 +696,33 @@ impl<W: std::io::Write> HtmlRenderContext<W> {
692696
component.starts_with(PAGE_SHELL_COMPONENT)
693697
}
694698

695-
#[async_recursion(? Send)]
696699
pub async fn handle_row(&mut self, data: &JsonValue) -> anyhow::Result<()> {
697700
let new_component = get_object_str(data, "component");
698701
let current_component = self
699702
.current_component
700703
.as_ref()
701704
.map(SplitTemplateRenderer::name);
702-
match (current_component, new_component) {
703-
(
704-
_,
705-
Some(
706-
component_name @ ("status_code" | "http_header" | "redirect" | "json"
707-
| "cookie" | "authentication"),
708-
),
709-
) => {
710-
bail!("The {component_name} component cannot be used after data has already been sent to the client's browser. \
711-
This component must be used before any other component. \
712-
To fix this, either move the call to the '{component_name}' component to the top of the SQL file, or create a new SQL file where '{component_name}' is the first component.");
713-
}
714-
(_, Some(c)) if Self::is_shell_component(c) => {
715-
bail!("There cannot be more than a single shell per page. \n\
716-
You are trying to open the {c:?} component, but a shell component is already opened for the current page. \n\
717-
You can fix this by removing the extra shell component, or by moving this component to the top of the SQL file, before any other component that displays data. \n")
718-
}
719-
(None, None) => {
720-
self.open_component_with_data(DEFAULT_COMPONENT, &JsonValue::Null)
721-
.await?;
722-
self.render_current_template_with_data(&data).await?;
705+
if let Some(comp_str) = new_component {
706+
if Self::is_shell_component(comp_str) {
707+
bail!("There cannot be more than a single shell per page. You are trying to open the {} component, but a shell component is already opened for the current page. You can fix this by removing the extra shell component, or by moving this component to the top of the SQL file, before any other component that displays data.", comp_str);
723708
}
724-
(_, Some(new_component)) => {
725-
self.open_component_with_data(new_component, &data).await?;
726-
}
727-
(Some(_current_component), None) => {
728-
self.render_current_template_with_data(&data).await?;
709+
710+
match self.open_component_with_data(comp_str, &data).await {
711+
Ok(_) => (),
712+
Err(err) => match HeaderComponent::try_from(comp_str) {
713+
Ok(_) => bail!("The {comp_str} component cannot be used after data has already been sent to the client's browser. \n\
714+
This component must be used before any other component. \n\
715+
To fix this, either move the call to the '{comp_str}' component to the top of the SQL file, \n\
716+
or create a new SQL file where '{comp_str}' is the first component."),
717+
Err(_) => return Err(err),
718+
},
729719
}
720+
} else if current_component.is_none() {
721+
self.open_component_with_data(DEFAULT_COMPONENT, &JsonValue::Null)
722+
.await?;
723+
self.render_current_template_with_data(&data).await?;
724+
} else {
725+
self.render_current_template_with_data(&data).await?;
730726
}
731727
Ok(())
732728
}
@@ -1056,3 +1052,30 @@ mod tests {
10561052
Ok(())
10571053
}
10581054
}
1055+
1056+
#[derive(Copy, Clone, PartialEq, Eq)]
1057+
enum HeaderComponent {
1058+
StatusCode,
1059+
HttpHeader,
1060+
Redirect,
1061+
Json,
1062+
Csv,
1063+
Cookie,
1064+
Authentication,
1065+
}
1066+
1067+
impl TryFrom<&str> for HeaderComponent {
1068+
type Error = ();
1069+
fn try_from(s: &str) -> Result<Self, Self::Error> {
1070+
match s {
1071+
"status_code" => Ok(HeaderComponent::StatusCode),
1072+
"http_header" => Ok(HeaderComponent::HttpHeader),
1073+
"redirect" => Ok(HeaderComponent::Redirect),
1074+
"json" => Ok(HeaderComponent::Json),
1075+
"csv" => Ok(HeaderComponent::Csv),
1076+
"cookie" => Ok(HeaderComponent::Cookie),
1077+
"authentication" => Ok(HeaderComponent::Authentication),
1078+
_ => Err(()),
1079+
}
1080+
}
1081+
}

0 commit comments

Comments
 (0)