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 {