diff --git a/demo/public/404.html b/demo/public/404.html
new file mode 100644
index 0000000..d75860c
--- /dev/null
+++ b/demo/public/404.html
@@ -0,0 +1,16 @@
+
+
+

+
404
+
diff --git a/demo/src/app/views/errors.zig b/demo/src/app/views/errors.zig
new file mode 100644
index 0000000..ccc1082
--- /dev/null
+++ b/demo/src/app/views/errors.zig
@@ -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);
+}
diff --git a/demo/src/app/views/errors/index.zmpl b/demo/src/app/views/errors/index.zmpl
new file mode 100644
index 0000000..f001d9b
--- /dev/null
+++ b/demo/src/app/views/errors/index.zmpl
@@ -0,0 +1,17 @@
+
+
+

+
{{.error.code}}
+
{{.error.message}}
+
diff --git a/src/jetzig/http/Headers.zig b/src/jetzig/http/Headers.zig
index ec65a02..0e2388a 100644
--- a/src/jetzig/http/Headers.zig
+++ b/src/jetzig/http/Headers.zig
@@ -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);
}
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig
index a8d5dee..16f9532 100644
--- a/src/jetzig/http/Request.zig
+++ b/src/jetzig/http/Request.zig
@@ -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 }),
};
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index 1c98d19..17ebbeb 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -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 {