Skip to content

Commit 02261f4

Browse files
committed
improve top-level error handling
1 parent 5e95a31 commit 02261f4

File tree

6 files changed

+96
-60
lines changed

6 files changed

+96
-60
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
- When no component is selected, display data with the `debug` component by default.
2727
This makes any simple `SELECT` statement a valid SQLPage file.
2828
Before, data returned outside of a component would be ignored.
29+
- Improved error handling. SQLPage now displays a nice error page when an error occurs, even if it's at the top of the page.
30+
This makes it easier to debug SQLPage websites. Before, errors that occured before SQLPage had started to render the page would be displayed as a raw text error message without any styling.
2931

3032
## 0.9.5 (2023-08-12)
3133

sqlpage/templates/error.handlebars

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,18 @@
66
We are sorry, but an error occurred while generating this page.
77
You should contact the site's administrator.
88
</p>
9-
{{#each_row}}
10-
<p>
11-
{{#if query_number}}
12-
Error in query number <strong>{{query_number}}</strong>:
13-
{{/if}}
14-
</p>
15-
<pre><code>{{description}}</code></pre>
16-
{{#if backtrace}}
17-
<details class="fs-5">
18-
<summary>Backtrace</summary>
19-
{{~#each backtrace~}}
20-
<pre class="fs-5 mt-1 p-1 my-1"><code>{{this}}</code></pre>
21-
{{~/each~}}
22-
</details>
9+
<p>
10+
{{#if query_number}}
11+
Error in query number <strong>{{query_number}}</strong>:
2312
{{/if}}
24-
{{/each_row}}
13+
</p>
14+
<pre><code>{{description}}</code></pre>
15+
{{#if backtrace}}
16+
<details open class="fs-5">
17+
<summary>Backtrace</summary>
18+
{{~#each backtrace~}}
19+
<pre class="fs-5 mt-1 p-1 my-1"><code>{{this}}</code></pre>
20+
{{~/each~}}
21+
</details>
22+
{{/if}}
2523
</div>

src/render.rs

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ impl<W: std::io::Write> HeaderContext<W> {
5656
}
5757
}
5858

59+
pub async fn handle_error(self, err: anyhow::Error) -> anyhow::Result<PageContext<W>> {
60+
log::debug!("Handling header error: {err}");
61+
let data = json!({
62+
"component": "error",
63+
"description": err.to_string(),
64+
"backtrace": get_backtrace(&err),
65+
});
66+
self.start_body(data).await
67+
}
68+
5969
fn status_code(mut self, data: &JsonValue) -> anyhow::Result<Self> {
6070
let status_code = data
6171
.as_object()
@@ -209,6 +219,16 @@ impl<W: std::io::Write> HeaderContext<W> {
209219
}
210220
}
211221

222+
fn get_backtrace(error: &anyhow::Error) -> Vec<String> {
223+
let mut backtrace = vec![];
224+
let mut source = error.source();
225+
while let Some(s) = source {
226+
backtrace.push(format!("{s}"));
227+
source = s.source();
228+
}
229+
backtrace
230+
}
231+
212232
fn get_object_str<'a>(json: &'a JsonValue, key: &str) -> Option<&'a str> {
213233
json.as_object()
214234
.and_then(|obj| obj.get(key))
@@ -278,8 +298,9 @@ impl<W: std::io::Write> RenderContext<W> {
278298

279299
if let Some(component) = initial_component {
280300
log::trace!("The page starts with a component without a shell: {component}");
281-
initial_context.open_component(component).await?;
282-
initial_context.handle_row(&initial_row).await?;
301+
initial_context
302+
.open_component_with_data(component, &initial_row)
303+
.await?;
283304
}
284305

285306
Ok(initial_context)
@@ -367,20 +388,13 @@ impl<W: std::io::Write> RenderContext<W> {
367388
pub async fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> {
368389
log::warn!("SQL error: {:?}", error);
369390
self.close_component()?;
370-
let saved_component = self.open_component("error").await?;
371391
let description = error.to_string();
372-
let mut backtrace = vec![];
373-
let mut source = error.source();
374-
while let Some(s) = source {
375-
backtrace.push(format!("{s}"));
376-
source = s.source();
377-
}
378-
self.render_current_template_with_data(&json!({
392+
let data = json!({
379393
"query_number": self.current_statement,
380394
"description": description,
381-
"backtrace": backtrace
382-
}))
383-
.await?;
395+
"backtrace": get_backtrace(error)
396+
});
397+
let saved_component = self.open_component_with_data("error", &data).await?;
384398
self.close_component()?;
385399
self.current_component = saved_component;
386400
Ok(())
@@ -416,13 +430,6 @@ impl<W: std::io::Write> RenderContext<W> {
416430
Ok(())
417431
}
418432

419-
async fn open_component(
420-
&mut self,
421-
component: &str,
422-
) -> anyhow::Result<Option<SplitTemplateRenderer>> {
423-
self.open_component_with_data(component, &json!(null)).await
424-
}
425-
426433
async fn create_renderer(
427434
component: &str,
428435
app_state: Arc<AppState>,
@@ -519,7 +526,13 @@ impl SplitTemplateRenderer {
519526
writer: W,
520527
data: JsonValue,
521528
) -> Result<(), RenderError> {
522-
log::trace!("Starting rendering of a new page with the following page-level data: {data}");
529+
log::trace!(
530+
"Starting rendering of a template{} with the following top-level parameters: {data}",
531+
self.split_template
532+
.name()
533+
.map(|n| format!(" ('{n}')"))
534+
.unwrap_or_default(),
535+
);
523536
let mut render_context = handlebars::RenderContext::new(None);
524537
*self.ctx.data_mut() = data;
525538
let mut output = HandlebarWriterOutput(writer);
@@ -569,7 +582,13 @@ impl SplitTemplateRenderer {
569582
}
570583

571584
fn render_end<W: std::io::Write>(&mut self, writer: W) -> Result<(), RenderError> {
572-
log::trace!("Closing the current page");
585+
log::trace!(
586+
"Closing a template {}",
587+
self.split_template
588+
.name()
589+
.map(|n| format!("('{n}')"))
590+
.unwrap_or_default(),
591+
);
573592
if let Some(local_vars) = self.local_vars.take() {
574593
let mut render_context = handlebars::RenderContext::new(None);
575594
*render_context

src/templates.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ pub struct SplitTemplate {
1919
pub after_list: Template,
2020
}
2121

22+
impl SplitTemplate {
23+
pub fn name(&self) -> Option<&str> {
24+
self.before_list.name.as_deref()
25+
}
26+
}
27+
2228
pub fn split_template(mut original: Template) -> SplitTemplate {
2329
let mut elements_after = Vec::new();
2430
let mut mapping_after = Vec::new();
@@ -287,8 +293,14 @@ impl AllTemplates {
287293
for file in STATIC_TEMPLATES.files() {
288294
let mut path = PathBuf::from(TEMPLATES_DIR);
289295
path.push(file.path());
296+
let name = file
297+
.path()
298+
.file_stem()
299+
.unwrap()
300+
.to_string_lossy()
301+
.to_string();
290302
let source = String::from_utf8_lossy(file.contents());
291-
let tpl = Template::compile(&source)?;
303+
let tpl = Template::compile_with_name(&source, name)?;
292304
let split_template = split_template(tpl);
293305
self.split_templates.add_static(path, split_template);
294306
}

src/webserver/database/sql.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ fn parse_single_statement(parser: &mut Parser<'_>, db_kind: AnyKind) -> Option<P
119119

120120
fn syntax_error(err: ParserError, parser: &mut Parser) -> ParsedStatement {
121121
let mut err_msg = "SQL syntax error before: ".to_string();
122+
parser.prev_token(); // go back to the token that caused the error
122123
for _ in 0..32 {
123124
let next_token = parser.next_token();
124125
if next_token == EOF {

src/webserver/http.rs

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -153,29 +153,33 @@ async fn build_response_header_and_stream<S: Stream<Item = DbItem>>(
153153
let mut head_context = HeaderContext::new(app_state, writer);
154154
let mut stream = Box::pin(database_entries);
155155
while let Some(item) = stream.next().await {
156-
match item {
157-
DbItem::Row(data) => match head_context.handle_row(data).await? {
158-
PageContext::Header(h) => {
159-
head_context = h;
160-
}
161-
PageContext::Body {
162-
mut http_response,
156+
let page_context = match item {
157+
DbItem::Row(data) => head_context.handle_row(data).await?,
158+
DbItem::FinishedQuery => {
159+
log::debug!("finished query");
160+
continue;
161+
}
162+
DbItem::Error(source_err) => head_context.handle_error(source_err).await?,
163+
};
164+
match page_context {
165+
PageContext::Header(h) => {
166+
head_context = h;
167+
}
168+
PageContext::Body {
169+
mut http_response,
170+
renderer,
171+
} => {
172+
let body_stream = tokio_stream::wrappers::ReceiverStream::new(receiver);
173+
let http_response = http_response.streaming(body_stream);
174+
return Ok(ResponseWithWriter::RenderStream {
175+
http_response,
163176
renderer,
164-
} => {
165-
let body_stream = tokio_stream::wrappers::ReceiverStream::new(receiver);
166-
let http_response = http_response.streaming(body_stream);
167-
return Ok(ResponseWithWriter::RenderStream {
168-
http_response,
169-
renderer,
170-
database_entries_stream: stream,
171-
});
172-
}
173-
PageContext::Close(http_response) => {
174-
return Ok(ResponseWithWriter::FinishedResponse { http_response })
175-
}
176-
},
177-
DbItem::FinishedQuery => log::debug!("finished query"),
178-
DbItem::Error(source_err) => return Err(source_err),
177+
database_entries_stream: stream,
178+
});
179+
}
180+
PageContext::Close(http_response) => {
181+
return Ok(ResponseWithWriter::FinishedResponse { http_response })
182+
}
179183
}
180184
}
181185
log::debug!("No SQL statements left to execute for the body of the response");

0 commit comments

Comments
 (0)