diff --git a/demo/src/app/middleware/DemoMiddleware.zig b/demo/src/app/middleware/DemoMiddleware.zig index 06822c7..accf8b9 100644 --- a/demo/src/app/middleware/DemoMiddleware.zig +++ b/demo/src/app/middleware/DemoMiddleware.zig @@ -16,8 +16,8 @@ const std = @import("std"); const jetzig = @import("jetzig"); /// Define any custom data fields you want to store here. Assigning to these fields in the `init` -/// function allows you to access them in the `beforeRequest` and `afterRequest` functions, where -/// they can also be modified. +/// function allows you to access them in various middleware callbacks defined below, where they +/// can also be modified. my_custom_value: []const u8, const Self = @This(); @@ -29,25 +29,34 @@ pub fn init(request: *jetzig.http.Request) !*Self { return middleware; } -/// Invoked immediately after the request head has been processed, before relevant view function -/// is processed. This gives you access to request headers but not the request body. -pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void { - request.server.logger.debug("[DemoMiddleware] my_custom_value: {s}", .{self.my_custom_value}); +/// Invoked immediately after the request is received but before it has started processing. +/// Any calls to `request.render` or `request.redirect` will prevent further processing of the +/// request, including any other middleware in the chain. +pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { + request.server.logger.debug("[DemoMiddleware:afterRequest] my_custom_value: {s}", .{self.my_custom_value}); self.my_custom_value = @tagName(request.method); } -/// Invoked immediately after the request has finished responding. Provides full access to the -/// response as well as the request. -pub fn afterRequest(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { +/// Invoked immediately before the response renders to the client. +/// The response can be modified here if needed. +pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { request.server.logger.debug( - "[DemoMiddleware] my_custom_value: {s}, response status: {s}", + "[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}", .{ self.my_custom_value, @tagName(response.status_code) }, ); } -/// Invoked after `afterRequest` is called, use this function to do any clean-up. +/// Invoked immediately after the response has been finalized and sent to the client. +/// Response data can be accessed for logging, but any modifications will have no impact. +pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { + _ = self; + _ = response; + request.server.logger.debug("[DemoMiddleware:afterResponse] response completed", .{}); +} + +/// Invoked after `afterResponse` is called. Use this function to do any clean-up. /// Note that `request.allocator` is an arena allocator, so any allocations are automatically -/// done before the next request starts processing. +/// freed before the next request starts processing. pub fn deinit(self: *Self, request: *jetzig.http.Request) void { request.allocator.destroy(self); } diff --git a/demo/src/app/views/htmx.zig b/demo/src/app/views/redirect.zig similarity index 83% rename from demo/src/app/views/htmx.zig rename to demo/src/app/views/redirect.zig index 52263bd..efef2fd 100644 --- a/demo/src/app/views/htmx.zig +++ b/demo/src/app/views/redirect.zig @@ -5,7 +5,6 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { _ = data; const params = try request.params(); if (params.get("redirect")) |location| { - std.debug.print("location: {s}\n", .{try location.toString()}); return request.redirect(try location.toString(), .moved_permanently); } diff --git a/demo/src/app/views/htmx/index.zmpl b/demo/src/app/views/redirect/index.zmpl similarity index 100% rename from demo/src/app/views/htmx/index.zmpl rename to demo/src/app/views/redirect/index.zmpl diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index f904420..b4088de 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -27,6 +27,7 @@ processed: bool = false, layout: ?[]const u8 = null, layout_disabled: bool = false, rendered: bool = false, +redirected: bool = false, rendered_multiple: bool = false, rendered_view: ?jetzig.views.View = null, @@ -127,7 +128,13 @@ pub fn respond(self: *Self) !void { try self.std_http_request.respond( self.response.content, - .{ .keep_alive = false, .extra_headers = std_response_headers.items }, + .{ + .keep_alive = false, + .status = switch (self.response.status_code) { + inline else => |tag| @field(std.http.Status, @tagName(tag)), + }, + .extra_headers = std_response_headers.items, + }, ); } @@ -149,10 +156,15 @@ pub fn render(self: *Self, status_code: jetzig.http.status_codes.StatusCode) jet /// return request.redirect("https://www.example.com/", .found); /// ``` /// The second argument must be `moved_permanently` or `found`. -pub fn redirect(self: *Self, location: []const u8, redirect_status: enum { moved_permanently, found }) jetzig.views.View { +pub fn redirect( + self: *Self, + location: []const u8, + redirect_status: enum { moved_permanently, found }, +) jetzig.views.View { if (self.rendered) self.rendered_multiple = true; self.rendered = true; + self.redirected = true; const status_code = switch (redirect_status) { .moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently, diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index ea54355..ee9dd68 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -99,13 +99,16 @@ fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server try request.process(); - var middleware_data = try jetzig.http.middleware.beforeMiddleware(&request); + var middleware_data = try jetzig.http.middleware.afterRequest(&request); try self.renderResponse(&request); try request.response.headers.append("content-type", response.content_type); + + try jetzig.http.middleware.beforeResponse(&middleware_data, &request); + try request.respond(); - try jetzig.http.middleware.afterMiddleware(&middleware_data, &request); + try jetzig.http.middleware.afterResponse(&middleware_data, &request); jetzig.http.middleware.deinit(&middleware_data, &request); const log_message = try self.requestLogMessage(&request); @@ -214,8 +217,9 @@ fn renderView( }; if (request.rendered_multiple) return error.JetzigMultipleRenderError; - if (request.rendered_view) |rendered_view| { + if (request.redirected) return .{ .view = rendered_view, .content = "" }; + if (template) |capture| { return .{ .view = rendered_view, diff --git a/src/jetzig/http/middleware.zig b/src/jetzig/http/middleware.zig index 2817f1b..965b612 100644 --- a/src/jetzig/http/middleware.zig +++ b/src/jetzig/http/middleware.zig @@ -10,7 +10,7 @@ else const MiddlewareData = std.BoundedArray(*anyopaque, middlewares.len); -pub fn beforeMiddleware(request: *jetzig.http.Request) !MiddlewareData { +pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData { var middleware_data = MiddlewareData.init(0) catch unreachable; inline for (middlewares, 0..) |middleware, index| { @@ -20,27 +20,6 @@ pub fn beforeMiddleware(request: *jetzig.http.Request) !MiddlewareData { middleware_data.insert(index, data) catch unreachable; } - inline for (middlewares, 0..) |middleware, index| { - if (comptime !@hasDecl(middleware, "beforeRequest")) continue; - if (comptime @hasDecl(middleware, "init")) { - const data = middleware_data.get(index); - try @call( - .always_inline, - middleware.beforeRequest, - .{ @as(*middleware, @ptrCast(@alignCast(data))), request }, - ); - } else { - try @call(.always_inline, middleware.beforeRequest, .{request}); - } - } - - return middleware_data; -} - -pub fn afterMiddleware( - middleware_data: *MiddlewareData, - request: *jetzig.http.Request, -) !void { inline for (middlewares, 0..) |middleware, index| { if (comptime !@hasDecl(middleware, "afterRequest")) continue; if (comptime @hasDecl(middleware, "init")) { @@ -48,10 +27,50 @@ pub fn afterMiddleware( try @call( .always_inline, middleware.afterRequest, + .{ @as(*middleware, @ptrCast(@alignCast(data))), request }, + ); + } else { + try @call(.always_inline, middleware.afterRequest, .{request}); + } + } + + return middleware_data; +} + +pub fn beforeResponse( + middleware_data: *MiddlewareData, + request: *jetzig.http.Request, +) !void { + inline for (middlewares, 0..) |middleware, index| { + if (comptime !@hasDecl(middleware, "beforeResponse")) continue; + if (comptime @hasDecl(middleware, "init")) { + const data = middleware_data.get(index); + try @call( + .always_inline, + middleware.beforeResponse, .{ @as(*middleware, @ptrCast(@alignCast(data))), request, request.response }, ); } else { - try @call(.always_inline, middleware.afterRequest, .{ request, request.response }); + try @call(.always_inline, middleware.beforeResponse, .{ request, request.response }); + } + } +} + +pub fn afterResponse( + middleware_data: *MiddlewareData, + request: *jetzig.http.Request, +) !void { + inline for (middlewares, 0..) |middleware, index| { + if (comptime !@hasDecl(middleware, "afterResponse")) continue; + if (comptime @hasDecl(middleware, "init")) { + const data = middleware_data.get(index); + try @call( + .always_inline, + middleware.afterResponse, + .{ @as(*middleware, @ptrCast(@alignCast(data))), request, request.response }, + ); + } else { + try @call(.always_inline, middleware.afterResponse, .{ request, request.response }); } } } diff --git a/src/jetzig/middleware/HtmxMiddleware.zig b/src/jetzig/middleware/HtmxMiddleware.zig index 570e4ff..453b5d0 100644 --- a/src/jetzig/middleware/HtmxMiddleware.zig +++ b/src/jetzig/middleware/HtmxMiddleware.zig @@ -13,7 +13,7 @@ pub fn init(request: *jetzig.http.Request) !*Self { /// request. This allows a view to specify a layout that will render the full page when the /// request doesn't come via htmx and, when the request does come from htmx, only return the /// content rendered directly by the view function. -pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void { +pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { _ = self; if (request.getHeader("HX-Target")) |target| { request.server.logger.debug( @@ -26,9 +26,10 @@ pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void { /// If a redirect was issued during request processing, reset any response data, set response /// status to `200 OK` and replace the `Location` header with a `HX-Redirect` header. -pub fn afterRequest(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { +pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { _ = self; if (response.status_code != .moved_permanently and response.status_code != .found) return; + if (request.headers.getFirstValue("HX-Request") == null) return; if (response.headers.getFirstValue("Location")) |location| { response.headers.remove("Location");