Error handling

This commit is contained in:
Bob Farrell 2024-01-18 21:05:17 +00:00
parent 65bb626bbc
commit cb52ccffb9
7 changed files with 194 additions and 22 deletions

View File

@ -18,10 +18,13 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* :white_check_mark: _JSON_-compatible response data builder.
* :white_check_mark: _HTML_ templating (see [Zmpl](https://github.com/bobf/zmpl)).
* :white_check_mark: Per-request arena allocator.
* :x: Sessions.
* :x: Cookies.
* :x: Headers.
* :white_check_mark: Sessions.
* :white_check_mark: Cookies.
* :x: Error handling.
* :x: Headers (available but not yet wrapped).
* :x: Param/JSON payload parsing/abstracting.
* :x: Development-mode responses for debugging.
* :x: Environment configurations (develompent/production/etc.)
* :x: Middleware extensions (for e.g. authentication).
* :x: Email delivery.
* :x: Custom/dynamic routes.

View File

@ -22,7 +22,7 @@
// the new URL.
//
.url = "https://github.com/bobf/zmpl/archive/refs/tags/0.0.1.tar.gz",
.hash = "12205f95df0bf7a66c9d00ed76f8c3b1548eb958f9210425045c77f88ea165f20fef",
.hash = "12204256376f262a58935d66a2a0b41ac0447299b7e63a4c6ff160ddcef6572cd3c7",
// This is computed from the file contents of the directory of files that is
// obtained after fetching `url` and applying the inclusion rules given by

View File

@ -6,7 +6,9 @@ const Data = jetzig.data.Data;
const View = jetzig.views.View;
pub fn index(request: *Request, data: *Data) anyerror!View {
_ = request;
var object = try data.object();
try object.put("foo", data.string("hello"));
return request.render(.ok);
return error.OhNo;
// return request.render(.ok);
}

View File

@ -12,8 +12,12 @@ pub fn put(id: []const u8, request: *Request, data: *Data) anyerror!View {
const count = try request.session.get("count");
if (count) |value| {
if (value == .integer) {
try request.session.put("count", data.integer(value.integer.value + 1));
try object.put("count", data.integer(value.integer.value + 1));
} else {
return error.InvalidSessionData;
}
} else {
try request.session.put("count", data.integer(0));
try object.put("count", data.integer(0));
@ -27,10 +31,14 @@ pub fn get(id: []const u8, request: *Request, data: *Data) anyerror!View {
var object = try data.object();
const count = try request.session.get("count");
if (count) |value| {
if (value == .integer) {
try object.put("count", data.integer(value.integer.value + 1));
} else {
try object.put("count", data.integer(0));
}
} else {
try object.put("count", data.integer(0));
}
}
return request.render(.ok);
}

View File

@ -36,8 +36,12 @@ pub fn init(allocator: std.mem.Allocator) !App {
}
};
const logger = loggers.Logger{ .development_logger = loggers.DevelopmentLogger.init(allocator) };
var logger = loggers.Logger{ .development_logger = loggers.DevelopmentLogger.init(allocator) };
const secret = try generateSecret(allocator);
logger.debug(
"Running in development mode, using auto-generated cookie encryption key:\n {s}",
.{secret},
);
const server_options = http.Server.ServerOptions{
.cache = server_cache,

View File

@ -68,8 +68,8 @@ fn processRequests(self: *Self) !void {
while (response.reset() != .closing) {
self.processNextRequest(&response) catch |err| {
switch (err) {
error.EndOfStream => continue,
error.ConnectionResetByPeer => continue,
error.EndOfStream, error.ConnectionResetByPeer => continue,
error.UnknownHttpMethod => continue, // TODO: Render 400 Bad Request here ?
else => return err,
}
};
@ -100,8 +100,7 @@ fn processNextRequest(self: *Self, response: *std.http.Server.Response) !void {
try response.headers.append("Set-Cookie", header);
}
response.status = switch (result.value.status_code) {
.ok => .ok,
.not_found => .not_found,
inline else => |status_code| @field(std.http.Status, @tagName(status_code)),
};
try response.do();
@ -148,9 +147,12 @@ fn renderHTML(
// FIXME: Tidy this up and use a hashmap for templates (or a more comprehensive
// matching system) instead of an array.
if (std.mem.eql(u8, expected_name, template.name)) {
const view = try matched_route.render(matched_route, request);
const content = try template.render(view.data);
return .{ .allocator = self.allocator, .content = content, .status_code = .ok };
const rendered = try self.renderView(matched_route, request, template);
return .{
.allocator = self.allocator,
.content = rendered.content,
.status_code = rendered.view.status_code,
};
}
}
@ -174,12 +176,15 @@ fn renderJSON(
route: ?jetzig.views.Route,
) !jetzig.http.Response {
if (route) |matched_route| {
const view = try matched_route.render(matched_route, request);
var data = view.data;
const rendered = try self.renderView(matched_route, request, null);
var data = rendered.view.data;
if (data.value) |_| {} else _ = try data.object();
return .{
.allocator = self.allocator,
.content = try data.toJson(),
.status_code = .ok,
.status_code = rendered.view.status_code,
};
} else return .{
.allocator = self.allocator,
@ -188,10 +193,42 @@ fn renderJSON(
};
}
const RenderedView = struct { view: jetzig.views.View, content: []const u8 };
fn renderView(
self: *Self,
matched_route: jetzig.views.Route,
request: *jetzig.http.Request,
template: ?jetzig.TemplateFn,
) !RenderedView {
const view = matched_route.render(matched_route, request) catch |err| {
switch (err) {
error.OutOfMemory => return err,
else => return try self.internalServerError(request, err),
}
};
const content = if (template) |capture| try capture.render(view.data) else "";
return .{ .view = view, .content = content };
}
fn internalServerError(self: *Self, request: *jetzig.http.Request, err: anyerror) !RenderedView {
_ = self;
request.response_data.reset();
var object = try request.response_data.object();
try object.put("error", request.response_data.string(@errorName(err)));
return .{
.view = jetzig.views.View{ .data = request.response_data, .status_code = .internal_server_error },
.content = "An unexpected error occurred.",
};
}
fn requestLogMessage(self: *Self, request: *jetzig.http.Request, result: jetzig.caches.Result) ![]const u8 {
const status: jetzig.http.status_codes.TaggedStatusCode = switch (result.value.status_code) {
.ok => .{ .ok = .{} },
.not_found => .{ .not_found = .{} },
inline else => |status_code| @unionInit(
jetzig.http.status_codes.TaggedStatusCode,
@tagName(status_code),
.{},
),
};
const formatted_duration = try jetzig.colors.duration(self.allocator, self.duration());

View File

@ -3,8 +3,67 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig");
pub const StatusCode = enum {
@"continue",
switching_protocols,
processing,
early_hints,
ok,
created,
accepted,
non_authoritative_info,
no_content,
reset_content,
partial_content,
multi_status,
already_reported,
im_used,
multiple_choice,
moved_permanently,
found,
see_other,
not_modified,
use_proxy,
temporary_redirect,
permanent_redirect,
bad_request,
unauthorized,
payment_required,
forbidden,
not_found,
method_not_allowed,
not_acceptable,
proxy_auth_required,
request_timeout,
conflict,
gone,
length_required,
precondition_failed,
payload_too_large,
uri_too_long,
unsupported_media_type,
range_not_satisfiable,
expectation_failed,
misdirected_request,
unprocessable_entity,
locked,
failed_dependency,
too_early,
upgrade_required,
precondition_required,
too_many_requests,
request_header_fields_too_large,
unavailable_for_legal_reasons,
internal_server_error,
not_implemented,
bad_gateway,
service_unavailable,
gateway_timeout,
http_version_not_supported,
variant_also_negotiates,
insufficient_storage,
loop_detected,
not_extended,
network_authentication_required,
};
pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) type {
@ -35,8 +94,67 @@ pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) t
}
pub const TaggedStatusCode = union(StatusCode) {
ok: StatusCodeType("200", "OK"),
@"continue": StatusCodeType("100", "Continue"),
switching_protocols: StatusCodeType("101", "Switching Protocols"),
processing: StatusCodeType("102", "Processing"),
early_hints: StatusCodeType("103", "Early Hints"),
ok: StatusCodeType("200", "Ok"),
created: StatusCodeType("201", "Created"),
accepted: StatusCodeType("202", "Accepted"),
non_authoritative_info: StatusCodeType("203", "Non Authoritative Information"),
no_content: StatusCodeType("204", "No Content"),
reset_content: StatusCodeType("205", "Reset Content"),
partial_content: StatusCodeType("206", "Partial Content"),
multi_status: StatusCodeType("207", "Multi Status"),
already_reported: StatusCodeType("208", "Already Reported"),
im_used: StatusCodeType("226", "IM Used"),
multiple_choice: StatusCodeType("300", "Multiple Choices"),
moved_permanently: StatusCodeType("301", "Moved Permanently"),
found: StatusCodeType("302", "Found"),
see_other: StatusCodeType("303", "See Other"),
not_modified: StatusCodeType("304", "Not Modified"),
use_proxy: StatusCodeType("305", "Use Proxy"),
temporary_redirect: StatusCodeType("307", "Temporary Redirect"),
permanent_redirect: StatusCodeType("308", "Permanent Redirect"),
bad_request: StatusCodeType("400", "Bad Request"),
unauthorized: StatusCodeType("401", "Unauthorized"),
payment_required: StatusCodeType("402", "Payment Required"),
forbidden: StatusCodeType("403", "Forbidden"),
not_found: StatusCodeType("404", "Not Found"),
method_not_allowed: StatusCodeType("405", "Method Not Allowed"),
not_acceptable: StatusCodeType("406", "Not Acceptable"),
proxy_auth_required: StatusCodeType("407", "Proxy Authentication Required"),
request_timeout: StatusCodeType("408", "Request Timeout"),
conflict: StatusCodeType("409", "Conflict"),
gone: StatusCodeType("410", "Gone"),
length_required: StatusCodeType("411", "Length Required"),
precondition_failed: StatusCodeType("412", "Precondition Failed"),
payload_too_large: StatusCodeType("413", "Payload Too Large"),
uri_too_long: StatusCodeType("414", "Request Uri Too Long"),
unsupported_media_type: StatusCodeType("415", "Unsupported Media Type"),
range_not_satisfiable: StatusCodeType("416", "Requested Range Not Satisfiable"),
expectation_failed: StatusCodeType("417", "Expectation Failed"),
misdirected_request: StatusCodeType("421", "Misdirected Request"),
unprocessable_entity: StatusCodeType("422", "Unprocessable Entity"),
locked: StatusCodeType("423", "Locked"),
failed_dependency: StatusCodeType("424", "Failed Dependency"),
too_early: StatusCodeType("425", "Too Early"),
upgrade_required: StatusCodeType("426", "Upgrade Required"),
precondition_required: StatusCodeType("428", "Precondition Required"),
too_many_requests: StatusCodeType("429", "Too Many Requests"),
request_header_fields_too_large: StatusCodeType("431", "Request Header Fields Too Large"),
unavailable_for_legal_reasons: StatusCodeType("451", "Unavailable for Legal Reasons"),
internal_server_error: StatusCodeType("500", "Internal Server Error"),
not_implemented: StatusCodeType("501", "Not Implemented"),
bad_gateway: StatusCodeType("502", "Bad Gateway"),
service_unavailable: StatusCodeType("503", "Service Unavailable"),
gateway_timeout: StatusCodeType("504", "Gateway Timeout"),
http_version_not_supported: StatusCodeType("505", "Http Version Not Supported"),
variant_also_negotiates: StatusCodeType("506", "Variant Also Negotiates"),
insufficient_storage: StatusCodeType("507", "Insufficient Storage"),
loop_detected: StatusCodeType("508", "Loop Detected"),
not_extended: StatusCodeType("510", "Not Extended"),
network_authentication_required: StatusCodeType("511", "Network Authentication Required"),
const Self = @This();