diff --git a/README.md b/README.md index d463809..f4d2ebd 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/build.zig.zon b/build.zig.zon index e91e8a8..8611165 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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 diff --git a/src/app/views/index.zig b/src/app/views/index.zig index ad1eb80..84eaa6f 100644 --- a/src/app/views/index.zig +++ b/src/app/views/index.zig @@ -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); } diff --git a/src/app/views/options.zig b/src/app/views/options.zig index 013d570..1123b45 100644 --- a/src/app/views/options.zig +++ b/src/app/views/options.zig @@ -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| { - try request.session.put("count", data.integer(value.integer.value + 1)); - try object.put("count", data.integer(value.integer.value + 1)); + 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,7 +31,11 @@ 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| { - try object.put("count", data.integer(value.integer.value + 1)); + 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)); } diff --git a/src/jetzig.zig b/src/jetzig.zig index 0ca86e3..96ddbd1 100644 --- a/src/jetzig.zig +++ b/src/jetzig.zig @@ -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, diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 6fdafcc..ed00fd1 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -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()); diff --git a/src/jetzig/http/status_codes.zig b/src/jetzig/http/status_codes.zig index 2f70e28..535c630 100644 --- a/src/jetzig/http/status_codes.zig +++ b/src/jetzig/http/status_codes.zig @@ -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();