diff --git a/README.md b/README.md
index f9c08ce..98355b8 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,6 @@
_Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :lizard: for _Linux_, _OS X_, _Windows_, and any _OS_ that can compile _Zig_ code.
-The framework is under active development and is currently in an alpha state.
-
Official website: [jetzig.dev](https://www.jetzig.dev/)
_Jetzig_ aims to provide a rich set of user-friendly tools for building modern web applications quickly. See the checklist below.
@@ -13,7 +11,7 @@ Join us on Discord ! [https://discord.gg/eufqssz7X6](https://discord.gg/eufqssz7
If you are interested in _Jetzig_ you will probably find these tools interesting too:
* [Zap](https://github.com/zigzap/zap)
-* [http.zig](https://github.com/karlseguin/http.zig)
+* [http.zig](https://github.com/karlseguin/http.zig) (_Jetzig_'s backend)
* [tokamak](https://github.com/cztomsik/tokamak)
* [zig-router](https://github.com/Cloudef/zig-router)
* [zig-webui](https://github.com/webui-dev/zig-webui/)
diff --git a/build.zig b/build.zig
index 19456fa..05a642e 100644
--- a/build.zig
+++ b/build.zig
@@ -57,6 +57,7 @@ pub fn build(b: *std.Build) !void {
const jetkv_dep = b.dependency("jetkv", .{ .target = target, .optimize = optimize });
const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });
+ const httpz_dep = b.dependency("httpz", .{ .target = target, .optimize = optimize });
// This is the way to make it look nice in the zig build script
// If we would do it the other way around, we would have to do
@@ -75,6 +76,7 @@ pub fn build(b: *std.Build) !void {
jetzig_module.addImport("zmd", zmd_dep.module("zmd"));
jetzig_module.addImport("jetkv", jetkv_dep.module("jetkv"));
jetzig_module.addImport("smtp", smtp_client_dep.module("smtp_client"));
+ jetzig_module.addImport("httpz", httpz_dep.module("httpz"));
const main_tests = b.addTest(.{
.root_source_file = .{ .path = "src/tests.zig" },
@@ -93,6 +95,7 @@ pub fn build(b: *std.Build) !void {
main_tests.root_module.addImport("zmpl", zmpl_dep.module("zmpl"));
main_tests.root_module.addImport("jetkv", jetkv_dep.module("jetkv"));
+ main_tests.root_module.addImport("httpz", httpz_dep.module("httpz"));
const run_main_tests = b.addRunArtifact(main_tests);
const test_step = b.step("test", "Run library tests");
diff --git a/build.zig.zon b/build.zig.zon
index 56d2735..70f44e3 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -7,20 +7,24 @@
.hash = "1220482f07f2bbaef335f20d6890c15a1e14739950b784232bc69182423520e058a5",
},
.zmpl = .{
- .url = "https://github.com/jetzig-framework/zmpl/archive/c14683521ca48c1de0a9b2d61dfb145e1bc4dac1.tar.gz",
- .hash = "122093b741ef4aff151e916fc6005cb0c2aed747a34b77c0d4b45099ea2b561df9c7",
+ .url = "https://github.com/jetzig-framework/zmpl/archive/ef04bf3579e176f9fa3a02effc4ffcbbb5d080d8.tar.gz",
+ .hash = "12209bd490ef2c841d607a6260be9cc40e20dc76786cb99d0fcd72cfef4a253a840d",
},
.jetkv = .{
- .url = "https://github.com/jetzig-framework/jetkv/archive/50016e13c89e86c89b7f7ae93d4f0a31d3be303b.tar.gz",
- .hash = "122090b828d2cdd4915d242cb3761fe9142b145e49a2341f8b29343839945d6ab256",
+ .url = "https://github.com/jetzig-framework/jetkv/archive/6fc375b1ece563ae6d16849bb7c0441ff2883a04.tar.gz",
+ .hash = "122079edca9ea46ebb5ce8f05ea2c58ee957cf2d73fcfd9a0fd6a50f65879f3bf88f",
},
.args = .{
.url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
.hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
},
.smtp_client = .{
- .url = "https://github.com/bobf/smtp_client.zig/archive/86315adf5527e6add304fea3f1ff110613126283.tar.gz",
- .hash = "12203ccdd3f7145f305c6f88b18ecd407d27b36051a523f8f579f0099db6a17cd757",
+ .url = "https://github.com/karlseguin/smtp_client.zig/archive/964152ad4e19dc1d22f6def6f659c86df60e7832.tar.gz",
+ .hash = "1220d4f1c2472769b0d689ea878f41f0a66cb07f28569a138aea2c0a648a5c90dd4e",
+ },
+ .httpz = .{
+ .url = "https://github.com/karlseguin/http.zig/archive/34f1aa8a1486478414e876f65364a501d73c8a76.tar.gz",
+ .hash = "12205404dd8bdd98c659e844385154eb28116c1be073103fc94739bc99fa912323e8",
},
},
diff --git a/demo/src/app/middleware/DemoMiddleware.zig b/demo/src/app/middleware/DemoMiddleware.zig
index a6758d2..d390535 100644
--- a/demo/src/app/middleware/DemoMiddleware.zig
+++ b/demo/src/app/middleware/DemoMiddleware.zig
@@ -20,11 +20,11 @@ const jetzig = @import("jetzig");
/// can also be modified.
my_custom_value: []const u8,
-const Self = @This();
+const DemoMiddleware = @This();
/// Initialize middleware.
-pub fn init(request: *jetzig.http.Request) !*Self {
- var middleware = try request.allocator.create(Self);
+pub fn init(request: *jetzig.http.Request) !*DemoMiddleware {
+ var middleware = try request.allocator.create(DemoMiddleware);
middleware.my_custom_value = "initial value";
return middleware;
}
@@ -32,7 +32,7 @@ pub fn init(request: *jetzig.http.Request) !*Self {
/// 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 {
+pub fn afterRequest(self: *DemoMiddleware, request: *jetzig.http.Request) !void {
try request.server.logger.DEBUG(
"[DemoMiddleware:afterRequest] my_custom_value: {s}",
.{self.my_custom_value},
@@ -42,7 +42,11 @@ pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !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 {
+pub fn beforeResponse(
+ self: *DemoMiddleware,
+ request: *jetzig.http.Request,
+ response: *jetzig.http.Response,
+) !void {
try request.server.logger.DEBUG(
"[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
.{ self.my_custom_value, @tagName(response.status_code) },
@@ -51,7 +55,11 @@ pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jet
/// 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 {
+pub fn afterResponse(
+ self: *DemoMiddleware,
+ request: *jetzig.http.Request,
+ response: *jetzig.http.Response,
+) !void {
_ = self;
_ = response;
try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{});
@@ -60,6 +68,6 @@ pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetz
/// 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
/// freed before the next request starts processing.
-pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
+pub fn deinit(self: *DemoMiddleware, request: *jetzig.http.Request) void {
request.allocator.destroy(self);
}
diff --git a/demo/src/app/views/basic.zig b/demo/src/app/views/basic.zig
new file mode 100644
index 0000000..4fac587
--- /dev/null
+++ b/demo/src/app/views/basic.zig
@@ -0,0 +1,7 @@
+const std = @import("std");
+const jetzig = @import("jetzig");
+
+pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ return request.render(.ok);
+}
diff --git a/demo/src/app/views/basic/index.zmpl b/demo/src/app/views/basic/index.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/demo/src/app/views/basic/index.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/demo/src/app/views/mail.zig b/demo/src/app/views/mail.zig
index 24b93ce..5e8a2f3 100644
--- a/demo/src/app/views/mail.zig
+++ b/demo/src/app/views/mail.zig
@@ -10,7 +10,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
// * `src/app/mailers/welcome/html.zmpl`
// * `src/app/mailers/welcome/text.zmpl`
// All mailer templates have access to the same template data as a view template.
- const mail = request.mail("welcome", .{ .to = &.{"hello.dev"} });
+ const mail = request.mail("welcome", .{ .to = &.{"hello@jetzig.dev"} });
// Deliver the email asynchronously via a built-in mail Job. Use `.now` to send the email
// synchronously (i.e. before the request has returned).
diff --git a/demo/src/app/views/redirect.zig b/demo/src/app/views/redirect.zig
index 7b951c4..2330df0 100644
--- a/demo/src/app/views/redirect.zig
+++ b/demo/src/app/views/redirect.zig
@@ -16,5 +16,6 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
}
}
+ try request.response.headers.append("foobar", "hello");
return request.render(.ok);
}
diff --git a/demo/src/app/views/session.zig b/demo/src/app/views/session.zig
index 75676af..ee85a1c 100644
--- a/demo/src/app/views/session.zig
+++ b/demo/src/app/views/session.zig
@@ -4,7 +4,9 @@ const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try data.object();
- if (try request.session.get("message")) |message| {
+ const session = try request.session();
+
+ if (try session.get("message")) |message| {
try root.put("message", message);
} else {
try root.put("message", data.string("No message saved yet"));
@@ -16,9 +18,10 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
const params = try request.params();
+ var session = try request.session();
if (params.get("message")) |message| {
- try request.session.put("message", message);
+ try session.put("message", message);
}
return request.redirect("/session", .moved_permanently);
diff --git a/demo/src/main.zig b/demo/src/main.zig
index 08213d1..e89b27a 100644
--- a/demo/src/main.zig
+++ b/demo/src/main.zig
@@ -15,7 +15,7 @@ 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.
@@ -27,6 +27,22 @@ pub const jetzig_options = struct {
// 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);
+ // 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;
+
+ // 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;
+
+ // 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;
+
// Path relative to cwd() to serve public content from. Symlinks are not followed.
// pub const public_content_path = "public";
diff --git a/src/jetzig.zig b/src/jetzig.zig
index 4d18546..fc5026d 100644
--- a/src/jetzig.zig
+++ b/src/jetzig.zig
@@ -78,6 +78,22 @@ pub const config = struct {
/// 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);
+ /// 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;
+
+ /// 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;
+
+ /// 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;
+
/// Path relative to cwd() to serve public content from. Symlinks are not followed.
pub const public_content_path = "public";
diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig
index 59af1df..4e209cd 100644
--- a/src/jetzig/App.zig
+++ b/src/jetzig/App.zig
@@ -79,6 +79,13 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
defer self.allocator.free(server_options.bind);
defer self.allocator.free(server_options.secret);
+ var log_thread = try std.Thread.spawn(
+ .{ .allocator = self.allocator },
+ jetzig.loggers.LogQueue.Reader.publish,
+ .{ &server_options.log_queue.reader, .{} },
+ );
+ defer log_thread.join();
+
if (server_options.detach) {
const argv = try std.process.argsAlloc(self.allocator);
defer std.process.argsFree(self.allocator, argv);
@@ -105,7 +112,6 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
&job_queue,
&cache,
);
- defer server.deinit();
var mutex = std.Thread.Mutex{};
var worker_pool = jetzig.jobs.Pool.init(
diff --git a/src/jetzig/Environment.zig b/src/jetzig/Environment.zig
index 47e8b1f..ce458a8 100644
--- a/src/jetzig/Environment.zig
+++ b/src/jetzig/Environment.zig
@@ -63,6 +63,13 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
const options = try args.parseForCurrentProcess(Options, self.allocator, .print);
defer options.deinit();
+ const log_queue = try self.allocator.create(jetzig.loggers.LogQueue);
+ log_queue.* = jetzig.loggers.LogQueue.init(self.allocator);
+ try log_queue.setFiles(
+ try getLogFile(.stdout, options.options),
+ try getLogFile(.stderr, options.options),
+ );
+
if (options.options.help) {
const writer = std.io.getStdErr().writer();
try args.printHelp(Options, options.executable_name orelse "", writer);
@@ -76,16 +83,14 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
.development_logger = jetzig.loggers.DevelopmentLogger.init(
self.allocator,
resolveLogLevel(options.options.@"log-level", environment),
- try getLogFile(.stdout, options.options),
- try getLogFile(.stderr, options.options),
+ log_queue,
),
},
.json => jetzig.loggers.Logger{
.json_logger = jetzig.loggers.JsonLogger.init(
self.allocator,
resolveLogLevel(options.options.@"log-level", environment),
- try getLogFile(.stdout, options.options),
- try getLogFile(.stderr, options.options),
+ log_queue,
),
},
};
@@ -111,6 +116,7 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
.port = options.options.port,
.detach = options.options.detach,
.environment = environment,
+ .log_queue = log_queue,
};
}
diff --git a/src/jetzig/colors.zig b/src/jetzig/colors.zig
index 36e4af0..5d74026 100644
--- a/src/jetzig/colors.zig
+++ b/src/jetzig/colors.zig
@@ -4,37 +4,83 @@ const builtin = @import("builtin");
const types = @import("types.zig");
+// Must be consistent with `std.io.tty.Color` for Windows compatibility.
const codes = .{
- .escape = "\x1B[",
- .reset = "0;0",
- .black = "0;30",
- .red = "0;31",
- .green = "0;32",
- .yellow = "0;33",
- .blue = "0;34",
- .purple = "0;35",
- .cyan = "0;36",
- .white = "0;37",
+ .escape = "\x1b[",
+ .black = "30m",
+ .red = "31m",
+ .green = "32m",
+ .yellow = "33m",
+ .blue = "34m",
+ .magenta = "35m",
+ .cyan = "36m",
+ .white = "37m",
+ .bright_black = "90m",
+ .bright_red = "91m",
+ .bright_green = "92m",
+ .bright_yellow = "93m",
+ .bright_blue = "94m",
+ .bright_magenta = "95m",
+ .bright_cyan = "96m",
+ .bright_white = "97m",
+ .bold = "1m",
+ .dim = "2m",
+ .reset = "0m",
};
+/// Map color codes generated by `std.io.tty.Config.setColor` back to `std.io.tty.Color`. Used by
+/// `jetzig.loggers.LogQueue.writeWindows` to parse escape codes so they can be passed to
+/// `std.io.tty.Config.setColor` (using Windows API to set console color mode).
+pub const codes_map = std.StaticStringMap(std.io.tty.Color).initComptime(.{
+ .{ "30", .black },
+ .{ "31", .red },
+ .{ "32", .green },
+ .{ "33", .yellow },
+ .{ "34", .blue },
+ .{ "35", .magenta },
+ .{ "36", .cyan },
+ .{ "37", .white },
+ .{ "90", .bright_black },
+ .{ "91", .bright_red },
+ .{ "92", .bright_green },
+ .{ "93", .bright_yellow },
+ .{ "94", .bright_blue },
+ .{ "95", .bright_magenta },
+ .{ "96", .bright_cyan },
+ .{ "97", .bright_white },
+ .{ "1", .bold },
+ .{ "2", .dim },
+ .{ "0", .reset },
+});
+
+/// Colorize a log message. Note that we force `.escape_codes` when we are a TTY even on Windows.
+/// `jetzig.loggers.LogQueue` parses the ANSI codes and uses `std.io.tty.Config.setColor` to
+/// invoke the appropriate Windows API call to set the terminal color before writing each token.
+/// We must do it this way because Windows colors are set by API calls at the time of write, not
+/// encoded into the message string.
+pub fn colorize(color: std.io.tty.Color, buf: []u8, input: []const u8, is_colorized: bool) ![]const u8 {
+ if (!is_colorized) return input;
+
+ const config: std.io.tty.Config = .escape_codes;
+ var stream = std.io.fixedBufferStream(buf);
+ const writer = stream.writer();
+ try config.setColor(writer, color);
+ try writer.writeAll(input);
+ try config.setColor(writer, .reset);
+
+ return stream.getWritten();
+}
+
fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 {
- if (builtin.os.tag == .windows) {
- return message;
- } else {
- return codes.escape ++ attribute ++ "m" ++ message ++ codes.escape ++ codes.reset ++ "m";
- }
+ return codes.escape ++ attribute ++ message ++ codes.escape ++ codes.reset;
}
fn runtimeWrap(allocator: std.mem.Allocator, attribute: []const u8, message: []const u8) ![]const u8 {
- if (builtin.os.tag == .windows) {
- return try allocator.dupe(u8, message);
- } else {
- return try std.mem.join(
- allocator,
- "",
- &[_][]const u8{ codes.escape, attribute, "m", message, codes.escape, codes.reset, "m" },
- );
- }
+ return try std.mem.join(
+ allocator,
+ "",
+ &[_][]const u8{ codes.escape, attribute, message, codes.escape, codes.reset },
+ );
}
pub fn black(comptime message: []const u8) []const u8 {
@@ -77,12 +123,12 @@ pub fn runtimeBlue(allocator: std.mem.Allocator, message: []const u8) ![]const u
return try runtimeWrap(allocator, codes.blue, message);
}
-pub fn purple(comptime message: []const u8) []const u8 {
- return wrap(codes.purple, message);
+pub fn magenta(comptime message: []const u8) []const u8 {
+ return wrap(codes.magenta, message);
}
-pub fn runtimePurple(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
- return try runtimeWrap(allocator, codes.purple, message);
+pub fn runtimeMagenta(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
+ return try runtimeWrap(allocator, codes.magenta, message);
}
pub fn cyan(comptime message: []const u8) []const u8 {
@@ -101,15 +147,26 @@ pub fn runtimeWhite(allocator: std.mem.Allocator, message: []const u8) ![]const
return try runtimeWrap(allocator, codes.white, message);
}
-pub fn duration(allocator: std.mem.Allocator, delta: i64) ![]const u8 {
- var buf: [1024]u8 = undefined;
- const formatted_duration = try std.fmt.bufPrint(&buf, "{}", .{std.fmt.fmtDurationSigned(delta)});
-
- if (delta < 1000000) {
- return try runtimeGreen(allocator, formatted_duration);
- } else if (delta < 5000000) {
- return try runtimeYellow(allocator, formatted_duration);
- } else {
- return try runtimeRed(allocator, formatted_duration);
+pub fn duration(buf: *[256]u8, delta: i64, is_colorized: bool) ![]const u8 {
+ if (!is_colorized) {
+ return try std.fmt.bufPrint(
+ buf,
+ "{}",
+ .{std.fmt.fmtDurationSigned(delta)},
+ );
}
+
+ const color: std.io.tty.Color = if (delta < 1000000)
+ .green
+ else if (delta < 5000000)
+ .yellow
+ else
+ .red;
+ var duration_buf: [256]u8 = undefined;
+ const formatted_duration = try std.fmt.bufPrint(
+ &duration_buf,
+ "{}",
+ .{std.fmt.fmtDurationSigned(delta)},
+ );
+ return try colorize(color, buf, formatted_duration, true);
}
diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig
index 5151d26..e35f325 100644
--- a/src/jetzig/http.zig
+++ b/src/jetzig/http.zig
@@ -1,4 +1,5 @@
const std = @import("std");
+const builtin = @import("builtin");
pub const Server = @import("http/Server.zig");
pub const Request = @import("http/Request.zig");
diff --git a/src/jetzig/http/Cookies.zig b/src/jetzig/http/Cookies.zig
index b5bd3a0..171ebf6 100644
--- a/src/jetzig/http/Cookies.zig
+++ b/src/jetzig/http/Cookies.zig
@@ -6,6 +6,7 @@ allocator: std.mem.Allocator,
cookie_string: []const u8,
buf: std.ArrayList(u8),
cookies: std.StringArrayHashMap(*Cookie),
+modified: bool = false,
const Self = @This();
@@ -38,6 +39,8 @@ pub fn get(self: *Self, key: []const u8) ?*Cookie {
}
pub fn put(self: *Self, key: []const u8, value: Cookie) !void {
+ self.modified = true;
+
if (self.cookies.fetchSwapRemove(key)) |entry| {
self.allocator.free(entry.key);
self.allocator.free(entry.value.value);
diff --git a/src/jetzig/http/Headers.zig b/src/jetzig/http/Headers.zig
index 0e2388a..8db8c56 100644
--- a/src/jetzig/http/Headers.zig
+++ b/src/jetzig/http/Headers.zig
@@ -1,120 +1,102 @@
const std = @import("std");
+
+const httpz = @import("httpz");
+
const jetzig = @import("../../jetzig.zig");
allocator: std.mem.Allocator,
-headers: HeadersArray,
+httpz_headers: *httpz.key_value.KeyValue,
+new_headers: std.ArrayList(Header),
const Headers = @This();
-pub const max_headers = 25;
-const HeadersArray = std.ArrayListUnmanaged(std.http.Header);
+const Header = struct { name: []const u8, value: []const u8 };
+const max_bytes_header_name = jetzig.config.get(u8, "max_bytes_header_name");
-pub fn init(allocator: std.mem.Allocator) Headers {
+pub fn init(allocator: std.mem.Allocator, httpz_headers: *httpz.key_value.KeyValue) Headers {
return .{
.allocator = allocator,
- .headers = HeadersArray.initCapacity(allocator, max_headers) catch @panic("OOM"),
+ .httpz_headers = httpz_headers,
+ .new_headers = std.ArrayList(Header).init(allocator),
};
}
pub fn deinit(self: *Headers) void {
- self.headers.deinit(self.allocator);
-}
+ self.httpz_headers.deinit(self.allocator);
-/// Gets the first value for a given header identified by `name`. Names are case insensitive.
-pub fn get(self: Headers, name: []const u8) ?[]const u8 {
- for (self.headers.items) |header| {
- if (jetzig.util.equalStringsCaseInsensitive(name, header.name)) return header.value;
+ for (self.new_headers.items) |header| {
+ self.allocator.free(header.name);
+ self.allocator.free(header.value);
}
- return null;
+
+ self.new_headers.deinit();
}
-/// Gets the first value for a given header identified by `name`. Names are case insensitive.
+/// Get the first value for a given header identified by `name`. Names are case insensitive.
+pub fn get(self: Headers, name: []const u8) ?[]const u8 {
+ std.debug.assert(name.len <= max_bytes_header_name);
+
+ var buf: [max_bytes_header_name]u8 = undefined;
+ const lower = std.ascii.lowerString(&buf, name);
+
+ return self.httpz_headers.get(lower);
+}
+
+/// Get all values for a given header identified by `name`. Names are case insensitive.
pub fn getAll(self: Headers, name: []const u8) []const []const u8 {
var headers = std.ArrayList([]const u8).init(self.allocator);
- for (self.headers.items) |header| {
- if (jetzig.util.equalStringsCaseInsensitive(name, header.name)) {
- headers.append(header.value) catch @panic("OOM");
- }
+ for (self.httpz_headers.keys, 0..) |key, index| {
+ var buf: [max_bytes_header_name]u8 = undefined;
+ const lower = std.ascii.lowerString(&buf, name);
+
+ if (std.mem.eql(u8, lower, key)) headers.append(self.httpz_headers.values[index]) catch @panic("OOM");
}
return headers.toOwnedSlice() catch @panic("OOM");
}
-// Deprecated
+/// Deprecated
pub fn getFirstValue(self: *const Headers, name: []const u8) ?[]const u8 {
return self.get(name);
}
-/// Appends `name` and `value` to headers.
+/// Add `name` and `value` to headers.
pub fn append(self: *Headers, name: []const u8, value: []const u8) !void {
- if (self.headers.items.len >= 25) return error.JetzigTooManyHeaders;
+ if (self.httpz_headers.len >= self.httpz_headers.keys.len) return error.JetzigTooManyHeaders;
- self.headers.appendAssumeCapacity(.{ .name = name, .value = value });
-}
+ var buf: [max_bytes_header_name]u8 = undefined;
+ const lower = std.ascii.lowerString(&buf, name);
-/// Removes **all** header entries matching `name`. Names are case-insensitive.
-pub fn remove(self: *Headers, name: []const u8) void {
- if (self.headers.items.len == 0) return;
-
- var index: usize = self.headers.items.len;
-
- while (index > 0) {
- index -= 1;
- if (jetzig.util.equalStringsCaseInsensitive(name, self.headers.items[index].name)) {
- _ = self.headers.orderedRemove(index);
- }
- }
-}
-
-/// Returns an iterator which implements `next()` returning each name/value of the stored headers.
-pub fn iterator(self: Headers) Iterator {
- return Iterator{ .headers = self.headers };
-}
-
-/// Returns an array of `std.http.Header`, can be used to set response headers directly.
-/// Caller owns memory.
-pub fn stdHeaders(self: *Headers) !std.ArrayListUnmanaged(std.http.Header) {
- var array = try std.ArrayListUnmanaged(std.http.Header).initCapacity(self.allocator, max_headers);
-
- var it = self.iterator();
- while (it.next()) |header| {
- array.appendAssumeCapacity(.{ .name = header.name, .value = header.value });
- }
- return array;
-}
-
-/// Iterates through stored headers yielidng a `Header` on each call to `next()`
-const Iterator = struct {
- headers: HeadersArray,
- index: usize = 0,
-
- const Header = struct {
- name: []const u8,
- value: []const u8,
+ const header = .{
+ .name = try self.allocator.dupe(u8, lower),
+ .value = try self.allocator.dupe(u8, value),
};
- /// Returns the next item in the current iteration of headers.
- pub fn next(self: *Iterator) ?Header {
- if (self.headers.items.len > self.index) {
- const std_header = self.headers.items[self.index];
- self.index += 1;
- return .{ .name = std_header.name, .value = std_header.value };
- } else {
- return null;
- }
- }
-};
+ try self.new_headers.append(header);
+ self.httpz_headers.add(header.name, header.value);
+}
-test "append" {
+test "append (deprecated)" {
const allocator = std.testing.allocator;
- var headers = Headers.init(allocator);
+ var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
+ var headers = Headers.init(allocator, &httpz_headers);
defer headers.deinit();
try headers.append("foo", "bar");
- try std.testing.expectEqualStrings(headers.getFirstValue("foo").?, "bar");
+ try std.testing.expectEqualStrings(headers.get("foo").?, "bar");
+}
+
+test "add" {
+ const allocator = std.testing.allocator;
+ var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
+ var headers = Headers.init(allocator, &httpz_headers);
+ defer headers.deinit();
+ try headers.append("foo", "bar");
+ try std.testing.expectEqualStrings(headers.get("foo").?, "bar");
}
test "get with multiple headers (bugfix regression test)" {
const allocator = std.testing.allocator;
- var headers = Headers.init(allocator);
+ var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
+ var headers = Headers.init(allocator, &httpz_headers);
defer headers.deinit();
try headers.append("foo", "bar");
try headers.append("bar", "baz");
@@ -123,75 +105,32 @@ test "get with multiple headers (bugfix regression test)" {
test "getAll" {
const allocator = std.testing.allocator;
- var headers = Headers.init(allocator);
+ var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
+ var headers = Headers.init(allocator, &httpz_headers);
defer headers.deinit();
try headers.append("foo", "bar");
try headers.append("foo", "baz");
try headers.append("bar", "qux");
const all = headers.getAll("foo");
defer allocator.free(all);
- try std.testing.expectEqualSlices([]const u8, all, &[_][]const u8{ "bar", "baz" });
+ try std.testing.expectEqualDeep(all, &[_][]const u8{ "bar", "baz" });
}
-test "append too many headers" {
+test "add too many headers" {
const allocator = std.testing.allocator;
- var headers = Headers.init(allocator);
+ var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
+ var headers = Headers.init(allocator, &httpz_headers);
defer headers.deinit();
- for (0..25) |_| try headers.append("foo", "bar");
+ for (0..10) |_| try headers.append("foo", "bar");
try std.testing.expectError(error.JetzigTooManyHeaders, headers.append("foo", "bar"));
}
test "case-insensitive matching" {
const allocator = std.testing.allocator;
- var headers = Headers.init(allocator);
+ var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10);
+ var headers = Headers.init(allocator, &httpz_headers);
defer headers.deinit();
try headers.append("Content-Type", "bar");
- try std.testing.expectEqualStrings(headers.getFirstValue("content-type").?, "bar");
-}
-
-test "iterator" {
- const allocator = std.testing.allocator;
- var headers = Headers.init(allocator);
- defer headers.deinit();
-
- try headers.append("foo", "bar");
-
- var it = headers.iterator();
- while (it.next()) |header| {
- try std.testing.expectEqualStrings("foo", header.name);
- try std.testing.expectEqualStrings("bar", header.value);
- break;
- } else {
- try std.testing.expect(false);
- }
-}
-
-test "remove" {
- const allocator = std.testing.allocator;
- var headers = Headers.init(allocator);
- defer headers.deinit();
- try headers.append("foo", "baz");
- try headers.append("foo", "qux");
- try headers.append("bar", "quux");
- headers.remove("Foo"); // Headers are case-insensitive.
- try std.testing.expect(headers.getFirstValue("foo") == null);
- try std.testing.expectEqualStrings(headers.getFirstValue("bar").?, "quux");
-}
-
-test "stdHeaders" {
- const allocator = std.testing.allocator;
- var headers = Headers.init(allocator);
- defer headers.deinit();
-
- try headers.append("foo", "bar");
- try headers.append("baz", "qux");
-
- var std_headers = try headers.stdHeaders();
- defer std_headers.deinit(allocator);
-
- try std.testing.expectEqualStrings("foo", std_headers.items[0].name);
- try std.testing.expectEqualStrings("bar", std_headers.items[0].value);
- try std.testing.expectEqualStrings("baz", std_headers.items[1].name);
- try std.testing.expectEqualStrings("qux", std_headers.items[1].value);
+ try std.testing.expectEqualStrings(headers.get("content-type").?, "bar");
}
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig
index e260a06..35de773 100644
--- a/src/jetzig/http/Request.zig
+++ b/src/jetzig/http/Request.zig
@@ -1,5 +1,7 @@
const std = @import("std");
+const httpz = @import("httpz");
+
const jetzig = @import("../../jetzig.zig");
const Request = @This();
@@ -14,14 +16,15 @@ path: jetzig.http.Path,
method: Method,
headers: jetzig.http.Headers,
server: *jetzig.http.Server,
-std_http_request: std.http.Server.Request,
+httpz_request: *httpz.Request,
+httpz_response: *httpz.Response,
response: *jetzig.http.Response,
status_code: jetzig.http.status_codes.StatusCode = .not_found,
response_data: *jetzig.data.Data,
query_params: ?*jetzig.http.Query = null,
query_body: ?*jetzig.http.Query = null,
-cookies: *jetzig.http.Cookies = undefined,
-session: *jetzig.http.Session = undefined,
+_cookies: ?*jetzig.http.Cookies = null,
+_session: ?*jetzig.http.Session = null,
body: []const u8 = undefined,
processed: bool = false,
layout: ?[]const u8 = null,
@@ -90,20 +93,18 @@ pub fn init(
allocator: std.mem.Allocator,
server: *jetzig.http.Server,
start_time: i128,
- std_http_request: std.http.Server.Request,
+ httpz_request: *httpz.Request,
+ httpz_response: *httpz.Response,
response: *jetzig.http.Response,
) !Request {
- const method = switch (std_http_request.head.method) {
+ const method = switch (httpz_request.method) {
.DELETE => Method.DELETE,
.GET => Method.GET,
.PATCH => Method.PATCH,
.POST => Method.POST,
.HEAD => Method.HEAD,
.PUT => Method.PUT,
- .CONNECT => Method.CONNECT,
.OPTIONS => Method.OPTIONS,
- .TRACE => Method.TRACE,
- _ => return error.JetzigUnsupportedHttpMethod,
};
const response_data = try allocator.create(jetzig.data.Data);
@@ -111,13 +112,14 @@ pub fn init(
return .{
.allocator = allocator,
- .path = jetzig.http.Path.init(std_http_request.head.target),
+ .path = jetzig.http.Path.init(httpz_request.url.raw),
.method = method,
- .headers = jetzig.http.Headers.init(allocator),
+ .headers = jetzig.http.Headers.init(allocator, &httpz_request.headers),
.server = server,
.response = response,
.response_data = response_data,
- .std_http_request = std_http_request,
+ .httpz_request = httpz_request,
+ .httpz_response = httpz_response,
.start_time = start_time,
.store = .{ .store = server.store, .allocator = allocator },
.cache = .{ .store = server.cache, .allocator = allocator },
@@ -132,63 +134,37 @@ pub fn deinit(self: *Request) void {
if (self.processed) self.allocator.free(self.body);
}
-/// Process request, read body if present, parse headers (TODO)
+/// Process request, read body if present.
pub fn process(self: *Request) !void {
- var headers_it = self.std_http_request.iterateHeaders();
- var cookie: ?[]const u8 = null;
-
- while (headers_it.next()) |header| {
- try self.headers.append(header.name, header.value);
- if (std.mem.eql(u8, header.name, "Cookie")) cookie = header.value;
- }
-
- self.cookies = try self.allocator.create(jetzig.http.Cookies);
- self.cookies.* = jetzig.http.Cookies.init(
- self.allocator,
- cookie orelse "",
- );
- try self.cookies.parse();
-
- self.session = try self.allocator.create(jetzig.http.Session);
- self.session.* = jetzig.http.Session.init(self.allocator, self.cookies, self.server.options.secret);
- self.session.parse() catch |err| {
- switch (err) {
- error.JetzigInvalidSessionCookie => {
- try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
- try self.session.reset();
- },
- else => return err,
- }
- };
-
- const reader = try self.std_http_request.reader();
- self.body = try reader.readAllAlloc(self.allocator, jetzig.config.get(usize, "max_bytes_request_body"));
+ self.body = self.httpz_request.body() orelse "";
self.processed = true;
}
+pub const CallbackState = struct {
+ arena: *std.heap.ArenaAllocator,
+ allocator: std.mem.Allocator,
+};
+
+pub fn responseCompleteCallback(ptr: *anyopaque) void {
+ var state: *CallbackState = @ptrCast(@alignCast(ptr));
+ state.arena.deinit();
+ state.allocator.destroy(state.arena);
+ state.allocator.destroy(state);
+}
+
/// Set response headers, write response payload, and finalize the response.
-pub fn respond(self: *Request) !void {
+pub fn respond(
+ self: *Request,
+ state: *CallbackState,
+) !void {
if (!self.processed) unreachable;
- var cookie_it = self.cookies.headerIterator();
- while (try cookie_it.next()) |header| {
- // FIXME: Skip setting cookies that are already present ?
- try self.response.headers.append("Set-Cookie", header);
- }
+ try self.setCookieHeaders();
- var std_response_headers = try self.response.headers.stdHeaders();
- defer std_response_headers.deinit(self.allocator);
-
- try self.std_http_request.respond(
- self.response.content,
- .{
- .keep_alive = false,
- .status = switch (self.response.status_code) {
- inline else => |tag| @field(std.http.Status, @tagName(tag)),
- },
- .extra_headers = std_response_headers.items,
- },
- );
+ const status = jetzig.http.status_codes.get(self.response.status_code);
+ self.httpz_response.status = try status.getCodeInt();
+ self.httpz_response.body = self.response.content;
+ self.httpz_response.callback(responseCompleteCallback, @ptrCast(state));
}
/// Render a response. This function can only be called once per request (repeat calls will
@@ -226,8 +202,15 @@ pub fn redirect(
self.response_data.reset();
- self.response.headers.remove("Location");
- self.response.headers.append("Location", location) catch @panic("OOM");
+ self.response.headers.append("Location", location) catch |err| {
+ switch (err) {
+ error.JetzigTooManyHeaders => std.debug.print(
+ "Header limit reached. Unable to add redirect header.\n",
+ .{},
+ ),
+ else => @panic("OOM"),
+ }
+ };
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
return self.rendered_view.?;
@@ -315,7 +298,7 @@ pub fn queryParams(self: *Request) !*jetzig.data.Value {
return self.query_params.?.data.value.?;
}
-// Parses request body as params if present, otherwise delegates to `queryParams`.
+// Parse request body as params if present, otherwise delegate to `queryParams`.
fn parseQuery(self: *Request) !*jetzig.data.Value {
if (self.body.len == 0) return try self.queryParams();
if (self.query_body) |parsed| return parsed.data.value.?;
@@ -332,7 +315,46 @@ fn parseQuery(self: *Request) !*jetzig.data.Value {
return self.query_body.?.data.value.?;
}
-/// Creates a new Job. Receives a job name which must resolve to `src/app/jobs/.zig`
+/// Parse `Cookie` header into separate cookies.
+pub fn cookies(self: *Request) !*jetzig.http.Cookies {
+ if (self._cookies) |capture| return capture;
+
+ const cookie = self.httpz_request.headers.get("cookie");
+
+ const local_cookies = try self.allocator.create(jetzig.http.Cookies);
+ local_cookies.* = jetzig.http.Cookies.init(
+ self.allocator,
+ cookie orelse "",
+ );
+ try local_cookies.parse();
+
+ self._cookies = local_cookies;
+
+ return local_cookies;
+}
+
+/// Parse cookies, decrypt Jetzig cookie (`jetzig.http.Session.cookie_name`) and return a mutable
+/// `jetzig.http.Session`.
+pub fn session(self: *Request) !*jetzig.http.Session {
+ if (self._session) |capture| return capture;
+
+ const local_session = try self.allocator.create(jetzig.http.Session);
+ local_session.* = jetzig.http.Session.init(self.allocator, try self.cookies(), self.server.options.secret);
+ local_session.parse() catch |err| {
+ switch (err) {
+ error.JetzigInvalidSessionCookie => {
+ try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
+ try local_session.reset();
+ },
+ else => return err,
+ }
+ };
+
+ self._session = local_session;
+ return local_session;
+}
+
+/// Create a new Job. Receives a job name which must resolve to `src/app/jobs/.zig`
/// Call `Job.put(...)` to set job params.
/// Call `Job.background()` to run the job outside of the request/response flow.
/// e.g.:
@@ -415,6 +437,14 @@ const RequestMail = struct {
}
};
+/// Create a new email from the mailer named `name` (`app/mailers/.zig`). Pass delivery
+/// params to override defaults defined my mailer (`to`, `from`, `subject`, etc.).
+/// Must call `deliver` on the returned `RequestMail` to send the email.
+/// Example:
+/// ```zig
+/// const mail = request.mail("welcome", .{ .to = &.{"hello@jetzig.dev"} });
+/// try mail.deliver(.background, .{});
+/// ```
pub fn mail(self: *Request, name: []const u8, mail_params: jetzig.mail.MailParams) RequestMail {
return .{
.request = self,
@@ -435,35 +465,23 @@ fn extensionFormat(self: *const Request) ?jetzig.http.Request.Format {
}
pub fn acceptHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
- const acceptHeader = self.getHeader("Accept");
-
- if (acceptHeader) |item| {
- if (std.mem.eql(u8, item, "text/html")) return .HTML;
- if (std.mem.eql(u8, item, "application/json")) return .JSON;
+ if (self.httpz_request.headers.get("accept")) |value| {
+ if (std.mem.eql(u8, value, "text/html")) return .HTML;
+ if (std.mem.eql(u8, value, "application/json")) return .JSON;
}
return null;
}
pub fn contentTypeHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
- const acceptHeader = self.getHeader("content-type");
-
- if (acceptHeader) |item| {
- if (std.mem.eql(u8, item, "text/html")) return .HTML;
- if (std.mem.eql(u8, item, "application/json")) return .JSON;
+ if (self.httpz_request.headers.get("content-type")) |value| {
+ if (std.mem.eql(u8, value, "text/html")) return .HTML;
+ if (std.mem.eql(u8, value, "application/json")) return .JSON;
}
return null;
}
-pub fn hash(self: *Request) ![]const u8 {
- return try std.fmt.allocPrint(
- self.allocator,
- "{s}-{s}-{s}",
- .{ @tagName(self.method), self.path, @tagName(self.requestFormat()) },
- );
-}
-
pub fn fmtMethod(self: *const Request, colorized: bool) []const u8 {
if (!colorized) return @tagName(self.method);
@@ -508,6 +526,17 @@ pub fn setResponse(
};
}
+fn setCookieHeaders(self: *Request) !void {
+ const local_cookies = self._cookies orelse return;
+ if (!local_cookies.modified) return;
+
+ var cookie_it = local_cookies.headerIterator();
+ while (try cookie_it.next()) |header| {
+ // FIXME: Skip setting cookies that are already present ?
+ try self.response.headers.append("Set-Cookie", header);
+ }
+}
+
// Determine if a given route matches the current request.
pub fn match(self: *Request, route: jetzig.views.Route) !bool {
return switch (self.method) {
diff --git a/src/jetzig/http/Response.zig b/src/jetzig/http/Response.zig
index 3f2bb0b..c848e0a 100644
--- a/src/jetzig/http/Response.zig
+++ b/src/jetzig/http/Response.zig
@@ -1,27 +1,28 @@
const std = @import("std");
+
+const httpz = @import("httpz");
+
const jetzig = @import("../../jetzig.zig");
const http = @import("../http.zig");
const Self = @This();
allocator: std.mem.Allocator,
-headers: *jetzig.http.Headers,
+headers: jetzig.http.Headers,
content: []const u8,
status_code: http.status_codes.StatusCode,
content_type: []const u8,
pub fn init(
allocator: std.mem.Allocator,
+ httpz_response: *httpz.Response,
) !Self {
- const headers = try allocator.create(jetzig.http.Headers);
- headers.* = jetzig.http.Headers.init(allocator);
-
return .{
.allocator = allocator,
.status_code = .no_content,
.content_type = "application/octet-stream",
.content = "",
- .headers = headers,
+ .headers = jetzig.http.Headers.init(allocator, &httpz_response.headers),
};
}
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index 9434bd6..879ee0b 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -1,8 +1,10 @@
const std = @import("std");
+const builtin = @import("builtin");
const jetzig = @import("../../jetzig.zig");
const zmpl = @import("zmpl");
const zmd = @import("zmd");
+const httpz = @import("httpz");
pub const ServerOptions = struct {
logger: jetzig.loggers.Logger,
@@ -11,6 +13,7 @@ pub const ServerOptions = struct {
secret: []const u8,
detach: bool,
environment: jetzig.Environment.EnvironmentName,
+ log_queue: *jetzig.loggers.LogQueue,
};
allocator: std.mem.Allocator,
@@ -59,64 +62,86 @@ pub fn deinit(self: *Server) void {
self.allocator.free(self.options.bind);
}
-pub fn listen(self: *Server) !void {
- const address = try std.net.Address.parseIp(self.options.bind, self.options.port);
- self.std_net_server = try address.listen(.{ .reuse_port = true });
+const Dispatcher = struct {
+ server: *Server,
- self.initialized = true;
+ 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);
+ };
+ }
+};
+
+pub fn listen(self: *Server) !void {
+ var httpz_server = try httpz.ServerCtx(Dispatcher, Dispatcher).init(
+ self.allocator,
+ .{
+ .port = self.options.port,
+ .address = self.options.bind,
+ .thread_pool = .{ .count = @intCast(try std.Thread.getCpuCount()) },
+ },
+ Dispatcher{ .server = self },
+ );
+ defer httpz_server.deinit();
try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{
self.options.bind,
self.options.port,
@tagName(self.options.environment),
});
- try self.processRequests();
+
+ self.initialized = true;
+
+ return try httpz_server.listen();
}
-fn processRequests(self: *Server) !void {
- // TODO: Keepalive
- while (true) {
- var arena = std.heap.ArenaAllocator.init(self.allocator);
- errdefer arena.deinit();
- const allocator = arena.allocator();
+pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.Response, err: anyerror) void {
+ if (isBadHttpError(err)) return;
- const connection = try self.std_net_server.accept();
-
- var buf: [jetzig.config.get(usize, "http_buffer_size")]u8 = undefined;
- var std_http_server = std.http.Server.init(connection, &buf);
- errdefer std_http_server.connection.stream.close();
-
- self.processNextRequest(allocator, &std_http_server) catch |err| {
- if (isBadHttpError(err)) {
- std_http_server.connection.stream.close();
- continue;
- } else return err;
- };
-
- std_http_server.connection.stream.close();
- arena.deinit();
- }
+ self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {};
+ response.body = "500 Internal Server Error";
}
-fn processNextRequest(self: *Server, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void {
+fn processNextRequest(
+ self: *Server,
+ httpz_request: *httpz.Request,
+ httpz_response: *httpz.Response,
+) !void {
const start_time = std.time.nanoTimestamp();
- const std_http_request = try std_http_server.receiveHead();
- if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError;
+ const state = try self.allocator.create(jetzig.http.Request.CallbackState);
+ const arena = try self.allocator.create(std.heap.ArenaAllocator);
+ arena.* = std.heap.ArenaAllocator.init(self.allocator);
+ state.* = .{
+ .arena = arena,
+ .allocator = self.allocator,
+ };
- var response = try jetzig.http.Response.init(allocator);
- var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response);
+ // Regular arena deinit occurs in jetzig.http.Request.responseCompletCallback
+ errdefer state.arena.deinit();
+
+ const allocator = state.arena.allocator();
+
+ var response = try jetzig.http.Response.init(allocator, httpz_response);
+ var request = try jetzig.http.Request.init(
+ allocator,
+ self,
+ start_time,
+ httpz_request,
+ httpz_response,
+ &response,
+ );
try request.process();
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
try self.renderResponse(&request);
- try request.response.headers.append("content-type", response.content_type);
+ try request.response.headers.append("Content-Type", response.content_type);
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
- try request.respond();
+ try request.respond(state);
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request);
@@ -168,7 +193,17 @@ fn renderHTML(
};
return request.setResponse(rendered, .{});
} else {
- return request.setResponse(try self.renderNotFound(request), .{});
+ // Try rendering without a template to see if we get a redirect.
+ const rendered = self.renderView(matched_route, request, null) catch |err| {
+ if (isUnhandledError(err)) return err;
+ const rendered_error = try self.renderInternalServerError(request, err);
+ return request.setResponse(rendered_error, .{});
+ };
+
+ return if (request.redirected)
+ request.setResponse(rendered, .{})
+ else
+ request.setResponse(try self.renderNotFound(request), .{});
}
} else {
if (try self.renderMarkdown(request)) |rendered| {
diff --git a/src/jetzig/http/Session.zig b/src/jetzig/http/Session.zig
index 7adfbc7..0d0a36b 100644
--- a/src/jetzig/http/Session.zig
+++ b/src/jetzig/http/Session.zig
@@ -28,6 +28,7 @@ pub fn init(
};
}
+/// Parse session cookie.
pub fn parse(self: *Self) !void {
if (self.cookies.get(cookie_name)) |cookie| {
try self.parseSessionCookie(cookie.value);
@@ -36,6 +37,7 @@ pub fn parse(self: *Self) !void {
}
}
+/// Reset session to an empty state.
pub fn reset(self: *Self) !void {
self.data.reset();
_ = try self.data.object();
@@ -43,12 +45,14 @@ pub fn reset(self: *Self) !void {
try self.save();
}
+/// Free allocated memory.
pub fn deinit(self: *Self) void {
if (self.state != .parsed) return;
self.data.deinit();
}
+/// Get a value from the session.
pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
if (self.state != .parsed) return error.UnparsedSessionCookie;
@@ -58,6 +62,7 @@ pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
};
}
+/// Put a value into the session.
pub fn put(self: *Self, key: []const u8, value: *jetzig.data.Value) !void {
if (self.state != .parsed) return error.UnparsedSessionCookie;
diff --git a/src/jetzig/http/status_codes.zig b/src/jetzig/http/status_codes.zig
index 611cc08..2987a8d 100644
--- a/src/jetzig/http/status_codes.zig
+++ b/src/jetzig/http/status_codes.zig
@@ -178,6 +178,12 @@ pub const TaggedStatusCode = union(StatusCode) {
};
}
+ pub fn getCodeInt(self: Self) !u16 {
+ return switch (self) {
+ inline else => |capture| try std.fmt.parseInt(u16, capture.code, 10),
+ };
+ }
+
pub fn getMessage(self: Self) []const u8 {
return switch (self) {
inline else => |capture| capture.message,
diff --git a/src/jetzig/loggers.zig b/src/jetzig/loggers.zig
index 84b354a..6ce6135 100644
--- a/src/jetzig/loggers.zig
+++ b/src/jetzig/loggers.zig
@@ -6,10 +6,18 @@ const Self = @This();
pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig");
pub const JsonLogger = @import("loggers/JsonLogger.zig");
+pub const LogQueue = @import("loggers/LogQueue.zig");
pub const LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
pub const LogFormat = enum { development, json };
+/// Infer a log target (stdout or stderr) from a given log level.
+pub inline fn logTarget(comptime level: LogLevel) LogQueue.Target {
+ return switch (level) {
+ .TRACE, .DEBUG, .INFO => .stdout,
+ .WARN, .ERROR, .FATAL => .stderr,
+ };
+}
pub const Logger = union(enum) {
development_logger: DevelopmentLogger,
json_logger: JsonLogger,
diff --git a/src/jetzig/loggers/DevelopmentLogger.zig b/src/jetzig/loggers/DevelopmentLogger.zig
index 3108065..2f9135f 100644
--- a/src/jetzig/loggers/DevelopmentLogger.zig
+++ b/src/jetzig/loggers/DevelopmentLogger.zig
@@ -8,28 +8,23 @@ const Timestamp = jetzig.types.Timestamp;
const LogLevel = jetzig.loggers.LogLevel;
allocator: std.mem.Allocator,
-stdout: std.fs.File,
-stderr: std.fs.File,
stdout_colorized: bool,
stderr_colorized: bool,
level: LogLevel,
-mutex: std.Thread.Mutex,
+log_queue: *jetzig.loggers.LogQueue,
/// Initialize a new Development Logger.
pub fn init(
allocator: std.mem.Allocator,
level: LogLevel,
- stdout: std.fs.File,
- stderr: std.fs.File,
+ log_queue: *jetzig.loggers.LogQueue,
) DevelopmentLogger {
return .{
.allocator = allocator,
.level = level,
- .stdout = stdout,
- .stderr = stderr,
- .stdout_colorized = stdout.isTty(),
- .stderr_colorized = stderr.isTty(),
- .mutex = std.Thread.Mutex{},
+ .log_queue = log_queue,
+ .stdout_colorized = log_queue.stdout_is_tty,
+ .stderr_colorized = log_queue.stderr_is_tty,
};
}
@@ -45,40 +40,28 @@ pub fn log(
const output = try std.fmt.allocPrint(self.allocator, message, args);
defer self.allocator.free(output);
- const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
- const iso8601 = try timestamp.iso8601();
- defer self.allocator.free(iso8601);
+ const timestamp = Timestamp.init(std.time.timestamp());
+ var timestamp_buf: [256]u8 = undefined;
+ const iso8601 = try timestamp.iso8601(×tamp_buf);
- const colorized = switch (level) {
- .TRACE, .DEBUG, .INFO => self.stdout_colorized,
- .WARN, .ERROR, .FATAL => self.stderr_colorized,
- };
- const file = switch (level) {
- .TRACE, .DEBUG, .INFO => self.stdout,
- .WARN, .ERROR, .FATAL => self.stderr,
- };
- const writer = file.writer();
- const level_formatted = if (colorized) colorizedLogLevel(level) else @tagName(level);
+ const target = jetzig.loggers.logTarget(level);
+ const formatted_level = colorizedLogLevel(level);
- @constCast(self).mutex.lock();
- defer @constCast(self).mutex.unlock();
-
- try writer.print("{s: >5} [{s}] {s}\n", .{ level_formatted, iso8601, output });
-
- if (!file.isTty()) try file.sync();
+ try self.log_queue.print(
+ "{s: >5} [{s}] {s}\n",
+ .{ formatted_level, iso8601, output },
+ target,
+ );
}
/// Log a one-liner including response status code, path, method, duration, etc.
pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) !void {
- const formatted_duration = if (self.stdout_colorized)
- try jetzig.colors.duration(self.allocator, jetzig.util.duration(request.start_time))
- else
- try std.fmt.allocPrint(
- self.allocator,
- "{}",
- .{std.fmt.fmtDurationSigned(jetzig.util.duration(request.start_time))},
- );
- defer self.allocator.free(formatted_duration);
+ var duration_buf: [256]u8 = undefined;
+ const formatted_duration = try jetzig.colors.duration(
+ &duration_buf,
+ jetzig.util.duration(request.start_time),
+ self.stdout_colorized,
+ );
const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) {
inline else => |status_code| @unionInit(
@@ -93,17 +76,23 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
else
status.getFormatted(.{});
- const message = try std.fmt.allocPrint(self.allocator, "[{s}/{s}/{s}] {s}", .{
+ const timestamp = Timestamp.init(std.time.timestamp());
+ var timestamp_buf: [256]u8 = undefined;
+ const iso8601 = try timestamp.iso8601(×tamp_buf);
+
+ 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", .{
+ formatted_level,
+ iso8601,
formatted_duration,
request.fmtMethod(self.stdout_colorized),
formatted_status,
request.path.path,
- });
- defer self.allocator.free(message);
- try self.log(.INFO, "{s}", .{message});
+ }, .stdout);
}
-fn colorizedLogLevel(comptime level: LogLevel) []const u8 {
+inline fn colorizedLogLevel(comptime level: LogLevel) []const u8 {
return switch (level) {
.TRACE => jetzig.colors.white(@tagName(level)),
.DEBUG => jetzig.colors.cyan(@tagName(level)),
diff --git a/src/jetzig/loggers/JsonLogger.zig b/src/jetzig/loggers/JsonLogger.zig
index 0399a77..5beb3e3 100644
--- a/src/jetzig/loggers/JsonLogger.zig
+++ b/src/jetzig/loggers/JsonLogger.zig
@@ -11,6 +11,7 @@ const LogMessage = struct {
timestamp: []const u8,
message: []const u8,
};
+
const RequestLogMessage = struct {
level: []const u8,
timestamp: []const u8,
@@ -21,24 +22,19 @@ const RequestLogMessage = struct {
};
allocator: std.mem.Allocator,
-stdout: std.fs.File,
-stderr: std.fs.File,
+log_queue: *jetzig.loggers.LogQueue,
level: LogLevel,
-mutex: std.Thread.Mutex,
/// Initialize a new JSON Logger.
pub fn init(
allocator: std.mem.Allocator,
level: LogLevel,
- stdout: std.fs.File,
- stderr: std.fs.File,
+ log_queue: *jetzig.loggers.LogQueue,
) JsonLogger {
return .{
.allocator = allocator,
.level = level,
- .stdout = stdout,
- .stderr = stderr,
- .mutex = std.Thread.Mutex{},
+ .log_queue = log_queue,
};
}
@@ -54,24 +50,16 @@ pub fn log(
const output = try std.fmt.allocPrint(self.allocator, message, args);
defer self.allocator.free(output);
- const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
- const iso8601 = try timestamp.iso8601();
- defer self.allocator.free(iso8601);
+ const timestamp = Timestamp.init(std.time.timestamp());
+ var timestamp_buf: [256]u8 = undefined;
+ const iso8601 = try timestamp.iso8601(×tamp_buf);
- const file = self.getFile(level);
- const writer = file.writer();
const log_message = LogMessage{ .level = @tagName(level), .timestamp = iso8601, .message = output };
const json = try std.json.stringifyAlloc(self.allocator, log_message, .{ .whitespace = .minified });
defer self.allocator.free(json);
- @constCast(self).mutex.lock();
- defer @constCast(self).mutex.unlock();
-
- try writer.writeAll(json);
- try writer.writeByte('\n');
-
- if (!file.isTty()) try file.sync(); // Make configurable ?
+ try self.log_queue.print("{s}\n", .{json}, jetzig.loggers.logTarget(level));
}
/// Log a one-liner including response status code, path, method, duration, etc.
@@ -80,9 +68,9 @@ pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request)
const duration = jetzig.util.duration(request.start_time);
- const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
- const iso8601 = try timestamp.iso8601();
- defer self.allocator.free(iso8601);
+ const timestamp = Timestamp.init(std.time.timestamp());
+ var timestamp_buf: [256]u8 = undefined;
+ const iso8601 = try timestamp.iso8601(×tamp_buf);
const status = switch (request.response.status_code) {
inline else => |status_code| @unionInit(
@@ -91,6 +79,7 @@ pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request)
.{},
),
};
+
const message = RequestLogMessage{
.level = @tagName(level),
.timestamp = iso8601,
@@ -99,19 +88,17 @@ pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request)
.path = request.path.path,
.duration = duration,
};
- const json = try std.json.stringifyAlloc(self.allocator, message, .{ .whitespace = .minified });
- defer self.allocator.free(json);
- const file = self.getFile(level);
- const writer = file.writer();
+ var buf: [4096]u8 = undefined;
+ var stream = std.io.fixedBufferStream(&buf);
+ std.json.stringify(message, .{ .whitespace = .minified }, stream.writer()) catch |err| {
+ switch (err) {
+ error.NoSpaceLeft => {}, // TODO: Spill to heap
+ else => return err,
+ }
+ };
- @constCast(self).mutex.lock();
- defer @constCast(self).mutex.unlock();
-
- try writer.writeAll(json);
- try writer.writeByte('\n');
-
- if (!file.isTty()) try file.sync(); // Make configurable ?
+ try self.log_queue.print("{s}\n", .{stream.getWritten()}, .stdout);
}
fn getFile(self: JsonLogger, level: LogLevel) std.fs.File {
diff --git a/src/jetzig/loggers/LogQueue.zig b/src/jetzig/loggers/LogQueue.zig
new file mode 100644
index 0000000..b49edd6
--- /dev/null
+++ b/src/jetzig/loggers/LogQueue.zig
@@ -0,0 +1,370 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+const jetzig = @import("../../jetzig.zig");
+
+const buffer_size = jetzig.config.get(usize, "log_message_buffer_len");
+const max_pool_len = jetzig.config.get(usize, "max_log_pool_len");
+
+const List = std.DoublyLinkedList(Event);
+const Buffer = [buffer_size]u8;
+
+allocator: std.mem.Allocator,
+node_allocator: std.heap.MemoryPool(List.Node),
+buffer_allocator: std.heap.MemoryPool(Buffer),
+list: List,
+read_write_mutex: std.Thread.Mutex,
+condition: std.Thread.Condition,
+condition_mutex: std.Thread.Mutex,
+writer: Writer = undefined,
+reader: Reader = undefined,
+node_pool: std.ArrayList(*List.Node),
+buffer_pool: std.ArrayList(*Buffer),
+position: usize,
+stdout_is_tty: bool = undefined,
+stderr_is_tty: bool = undefined,
+stdout_colorize: bool = undefined,
+stderr_colorize: bool = undefined,
+state: enum { pending, ready } = .pending,
+
+const LogQueue = @This();
+
+pub const Target = enum { stdout, stderr };
+
+const Event = struct {
+ message: *Buffer,
+ len: usize,
+ target: Target,
+ ptr: ?[]const u8,
+};
+
+/// Create a new `LogQueue`.
+pub fn init(allocator: std.mem.Allocator) LogQueue {
+ return .{
+ .allocator = allocator,
+ .node_allocator = initPool(allocator, List.Node),
+ .buffer_allocator = initPool(allocator, Buffer),
+ .list = List{},
+ .condition = std.Thread.Condition{},
+ .condition_mutex = std.Thread.Mutex{},
+ .read_write_mutex = std.Thread.Mutex{},
+ .node_pool = std.ArrayList(*List.Node).init(allocator),
+ .buffer_pool = std.ArrayList(*Buffer).init(allocator),
+ .position = 0,
+ };
+}
+
+/// Free allocated resources and return to `pending` state.
+pub fn deinit(self: *LogQueue) void {
+ self.node_pool.deinit();
+ self.buffer_pool.deinit();
+
+ self.buffer_allocator.deinit();
+ self.node_allocator.deinit();
+
+ self.state = .pending;
+}
+
+/// Set the stdout and stderr outputs. Must be called before `print`.
+pub fn setFiles(self: *LogQueue, stdout_file: std.fs.File, stderr_file: std.fs.File) !void {
+ self.writer = Writer{
+ .queue = self,
+ .mutex = std.Thread.Mutex{},
+ };
+ self.reader = Reader{
+ .stdout_file = stdout_file,
+ .stderr_file = stderr_file,
+ .queue = self,
+ };
+ self.stdout_is_tty = stdout_file.isTty();
+ self.stderr_is_tty = stderr_file.isTty();
+
+ self.stdout_colorize = std.io.tty.detectConfig(stdout_file) != .no_color;
+ self.stderr_colorize = std.io.tty.detectConfig(stderr_file) != .no_color;
+
+ self.state = .ready;
+}
+
+pub fn print(self: *LogQueue, comptime message: []const u8, args: anytype, target: Target) !void {
+ std.debug.assert(self.state == .ready);
+
+ try self.writer.print(message, args, target);
+}
+
+/// Writer for `LogQueue`. Receives log events and publishes to the queue.
+pub const Writer = struct {
+ queue: *LogQueue,
+ position: usize = 0,
+ mutex: std.Thread.Mutex,
+
+ /// Print a log event. Messages longer than `jetzig.config.get(usize, "log_message_buffer_len")`
+ /// spill to heap with degraded performance. Adjust buffer length or limit long entries to
+ /// ensure fast logging performance.
+ /// `target` must be `.stdout` or `.stderr`.
+ pub fn print(
+ self: *Writer,
+ comptime message: []const u8,
+ args: anytype,
+ target: Target,
+ ) !void {
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ const buf = try self.getBuffer();
+ self.position += 1;
+ var ptr: ?[]const u8 = null;
+
+ const result = std.fmt.bufPrint(buf, message, args) catch |err| switch (err) {
+ error.NoSpaceLeft => blk: {
+ ptr = try std.fmt.allocPrint(self.queue.allocator, message, args);
+ self.position -= 1;
+ break :blk null;
+ },
+ };
+
+ try self.queue.append(.{
+ .message = buf,
+ .target = target,
+ .len = if (ptr) |capture| capture.len else result.?.len,
+ .ptr = ptr,
+ });
+ }
+
+ fn getBuffer(self: *Writer) !*Buffer {
+ const buffer = if (self.position >= self.queue.buffer_pool.items.len)
+ try self.queue.buffer_allocator.create()
+ else
+ self.queue.buffer_pool.items[self.position];
+
+ return buffer;
+ }
+};
+
+/// Reader for `LogQueue`. Reads log events from the queue and writes them to the designated
+/// target (stdout or stderr).
+pub const Reader = struct {
+ stdout_file: std.fs.File,
+ stderr_file: std.fs.File,
+ queue: *LogQueue,
+
+ /// Publish log events from the queue. Invoke from a dedicated thread. Sleeps when log queue
+ /// is empty, wakes up when a new event is published.
+ pub fn publish(self: *Reader, options: struct { oneshot: bool = false }) !void {
+ std.debug.assert(self.queue.state == .ready);
+
+ const stdout_writer = self.stdout_file.writer();
+ const stderr_writer = self.stderr_file.writer();
+
+ while (true) {
+ self.queue.condition_mutex.lock();
+ defer self.queue.condition_mutex.unlock();
+
+ if (!options.oneshot) self.queue.condition.wait(&self.queue.condition_mutex);
+
+ var stdout_written = false;
+ var stderr_written = false;
+ var file: std.fs.File = undefined;
+ var colorize = false;
+
+ while (try self.queue.popFirst()) |event| {
+ self.queue.writer.mutex.lock();
+ defer self.queue.writer.mutex.unlock();
+
+ const writer = switch (event.target) {
+ .stdout => blk: {
+ stdout_written = true;
+ if (builtin.os.tag == .windows) {
+ file = self.stdout_file;
+ colorize = self.queue.stdout_colorize;
+ }
+ break :blk stdout_writer;
+ },
+ .stderr => blk: {
+ stderr_written = true;
+ if (builtin.os.tag == .windows) {
+ file = self.stderr_file;
+ colorize = self.queue.stderr_colorize;
+ }
+ break :blk stderr_writer;
+ },
+ };
+
+ if (event.ptr) |ptr| {
+ // Log message spilled to heap
+ defer self.queue.allocator.free(ptr);
+ try writer.writeAll(ptr);
+ continue;
+ }
+
+ if (builtin.os.tag == .windows and colorize) {
+ try writeWindows(file, writer, event);
+ } else {
+ try writer.writeAll(event.message[0..event.len]);
+ }
+
+ self.queue.writer.position -= 1;
+
+ if (self.queue.writer.position < self.queue.buffer_pool.items.len) {
+ self.queue.buffer_pool.items[self.queue.writer.position] = event.message;
+ } else {
+ if (self.queue.buffer_pool.items.len >= max_pool_len) {
+ self.queue.buffer_allocator.destroy(@alignCast(event.message));
+ self.queue.writer.position += 1;
+ } else {
+ try self.queue.buffer_pool.append(event.message);
+ }
+ }
+ }
+
+ if (stdout_written and !self.queue.stdout_is_tty) try self.stdout_file.sync();
+ if (stderr_written and !self.queue.stderr_is_tty) try self.stderr_file.sync();
+
+ if (options.oneshot) break;
+ }
+ }
+};
+
+// Append a log event to the queue. Signal the publish loop thread to wake up. Recycle nodes if
+// available in the pool, otherwise create a new one.
+fn append(self: *LogQueue, event: Event) !void {
+ self.read_write_mutex.lock();
+ defer self.read_write_mutex.unlock();
+
+ const node = if (self.position >= self.node_pool.items.len)
+ try self.node_allocator.create()
+ else
+ self.node_pool.items[self.position];
+
+ self.position += 1;
+
+ node.* = .{ .data = event };
+ self.list.append(node);
+
+ self.condition.signal();
+}
+
+// Pop a log event from the queue. Return node to the pool for re-use.
+fn popFirst(self: *LogQueue) !?Event {
+ self.read_write_mutex.lock();
+ defer self.read_write_mutex.unlock();
+
+ if (self.list.popFirst()) |node| {
+ const value = node.data;
+ self.position -= 1;
+ if (self.position < self.node_pool.items.len) {
+ self.node_pool.items[self.position] = node;
+ } else {
+ if (self.node_pool.items.len >= max_pool_len) {
+ self.node_allocator.destroy(node);
+ self.position += 1;
+ } else {
+ try self.node_pool.append(node);
+ }
+ }
+ return value;
+ } else {
+ return null;
+ }
+}
+
+fn initPool(allocator: std.mem.Allocator, T: type) std.heap.MemoryPool(T) {
+ return std.heap.MemoryPool(T).initPreheated(allocator, max_pool_len) catch @panic("OOM");
+}
+
+fn writeWindows(file: std.fs.File, writer: anytype, event: Event) !void {
+ var info: std.os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
+ const config: std.io.tty.Config = if (std.os.windows.kernel32.GetConsoleScreenBufferInfo(
+ file.handle,
+ &info,
+ ) != std.os.windows.TRUE)
+ .no_color
+ else
+ .{ .windows_api = .{
+ .handle = file.handle,
+ .reset_attributes = info.wAttributes,
+ } };
+
+ var it = std.mem.tokenizeSequence(u8, event.message[0..event.len], "\x1b[");
+ while (it.next()) |token| {
+ if (std.mem.indexOfScalar(u8, token, 'm')) |index| {
+ if (index > 0 and index + 1 < token.len) {
+ if (jetzig.colors.codes_map.get(token[0..index])) |color| {
+ try config.setColor(writer, color);
+ try writer.writeAll(token[index + 1 ..]);
+ continue;
+ }
+ }
+ }
+ // Fallback
+ try writer.writeAll(token);
+ }
+}
+
+test "print to stdout and stderr" {
+ var log_queue = LogQueue.init(std.testing.allocator);
+ defer log_queue.deinit();
+
+ var tmp_dir = std.testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ const stdout = try tmp_dir.dir.createFile("stdout.log", .{ .read = true });
+ defer stdout.close();
+
+ const stderr = try tmp_dir.dir.createFile("stderr.log", .{ .read = true });
+ defer stderr.close();
+
+ try log_queue.setFiles(stdout, stderr);
+ try log_queue.print("foo {s}\n", .{"bar"}, .stdout);
+ try log_queue.print("baz {s}\n", .{"qux"}, .stderr);
+ try log_queue.print("quux {s}\n", .{"corge"}, .stdout);
+ try log_queue.print("grault {s}\n", .{"garply"}, .stderr);
+ try log_queue.print("waldo {s}\n", .{"fred"}, .stderr);
+ try log_queue.print("plugh {s}\n", .{"zyzzy"}, .stdout);
+
+ try log_queue.reader.publish(.{ .oneshot = true });
+
+ try stdout.seekTo(0);
+ var buf: [1024]u8 = undefined;
+ var len = try stdout.readAll(&buf);
+
+ try std.testing.expectEqualStrings(
+ \\foo bar
+ \\quux corge
+ \\plugh zyzzy
+ \\
+ , buf[0..len]);
+
+ try stderr.seekTo(0);
+ len = try stderr.readAll(&buf);
+ try std.testing.expectEqualStrings(
+ \\baz qux
+ \\grault garply
+ \\waldo fred
+ \\
+ , buf[0..len]);
+}
+
+test "long messages" {
+ var log_queue = LogQueue.init(std.testing.allocator);
+ defer log_queue.deinit();
+
+ var tmp_dir = std.testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ const stdout = try tmp_dir.dir.createFile("stdout.log", .{ .read = true });
+ defer stdout.close();
+
+ const stderr = try tmp_dir.dir.createFile("stderr.log", .{ .read = true });
+ defer stderr.close();
+
+ try log_queue.setFiles(stdout, stderr);
+ try log_queue.print("foo" ** buffer_size, .{}, .stdout);
+
+ try log_queue.reader.publish(.{ .oneshot = true });
+
+ try stdout.seekTo(0);
+ var buf: [buffer_size * 3]u8 = undefined;
+ const len = try stdout.readAll(&buf);
+
+ try std.testing.expectEqualStrings("foo" ** buffer_size, buf[0..len]);
+}
diff --git a/src/jetzig/middleware/HtmxMiddleware.zig b/src/jetzig/middleware/HtmxMiddleware.zig
index ebedcfe..8c56431 100644
--- a/src/jetzig/middleware/HtmxMiddleware.zig
+++ b/src/jetzig/middleware/HtmxMiddleware.zig
@@ -15,7 +15,7 @@ pub fn init(request: *jetzig.http.Request) !*Self {
/// content rendered directly by the view function.
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
_ = self;
- if (request.getHeader("HX-Target")) |target| {
+ if (request.headers.get("HX-Target")) |target| {
try request.server.logger.DEBUG(
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
.{target},
@@ -29,10 +29,9 @@ pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !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 (request.headers.get("HX-Request") == null) return;
- if (response.headers.getFirstValue("Location")) |location| {
- response.headers.remove("Location");
+ if (response.headers.get("Location")) |location| {
response.status_code = .ok;
request.response_data.reset();
try response.headers.append("HX-Redirect", location);
diff --git a/src/jetzig/types/Timestamp.zig b/src/jetzig/types/Timestamp.zig
index 58c7720..cce387f 100644
--- a/src/jetzig/types/Timestamp.zig
+++ b/src/jetzig/types/Timestamp.zig
@@ -3,7 +3,6 @@ const std = @import("std");
const Self = @This();
timestamp: i64,
-allocator: std.mem.Allocator,
const constants = struct {
pub const seconds_in_day: i64 = 60 * 60 * 24;
@@ -12,18 +11,18 @@ const constants = struct {
pub const epoch_year: i64 = 1970;
};
-pub fn init(timestamp: i64, allocator: std.mem.Allocator) Self {
- return .{ .allocator = allocator, .timestamp = timestamp };
+pub fn init(timestamp: i64) Self {
+ return .{ .timestamp = timestamp };
}
-pub fn iso8601(self: *const Self) ![]const u8 {
+pub fn iso8601(self: *const Self, buf: *[256]u8) ![]const u8 {
const u32_year: u32 = @intCast(self.year());
const u32_month: u32 = @intCast(self.month());
const u32_day_of_month: u32 = @intCast(self.dayOfMonth());
const u32_hour: u32 = @intCast(self.hour());
const u32_minute: u32 = @intCast(self.minute());
const u32_second: u32 = @intCast(self.second());
- return try std.fmt.allocPrint(self.allocator, "{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}", .{
+ return try std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}", .{
u32_year,
u32_month,
u32_day_of_month,
diff --git a/src/tests.zig b/src/tests.zig
index fbc9ce4..544934c 100644
--- a/src/tests.zig
+++ b/src/tests.zig
@@ -6,4 +6,5 @@ test {
_ = @import("jetzig/http/Path.zig");
_ = @import("jetzig/jobs/Job.zig");
_ = @import("jetzig/mail/Mail.zig");
+ _ = @import("jetzig/loggers/LogQueue.zig");
}