mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
Custom error pages
Render `src/app/views/errors.zig` view (`index` action) when an unexpected error occurs. If the view does not exist, try rendering `public/404.html`, `public/500.html` (etc.), finally falling back to a standard basic HTML/JSON response.
This commit is contained in:
parent
0c450ff9b0
commit
1c7df8a91d
16
demo/public/404.html
Normal file
16
demo/public/404.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<style>
|
||||||
|
div {
|
||||||
|
padding: 15px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
-ms-transform: translateX(-50%) translateY(-50%);
|
||||||
|
-webkit-transform: translate(-50%,-50%);
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div>
|
||||||
|
<img src="/jetzig.png">
|
||||||
|
<h1 style="font-size: 10rem; font-family: sans-serif; color: #f7931e">404</h1>
|
||||||
|
</div>
|
20
demo/src/app/views/errors.zig
Normal file
20
demo/src/app/views/errors.zig
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const jetzig = @import("jetzig");
|
||||||
|
|
||||||
|
// Generic handler for all errors.
|
||||||
|
// Use `jetzig.http.status_codes.get(request.status_code)` to get a value that provides string
|
||||||
|
// versions of the error code and message for use in templates.
|
||||||
|
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||||
|
var root = try data.object();
|
||||||
|
var error_info = try data.object();
|
||||||
|
|
||||||
|
const status = jetzig.http.status_codes.get(request.status_code);
|
||||||
|
|
||||||
|
try error_info.put("code", data.string(status.getCode()));
|
||||||
|
try error_info.put("message", data.string(status.getMessage()));
|
||||||
|
|
||||||
|
try root.put("error", error_info);
|
||||||
|
|
||||||
|
// Render with the original error status code, or override if preferred.
|
||||||
|
return request.render(request.status_code);
|
||||||
|
}
|
17
demo/src/app/views/errors/index.zmpl
Normal file
17
demo/src/app/views/errors/index.zmpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<style>
|
||||||
|
div {
|
||||||
|
padding: 15px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
-ms-transform: translateX(-50%) translateY(-50%);
|
||||||
|
-webkit-transform: translate(-50%,-50%);
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div>
|
||||||
|
<img src="/jetzig.png" />
|
||||||
|
<h1 style="font-size: 6rem; font-family: sans-serif; color: #f7931e">{{.error.code}}</h1>
|
||||||
|
<h1 style="font-size: 4rem; font-family: sans-serif; color: #39b54a">{{.error.message}}</h1>
|
||||||
|
</div>
|
@ -40,7 +40,7 @@ pub fn getAll(self: Headers, name: []const u8) []const []const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
pub fn getFirstValue(self: *Headers, name: []const u8) ?[]const u8 {
|
pub fn getFirstValue(self: *const Headers, name: []const u8) ?[]const u8 {
|
||||||
return self.get(name);
|
return self.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ pub fn redirect(
|
|||||||
/// * `Accept` header (`application/json` or `text/html`)
|
/// * `Accept` header (`application/json` or `text/html`)
|
||||||
/// * `Content-Type` header (`application/json` or `text/html`)
|
/// * `Content-Type` header (`application/json` or `text/html`)
|
||||||
/// * Fall back to default: HTML
|
/// * Fall back to default: HTML
|
||||||
pub fn requestFormat(self: *Request) jetzig.http.Request.Format {
|
pub fn requestFormat(self: *const Request) jetzig.http.Request.Format {
|
||||||
return self.extensionFormat() orelse
|
return self.extensionFormat() orelse
|
||||||
self.acceptHeaderFormat() orelse
|
self.acceptHeaderFormat() orelse
|
||||||
self.contentTypeHeaderFormat() orelse
|
self.contentTypeHeaderFormat() orelse
|
||||||
@ -211,7 +211,7 @@ pub fn getLayout(self: *Request, route: *jetzig.views.Route) ?[]const u8 {
|
|||||||
|
|
||||||
/// Shortcut for `request.headers.getFirstValue`. Returns the first matching value for a given
|
/// Shortcut for `request.headers.getFirstValue`. Returns the first matching value for a given
|
||||||
/// header name or `null` if not found. Header names are case-insensitive.
|
/// header name or `null` if not found. Header names are case-insensitive.
|
||||||
pub fn getHeader(self: *Request, key: []const u8) ?[]const u8 {
|
pub fn getHeader(self: *const Request, key: []const u8) ?[]const u8 {
|
||||||
return self.headers.getFirstValue(key);
|
return self.headers.getFirstValue(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,7 +389,7 @@ pub fn mail(self: *Request, name: []const u8, mail_params: jetzig.mail.MailParam
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extensionFormat(self: *Request) ?jetzig.http.Request.Format {
|
fn extensionFormat(self: *const Request) ?jetzig.http.Request.Format {
|
||||||
const extension = self.path.extension orelse return null;
|
const extension = self.path.extension orelse return null;
|
||||||
if (std.mem.eql(u8, extension, ".html")) {
|
if (std.mem.eql(u8, extension, ".html")) {
|
||||||
return .HTML;
|
return .HTML;
|
||||||
@ -400,7 +400,7 @@ fn extensionFormat(self: *Request) ?jetzig.http.Request.Format {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn acceptHeaderFormat(self: *Request) ?jetzig.http.Request.Format {
|
pub fn acceptHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
||||||
const acceptHeader = self.getHeader("Accept");
|
const acceptHeader = self.getHeader("Accept");
|
||||||
|
|
||||||
if (acceptHeader) |item| {
|
if (acceptHeader) |item| {
|
||||||
@ -411,7 +411,7 @@ pub fn acceptHeaderFormat(self: *Request) ?jetzig.http.Request.Format {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contentTypeHeaderFormat(self: *Request) ?jetzig.http.Request.Format {
|
pub fn contentTypeHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
||||||
const acceptHeader = self.getHeader("content-type");
|
const acceptHeader = self.getHeader("content-type");
|
||||||
|
|
||||||
if (acceptHeader) |item| {
|
if (acceptHeader) |item| {
|
||||||
@ -447,13 +447,15 @@ pub fn fmtMethod(self: *const Request, colorized: bool) []const u8 {
|
|||||||
/// Format a status code appropriately for the current request format.
|
/// Format a status code appropriately for the current request format.
|
||||||
/// e.g. `.HTML` => `404 Not Found`
|
/// e.g. `.HTML` => `404 Not Found`
|
||||||
/// `.JSON` => `{ "message": "Not Found", "status": "404" }`
|
/// `.JSON` => `{ "message": "Not Found", "status": "404" }`
|
||||||
pub fn formatStatus(self: *Request, status_code: jetzig.http.StatusCode) ![]const u8 {
|
pub fn formatStatus(self: *const Request, status_code: jetzig.http.StatusCode) ![]const u8 {
|
||||||
const status = jetzig.http.status_codes.get(status_code);
|
const status = jetzig.http.status_codes.get(status_code);
|
||||||
|
|
||||||
return switch (self.requestFormat()) {
|
return switch (self.requestFormat()) {
|
||||||
.JSON => try std.json.stringifyAlloc(self.allocator, .{
|
.JSON => try std.json.stringifyAlloc(self.allocator, .{
|
||||||
.message = status.getMessage(),
|
.@"error" = .{
|
||||||
.status = status.getCode(),
|
.message = status.getMessage(),
|
||||||
|
.code = status.getCode(),
|
||||||
|
},
|
||||||
}, .{}),
|
}, .{}),
|
||||||
.HTML, .UNKNOWN => status.getFormatted(.{ .linebreak = true }),
|
.HTML, .UNKNOWN => status.getFormatted(.{ .linebreak = true }),
|
||||||
};
|
};
|
||||||
|
@ -162,13 +162,13 @@ fn renderHTML(
|
|||||||
};
|
};
|
||||||
return request.setResponse(rendered, .{});
|
return request.setResponse(rendered, .{});
|
||||||
} else {
|
} else {
|
||||||
return request.setResponse(try renderNotFound(request), .{});
|
return request.setResponse(try self.renderNotFound(request), .{});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (try self.renderMarkdown(request)) |rendered| {
|
if (try self.renderMarkdown(request)) |rendered| {
|
||||||
return request.setResponse(rendered, .{});
|
return request.setResponse(rendered, .{});
|
||||||
} else {
|
} else {
|
||||||
return request.setResponse(try renderNotFound(request), .{});
|
return request.setResponse(try self.renderNotFound(request), .{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,7 +191,7 @@ fn renderJSON(
|
|||||||
|
|
||||||
request.setResponse(rendered, .{});
|
request.setResponse(rendered, .{});
|
||||||
} else {
|
} else {
|
||||||
request.setResponse(try renderNotFound(request), .{});
|
request.setResponse(try self.renderNotFound(request), .{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ fn renderView(
|
|||||||
_ = route.render(route.*, request) catch |err| {
|
_ = route.render(route.*, request) catch |err| {
|
||||||
try self.logger.ERROR("Encountered error: {s}", .{@errorName(err)});
|
try self.logger.ERROR("Encountered error: {s}", .{@errorName(err)});
|
||||||
if (isUnhandledError(err)) return err;
|
if (isUnhandledError(err)) return err;
|
||||||
if (isBadRequest(err)) return try renderBadRequest(request);
|
if (isBadRequest(err)) return try self.renderBadRequest(request);
|
||||||
return try self.renderInternalServerError(request, err);
|
return try self.renderInternalServerError(request, err);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -239,7 +239,7 @@ fn renderView(
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return switch (request.requestFormat()) {
|
return switch (request.requestFormat()) {
|
||||||
.HTML, .UNKNOWN => try renderNotFound(request),
|
.HTML, .UNKNOWN => try self.renderNotFound(request),
|
||||||
.JSON => .{ .view = rendered_view, .content = "" },
|
.JSON => .{ .view = rendered_view, .content = "" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -326,37 +326,117 @@ fn renderInternalServerError(self: *Server, request: *jetzig.http.Request, err:
|
|||||||
if (stack) |capture| try self.logStackTrace(capture, request);
|
if (stack) |capture| try self.logStackTrace(capture, request);
|
||||||
|
|
||||||
const status = .internal_server_error;
|
const status = .internal_server_error;
|
||||||
const content = try request.formatStatus(status);
|
return try self.renderError(request, status);
|
||||||
return .{
|
|
||||||
.view = jetzig.views.View{ .data = request.response_data, .status_code = status },
|
|
||||||
.content = content,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn renderNotFound(request: *jetzig.http.Request) !RenderedView {
|
fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView {
|
||||||
request.response_data.reset();
|
request.response_data.reset();
|
||||||
|
|
||||||
const status: jetzig.http.StatusCode = .not_found;
|
const status: jetzig.http.StatusCode = .not_found;
|
||||||
const content = try request.formatStatus(status);
|
return try self.renderError(request, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderBadRequest(self: *Server, request: *jetzig.http.Request) !RenderedView {
|
||||||
|
request.response_data.reset();
|
||||||
|
|
||||||
|
const status: jetzig.http.StatusCode = .bad_request;
|
||||||
|
return try self.renderError(request, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderError(
|
||||||
|
self: Server,
|
||||||
|
request: *jetzig.http.Request,
|
||||||
|
status_code: jetzig.http.StatusCode,
|
||||||
|
) !RenderedView {
|
||||||
|
if (try self.renderErrorView(request, status_code)) |view| return view;
|
||||||
|
if (try renderStaticErrorPage(request, status_code)) |view| return view;
|
||||||
|
|
||||||
|
return try renderDefaultError(request, status_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderErrorView(
|
||||||
|
self: Server,
|
||||||
|
request: *jetzig.http.Request,
|
||||||
|
status_code: jetzig.http.StatusCode,
|
||||||
|
) !?RenderedView {
|
||||||
|
for (self.routes) |route| {
|
||||||
|
if (std.mem.eql(u8, route.view_name, "errors") and route.action == .index) {
|
||||||
|
request.response_data.reset();
|
||||||
|
request.status_code = status_code;
|
||||||
|
|
||||||
|
_ = route.render(route.*, request) catch |err| {
|
||||||
|
if (isUnhandledError(err)) return err;
|
||||||
|
try self.logger.ERROR(
|
||||||
|
"Unexepected error occurred while rendering error page: {s}",
|
||||||
|
.{@errorName(err)},
|
||||||
|
);
|
||||||
|
const stack = @errorReturnTrace();
|
||||||
|
if (stack) |capture| try self.logStackTrace(capture, request);
|
||||||
|
return try renderDefaultError(request, status_code);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.rendered_view) |view| {
|
||||||
|
switch (request.requestFormat()) {
|
||||||
|
.HTML, .UNKNOWN => {
|
||||||
|
if (zmpl.findPrefixed("views", route.template)) |template| {
|
||||||
|
try addTemplateConstants(view, route);
|
||||||
|
return .{ .view = view, .content = try template.render(request.response_data) };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.JSON => return .{ .view = view, .content = try request.response_data.toJson() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderStaticErrorPage(request: *jetzig.http.Request, status_code: jetzig.http.StatusCode) !?RenderedView {
|
||||||
|
if (request.requestFormat() == .JSON) return null;
|
||||||
|
|
||||||
|
var dir = std.fs.cwd().openDir(
|
||||||
|
jetzig.config.get([]const u8, "public_content_path"),
|
||||||
|
.{ .iterate = false, .no_follow = true },
|
||||||
|
) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.FileNotFound => return null,
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
const status = jetzig.http.status_codes.get(status_code);
|
||||||
|
const content = dir.readFileAlloc(
|
||||||
|
request.allocator,
|
||||||
|
try std.mem.concat(request.allocator, u8, &.{ status.getCode(), ".html" }),
|
||||||
|
jetzig.config.get(usize, "max_bytes_public_content"),
|
||||||
|
) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.FileNotFound => return null,
|
||||||
|
else => return err,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.view = .{ .data = request.response_data, .status_code = status },
|
.view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
|
||||||
.content = content,
|
.content = content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn renderBadRequest(request: *jetzig.http.Request) !RenderedView {
|
fn renderDefaultError(
|
||||||
request.response_data.reset();
|
request: *const jetzig.http.Request,
|
||||||
|
status_code: jetzig.http.StatusCode,
|
||||||
const status: jetzig.http.StatusCode = .bad_request;
|
) !RenderedView {
|
||||||
const content = try request.formatStatus(status);
|
const content = try request.formatStatus(status_code);
|
||||||
return .{
|
return .{
|
||||||
.view = jetzig.views.View{ .data = request.response_data, .status_code = status },
|
.view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
|
||||||
.content = content,
|
.content = content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logStackTrace(
|
fn logStackTrace(
|
||||||
self: *Server,
|
self: Server,
|
||||||
stack: *std.builtin.StackTrace,
|
stack: *std.builtin.StackTrace,
|
||||||
request: *jetzig.http.Request,
|
request: *jetzig.http.Request,
|
||||||
) !void {
|
) !void {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user