Implement redirects + htmx redirect support

This commit is contained in:
Bob Farrell 2024-03-17 18:35:22 +00:00
parent 9b255eb19a
commit 993caa5d3f
7 changed files with 87 additions and 43 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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,

View File

@ -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,

View File

@ -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 });
}
}
}

View File

@ -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");