Merge pull request #60 from jetzig-framework/custom-error-pages

Custom error pages
This commit is contained in:
bobf 2024-04-28 12:01:29 +01:00 committed by GitHub
commit f55a3278ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 164 additions and 29 deletions

16
demo/public/404.html Normal file
View 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>

View 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);
}

View 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>

View File

@ -40,7 +40,7 @@ pub fn getAll(self: Headers, name: []const u8) []const []const u8 {
}
// 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);
}

View File

@ -181,7 +181,7 @@ pub fn redirect(
/// * `Accept` header (`application/json` or `text/html`)
/// * `Content-Type` header (`application/json` or `text/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
self.acceptHeaderFormat() 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
/// 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);
}
@ -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;
if (std.mem.eql(u8, extension, ".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");
if (acceptHeader) |item| {
@ -411,7 +411,7 @@ pub fn acceptHeaderFormat(self: *Request) ?jetzig.http.Request.Format {
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");
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.
/// e.g. `.HTML` => `404 Not Found`
/// `.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);
return switch (self.requestFormat()) {
.JSON => try std.json.stringifyAlloc(self.allocator, .{
.message = status.getMessage(),
.status = status.getCode(),
.@"error" = .{
.message = status.getMessage(),
.code = status.getCode(),
},
}, .{}),
.HTML, .UNKNOWN => status.getFormatted(.{ .linebreak = true }),
};

View File

@ -162,13 +162,13 @@ fn renderHTML(
};
return request.setResponse(rendered, .{});
} else {
return request.setResponse(try renderNotFound(request), .{});
return request.setResponse(try self.renderNotFound(request), .{});
}
} else {
if (try self.renderMarkdown(request)) |rendered| {
return request.setResponse(rendered, .{});
} else {
return request.setResponse(try renderNotFound(request), .{});
return request.setResponse(try self.renderNotFound(request), .{});
}
}
}
@ -191,7 +191,7 @@ fn renderJSON(
request.setResponse(rendered, .{});
} else {
request.setResponse(try renderNotFound(request), .{});
request.setResponse(try self.renderNotFound(request), .{});
}
}
@ -223,7 +223,7 @@ fn renderView(
_ = route.render(route.*, request) catch |err| {
try self.logger.ERROR("Encountered error: {s}", .{@errorName(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);
};
@ -239,7 +239,7 @@ fn renderView(
};
} else {
return switch (request.requestFormat()) {
.HTML, .UNKNOWN => try renderNotFound(request),
.HTML, .UNKNOWN => try self.renderNotFound(request),
.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);
const status = .internal_server_error;
const content = try request.formatStatus(status);
return .{
.view = jetzig.views.View{ .data = request.response_data, .status_code = status },
.content = content,
};
return try self.renderError(request, status);
}
fn renderNotFound(request: *jetzig.http.Request) !RenderedView {
fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView {
request.response_data.reset();
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 .{
.view = .{ .data = request.response_data, .status_code = status },
.view = jetzig.views.View{ .data = request.response_data, .status_code = status_code },
.content = content,
};
}
fn renderBadRequest(request: *jetzig.http.Request) !RenderedView {
request.response_data.reset();
const status: jetzig.http.StatusCode = .bad_request;
const content = try request.formatStatus(status);
fn renderDefaultError(
request: *const jetzig.http.Request,
status_code: jetzig.http.StatusCode,
) !RenderedView {
const content = try request.formatStatus(status_code);
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,
};
}
fn logStackTrace(
self: *Server,
self: Server,
stack: *std.builtin.StackTrace,
request: *jetzig.http.Request,
) !void {