Merge pull request #78 from jetzig-framework/middleware-request-resolution

Middleware request resolution
This commit is contained in:
bobf 2024-05-27 16:52:50 +01:00 committed by GitHub
commit 3cad3fddc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 173 additions and 63 deletions

View File

@ -33,6 +33,12 @@ pub fn init(request: *jetzig.http.Request) !*DemoMiddleware {
/// 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: *DemoMiddleware, request: *jetzig.http.Request) !void {
// Middleware can invoke `request.redirect` or `request.render`. All request processing stops
// and the response is immediately returned if either of these two functions are called
// during middleware processing.
// _ = request.redirect("/foobar", .moved_permanently);
// _ = request.render(.unauthorized);
try request.server.logger.DEBUG(
"[DemoMiddleware:afterRequest] my_custom_value: {s}",
.{self.my_custom_value},

View File

@ -0,0 +1 @@
Redirecting to <a href="{{.location}}">{{.location}}</a>

View File

@ -16,39 +16,55 @@ pub const jetzig_options = struct {
jetzig.middleware.HtmxMiddleware,
// Demo middleware included with new projects. Remove once you are familiar with Jetzig's
// middleware system.
// @import("app/middleware/DemoMiddleware.zig"),
@import("app/middleware/DemoMiddleware.zig"),
};
// Maximum bytes to allow in request body.
// pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
// Maximum filesize for `public/` content.
// pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20);
pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20);
// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
// pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
// Maximum length of a header name. There is no limit imposed by the HTTP specification but
// AWS load balancers reference 40 as a limit so we use that as a baseline:
// https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_HttpHeaderConditionConfig.html
// This can be increased if needed.
// pub const max_bytes_header_name: u16 = 40;
pub const max_bytes_header_name: u16 = 40;
// Log message buffer size. Log messages exceeding this size spill to heap with degraded
// performance. Log messages should aim to fit in the message buffer.
// pub const log_message_buffer_len: usize = 4096;
pub const log_message_buffer_len: usize = 4096;
// Maximum log pool size. When a log buffer is no longer required it is returned to a pool
// for recycling. When logging i/o is slow, a high volume of requests will result in this
// pool growing. When the pool size reaches the maximum value defined here, log events are
// freed instead of recycled.
// pub const max_log_pool_len: usize = 256;
pub const max_log_pool_len: usize = 256;
// Number of request threads. Defaults to number of detected CPUs.
pub const thread_count: ?u16 = null;
// Number of response worker threads.
pub const worker_count: u16 = 4;
// Total number of connections managed by worker threads.
pub const max_connections: u16 = 512;
// Per-thread stack memory to use before spilling into request arena (possibly with allocations).
pub const buffer_size: usize = 64 * 1024;
// The size of each item in the available memory pool used by requests for rendering.
// Total retained allocation: `worker_count * max_connections`.
pub const arena_size: usize = 1024 * 1024;
// Path relative to cwd() to serve public content from. Symlinks are not followed.
// pub const public_content_path = "public";
pub const public_content_path = "public";
// HTTP buffer. Must be large enough to store all headers. This should typically not be modified.
// pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
// The number of worker threads to spawn on startup for processing Jobs (NOT the number of
// HTTP server worker threads).
@ -56,7 +72,7 @@ pub const jetzig_options = struct {
// Duration before looking for more Jobs when the queue is found to be empty, in
// milliseconds.
// pub const job_worker_sleep_interval_ms: usize = 10;
pub const job_worker_sleep_interval_ms: usize = 10;
/// Key-value store options. Set backend to `.file` to use a file-based store.
/// When using `.file` backend, you must also set `.file_options`.
@ -108,7 +124,7 @@ pub const jetzig_options = struct {
// };
/// Force email delivery in development mode (instead of printing email body to logger).
// pub const force_development_email_delivery = false;
pub const force_development_email_delivery = false;
// Set custom fragments for rendering markdown templates. Any values will fall back to
// defaults provided by Zmd (https://github.com/jetzig-framework/zmd/blob/main/src/zmd/html.zig).

View File

@ -101,16 +101,21 @@ pub const config = struct {
/// Number of request threads. Defaults to number of detected CPUs.
pub const thread_count: ?u16 = null;
/// Per-thread stack memory to use before spilling into request arena (possibly with allocations).
pub const buffer_size: usize = 64 * 1024;
/// The pre-heated size of each item in the available memory pool used by requests for
/// rendering. Total retained allocation: `worker_count * max_connections`. Requests
/// requiring more memory will allocate per-request, leaving `arena_size` bytes pre-allocated
/// for the next request.
pub const arena_size: usize = 1024 * 1024;
/// Number of response worker threads.
pub const worker_count: u16 = 4;
/// Total number of connections managed by worker threads.
pub const max_connections: u16 = 512;
/// The size of each item in the available memory pool used by requests for rendering.
/// Total retained allocation: `worker_count * max_connections`.
pub const arena_size: usize = 1024 * 1024;
/// Path relative to cwd() to serve public content from. Symlinks are not followed.
pub const public_content_path = "public";

View File

@ -5,7 +5,7 @@ const builtin = @import("builtin");
const types = @import("types.zig");
// Must be consistent with `std.io.tty.Color` for Windows compatibility.
const codes = .{
pub const codes = .{
.escape = "\x1b[",
.black = "30m",
.red = "31m",

View File

@ -14,3 +14,7 @@ pub const status_codes = @import("http/status_codes.zig");
pub const StatusCode = status_codes.StatusCode;
pub const middleware = @import("http/middleware.zig");
pub const mime = @import("http/mime.zig");
pub const SimplifiedRequest = struct {
location: ?[]const u8,
};

View File

@ -26,12 +26,16 @@ query_body: ?*jetzig.http.Query = null,
_cookies: ?*jetzig.http.Cookies = null,
_session: ?*jetzig.http.Session = null,
body: []const u8 = undefined,
processed: bool = false,
state: enum { initial, processed } = .initial,
response_started: bool = false,
dynamic_assigned_template: ?[]const u8 = null,
layout: ?[]const u8 = null,
layout_disabled: bool = false,
rendered: bool = false,
redirected: bool = false,
redirect_state: ?RedirectState = null,
middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null,
middleware_rendered_during_response: bool = false,
rendered_multiple: bool = false,
rendered_view: ?jetzig.views.View = null,
start_time: i128,
@ -132,18 +136,18 @@ pub fn deinit(self: *Request) void {
self.cookies.deinit();
self.allocator.destroy(self.cookies);
self.allocator.destroy(self.session);
if (self.processed) self.allocator.free(self.body);
if (self.state != .initial) self.allocator.free(self.body);
}
/// Process request, read body if present.
pub fn process(self: *Request) !void {
self.body = self.httpz_request.body() orelse "";
self.processed = true;
self.state = .processed;
}
/// Set response headers, write response payload, and finalize the response.
pub fn respond(self: *Request) !void {
if (!self.processed) unreachable;
if (self.state == .initial) unreachable;
try self.setCookieHeaders();
@ -158,6 +162,7 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode)
if (self.rendered) self.rendered_multiple = true;
self.rendered = true;
if (self.response_started) self.middleware_rendered_during_response = true;
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
return self.rendered_view.?;
}
@ -179,15 +184,23 @@ pub fn redirect(
self.rendered = true;
self.redirected = true;
if (self.response_started) self.middleware_rendered_during_response = true;
const status_code = switch (redirect_status) {
.moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently,
.found => jetzig.http.status_codes.StatusCode.found,
};
self.redirect_state = .{ .location = location, .status_code = status_code };
return .{ .data = self.response_data, .status_code = status_code };
}
const RedirectState = struct { location: []const u8, status_code: jetzig.http.status_codes.StatusCode };
pub fn renderRedirect(self: *Request, state: RedirectState) !void {
self.response_data.reset();
self.response.headers.append("Location", location) catch |err| {
self.response.headers.append("Location", state.location) catch |err| {
switch (err) {
error.JetzigTooManyHeaders => std.debug.print(
"Header limit reached. Unable to add redirect header.\n",
@ -197,8 +210,32 @@ pub fn redirect(
}
};
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
return self.rendered_view.?;
const view = .{ .data = self.response_data, .status_code = state.status_code };
const status = jetzig.http.status_codes.get(state.status_code);
const maybe_template = jetzig.zmpl.findPrefixed("views", status.getCode());
self.rendered_view = view;
var root = try self.response_data.root(.object);
try root.put("location", self.response_data.string(state.location));
const content = switch (self.requestFormat()) {
.HTML, .UNKNOWN => if (maybe_template) |template| blk: {
try view.data.addConst("jetzig_view", view.data.string("internal"));
try view.data.addConst("jetzig_action", view.data.string(@tagName(state.status_code)));
break :blk try template.render(self.response_data);
} else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}),
.JSON => blk: {
break :blk try std.json.stringifyAlloc(
self.allocator,
.{ .location = state.location, .status = .{
.message = status.getMessage(),
.code = status.getCode(),
} },
.{},
);
},
};
self.setResponse(.{ .view = view, .content = content }, .{});
}
/// Infer the current format (JSON or HTML) from the request in this order:
@ -246,7 +283,7 @@ pub fn getHeader(self: *const Request, key: []const u8) ?[]const u8 {
/// otherwise the parsed JSON request body will take precedence and query parameters will be
/// ignored.
pub fn params(self: *Request) !*jetzig.data.Value {
if (!self.processed) unreachable;
if (self.state == .initial) unreachable;
switch (self.requestFormat()) {
.JSON => {
@ -489,7 +526,7 @@ pub fn formatStatus(self: *const Request, status_code: jetzig.http.StatusCode) !
return switch (self.requestFormat()) {
.JSON => try std.json.stringifyAlloc(self.allocator, .{
.@"error" = .{
.status = .{
.message = status.getMessage(),
.code = status.getCode(),
},

View File

@ -70,7 +70,7 @@ const Dispatcher = struct {
pub fn handle(self: Dispatcher, request: *httpz.Request, response: *httpz.Response) void {
self.server.processNextRequest(request, response) catch |err| {
self.server.errorHandlerFn(request, response, err);
self.server.errorHandlerFn(request, response, err) catch {};
};
}
};
@ -83,6 +83,7 @@ pub fn listen(self: *Server) !void {
.address = self.options.bind,
.thread_pool = .{
.count = jetzig.config.get(?u16, "thread_count") orelse @intCast(try std.Thread.getCpuCount()),
.buffer_size = jetzig.config.get(usize, "buffer_size"),
},
.workers = .{
.count = jetzig.config.get(u16, "worker_count"),
@ -105,10 +106,13 @@ pub fn listen(self: *Server) !void {
return try httpz_server.listen();
}
pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.Response, err: anyerror) void {
pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.Response, err: anyerror) !void {
if (isBadHttpError(err)) return;
self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {};
const stack = @errorReturnTrace();
if (stack) |capture| self.logStackTrace(capture, .{ .httpz = request }) catch {};
response.body = "500 Internal Server Error";
}
@ -119,11 +123,9 @@ fn processNextRequest(
) !void {
const start_time = std.time.nanoTimestamp();
const allocator = httpz_request.arena;
var response = try jetzig.http.Response.init(allocator, httpz_response);
var response = try jetzig.http.Response.init(httpz_response.arena, httpz_response);
var request = try jetzig.http.Request.init(
allocator,
httpz_request.arena,
self,
start_time,
httpz_request,
@ -135,15 +137,24 @@ fn processNextRequest(
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
try self.renderResponse(&request);
try request.response.headers.append("Content-Type", response.content_type);
if (request.middleware_rendered) |_| { // Request processing ends when a middleware renders or redirects.
if (request.redirect_state) |state| {
try request.renderRedirect(state);
} else if (request.rendered_view) |rendered| {
// TODO: Allow middleware to set content
request.setResponse(.{ .view = rendered, .content = "" }, .{});
}
try request.response.headers.append("Content-Type", response.content_type);
try request.respond();
} else {
try self.renderResponse(&request);
try request.response.headers.append("Content-Type", response.content_type);
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request);
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
try request.respond();
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request);
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
try request.respond();
}
try self.logger.logRequest(&request);
}
@ -180,6 +191,8 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
.JSON => try self.renderJSON(request, route),
.UNKNOWN => try self.renderHTML(request, route),
}
if (request.redirect_state) |state| return try request.renderRedirect(state);
}
fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void {
@ -388,7 +401,7 @@ fn renderInternalServerError(self: *Server, request: *jetzig.http.Request, err:
try self.logger.ERROR("Encountered Error: {s}", .{@errorName(err)});
const stack = @errorReturnTrace();
if (stack) |capture| try self.logStackTrace(capture, request);
if (stack) |capture| try self.logStackTrace(capture, .{ .jetzig = request });
const status = .internal_server_error;
return try self.renderError(request, status);
@ -436,7 +449,7 @@ fn renderErrorView(
.{@errorName(err)},
);
const stack = @errorReturnTrace();
if (stack) |capture| try self.logStackTrace(capture, request);
if (stack) |capture| try self.logStackTrace(capture, .{ .jetzig = request });
return try renderDefaultError(request, status_code);
};
@ -503,14 +516,18 @@ fn renderDefaultError(
fn logStackTrace(
self: Server,
stack: *std.builtin.StackTrace,
request: *jetzig.http.Request,
request: union(enum) { jetzig: *const jetzig.http.Request, httpz: *const httpz.Request },
) !void {
try self.logger.ERROR("\nStack Trace:\n{}", .{stack});
var buf = std.ArrayList(u8).init(request.allocator);
const allocator = switch (request) {
.jetzig => |capture| capture.allocator,
.httpz => |capture| capture.arena,
};
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
const writer = buf.writer();
try stack.format("", .{}, writer);
try self.logger.ERROR("{s}\n", .{buf.items});
if (buf.items.len > 0) try self.logger.ERROR("{s}\n", .{buf.items});
}
fn matchCustomRoute(self: Server, request: *const jetzig.http.Request) ?jetzig.views.Route {

View File

@ -3,22 +3,24 @@ const jetzig = @import("../../jetzig.zig");
const middlewares: []const type = jetzig.config.get([]const type, "middleware");
const MiddlewareData = std.BoundedArray(*anyopaque, middlewares.len);
const MiddlewareData = std.BoundedArray(?*anyopaque, middlewares.len);
pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
var middleware_data = MiddlewareData.init(0) catch unreachable;
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "init")) continue;
const data = try @call(.always_inline, middleware.init, .{request});
// We cannot overflow here because we know the length of the array
middleware_data.insert(index, data) catch unreachable;
if (comptime !@hasDecl(middleware, "init")) {
try middleware_data.insert(index, null);
} else {
const data = try @call(.always_inline, middleware.init, .{request});
try middleware_data.insert(index, data);
}
}
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "afterRequest")) continue;
if (comptime @hasDecl(middleware, "init")) {
const data = middleware_data.get(index);
const data = middleware_data.get(index).?;
try @call(
.always_inline,
middleware.afterRequest,
@ -27,6 +29,10 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
} else {
try @call(.always_inline, middleware.afterRequest, .{request});
}
if (request.rendered or request.redirected) {
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "afterRequest" };
break;
}
}
return middleware_data;
@ -36,17 +42,25 @@ pub fn beforeResponse(
middleware_data: *MiddlewareData,
request: *jetzig.http.Request,
) !void {
request.response_started = true;
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.beforeResponse, .{ request, request.response });
if (!request.middleware_rendered_during_response) {
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.beforeResponse, .{ request, request.response });
}
}
if (request.middleware_rendered_during_response) {
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "beforeResponse" };
break;
}
}
}
@ -58,7 +72,7 @@ pub fn afterResponse(
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "afterResponse")) continue;
if (comptime @hasDecl(middleware, "init")) {
const data = middleware_data.get(index);
const data = middleware_data.get(index).?;
try @call(
.always_inline,
middleware.afterResponse,

View File

@ -84,12 +84,22 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
const formatted_level = if (self.stdout_colorized) colorizedLogLevel(.INFO) else @tagName(.INFO);
try self.log_queue.print("{s: >5} [{s}] [{s}/{s}/{s}] {s}\n", .{
try self.log_queue.print("{s: >5} [{s}] [{s}/{s}/{s}]{s}{s}{s}{s}{s}{s}{s}{s}{s}{s} {s}\n", .{
formatted_level,
iso8601,
formatted_duration,
request.fmtMethod(self.stdout_colorized),
formatted_status,
if (request.middleware_rendered) |_| " [" ++ jetzig.colors.codes.escape ++ jetzig.colors.codes.magenta else "",
if (request.middleware_rendered) |middleware| middleware.name else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.white ++ ":" else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.blue else "",
if (request.middleware_rendered) |middleware| middleware.action else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.white ++ ":" else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.bright_cyan else "",
if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.reset else "",
if (request.middleware_rendered) |_| "]" else "",
request.path.path,
}, .stdout);
}

View File

@ -19,7 +19,7 @@ pub fn afterRequest(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 beforeResponse(request: *const jetzig.http.Request, response: *jetzig.http.Response) !void {
pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
if (response.status_code != .moved_permanently and response.status_code != .found) return;
if (request.headers.get("HX-Request") == null) return;