diff --git a/cli/util.zig b/cli/util.zig index 9ff49d9..002f0b7 100644 --- a/cli/util.zig +++ b/cli/util.zig @@ -82,7 +82,7 @@ fn isPath(dir: std.fs.Dir, sub_path: []const u8, path_type: enum { file, dir }) } // Strip leading and trailing whitespace from a u8 slice. -pub fn strip(input: []const u8) []const u8 { +pub inline fn strip(input: []const u8) []const u8 { return std.mem.trim(u8, input, &std.ascii.whitespace); } diff --git a/demo/src/main.zig b/demo/src/main.zig index d22356f..013d928 100644 --- a/demo/src/main.zig +++ b/demo/src/main.zig @@ -11,12 +11,9 @@ pub const jetzig_options = struct { /// Middleware chain. Add any custom middleware here, or use middleware provided in /// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`). pub const middleware: []const type = &.{ - // htmx middleware skips layouts when `HX-Target` header is present and issues - // `HX-Redirect` instead of a regular HTTP redirect when `request.redirect` is called. - jetzig.middleware.HtmxMiddleware, - // Demo middleware included with new projects. Remove once you are familiar with Jetzig's - // middleware system. - @import("app/middleware/DemoMiddleware.zig"), + // jetzig.middleware.HtmxMiddleware, + // jetzig.middleware.CompressionMiddleware, + // @import("app/middleware/DemoMiddleware.zig"), }; // Maximum bytes to allow in request body. diff --git a/src/jetzig/http/Headers.zig b/src/jetzig/http/Headers.zig index 8db8c56..89d3671 100644 --- a/src/jetzig/http/Headers.zig +++ b/src/jetzig/http/Headers.zig @@ -59,6 +59,10 @@ pub fn getFirstValue(self: *const Headers, name: []const u8) ?[]const u8 { return self.get(name); } +pub fn count(self: Headers) usize { + return self.httpz_headers.len; +} + /// Add `name` and `value` to headers. pub fn append(self: *Headers, name: []const u8, value: []const u8) !void { if (self.httpz_headers.len >= self.httpz_headers.keys.len) return error.JetzigTooManyHeaders; @@ -75,6 +79,46 @@ pub fn append(self: *Headers, name: []const u8, value: []const u8) !void { self.httpz_headers.add(header.name, header.value); } +const Iterator = struct { + position: usize = 0, + headers: Headers, + filter_name: ?[]const u8 = null, + + pub fn next(self: *Iterator) ?Header { + const header_count = self.headers.count(); + if (self.position >= header_count) { + return null; + } + const start = self.position; + + var buf: [jetzig.config.get(u16, "max_bytes_header_name")]u8 = undefined; + const filter_name = if (self.filter_name) |name| std.ascii.lowerString(&buf, name) else null; + + for (start..header_count) |index| { + const key = self.headers.httpz_headers.keys[start + index]; + const value = self.headers.httpz_headers.values[start + index]; + self.position += 1; + if (filter_name) |name| { + if (std.mem.eql(u8, name, key)) { + return .{ .name = key, .value = value }; + } + } else { + return .{ .name = key, .value = value }; + } + } + + return null; + } +}; + +pub fn getAllIterator(self: Headers, name: []const u8) Iterator { + return .{ .headers = self, .filter_name = name }; +} + +pub fn iterator(self: Headers) Iterator { + return .{ .headers = self }; +} + test "append (deprecated)" { const allocator = std.testing.allocator; var httpz_headers = try httpz.key_value.KeyValue.init(allocator, 10); diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index faf33b8..6415358 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -398,10 +398,7 @@ fn isBadHttpError(err: anyerror) bool { fn renderInternalServerError(self: *Server, request: *jetzig.http.Request, err: anyerror) !RenderedView { request.response_data.reset(); - try self.logger.ERROR("Encountered Error: {s}", .{@errorName(err)}); - - const stack = @errorReturnTrace(); - if (stack) |capture| try self.logStackTrace(capture, .{ .jetzig = request }); + try self.logger.logError(err); const status = .internal_server_error; return try self.renderError(request, status); @@ -444,12 +441,7 @@ fn renderErrorView( _ = route.render(route.*, request) catch |err| { if (isUnhandledError(err)) return err; - try self.logger.ERROR( - "Unexepected error occurred while rendering error page: {s}", - .{@errorName(err)}, - ); - const stack = @errorReturnTrace(); - if (stack) |capture| try self.logStackTrace(capture, .{ .jetzig = request }); + try self.logger.logError(err); return try renderDefaultError(request, status_code); }; diff --git a/src/jetzig/loggers.zig b/src/jetzig/loggers.zig index 6ce6135..e98f3c8 100644 --- a/src/jetzig/loggers.zig +++ b/src/jetzig/loggers.zig @@ -70,6 +70,12 @@ pub const Logger = union(enum) { } } + pub fn logError(self: *const Logger, err: anyerror) !void { + switch (self.*) { + inline else => |*logger| try logger.logError(err), + } + } + pub fn log( self: *const Logger, comptime level: LogLevel, diff --git a/src/jetzig/loggers/DevelopmentLogger.zig b/src/jetzig/loggers/DevelopmentLogger.zig index 19e6e7b..44d65dd 100644 --- a/src/jetzig/loggers/DevelopmentLogger.zig +++ b/src/jetzig/loggers/DevelopmentLogger.zig @@ -104,6 +104,19 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) }, .stdout); } +pub fn logError(self: *const DevelopmentLogger, err: anyerror) !void { + if (@errorReturnTrace()) |stack| { + try self.log(.ERROR, "\nStack Trace:\n{}", .{stack}); + var buf = std.ArrayList(u8).init(self.allocator); + defer buf.deinit(); + const writer = buf.writer(); + try stack.format("", .{}, writer); + try self.logger.ERROR("{s}\n", .{buf.items}); + } + + try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)}); +} + inline fn colorizedLogLevel(comptime level: LogLevel) []const u8 { return switch (level) { .TRACE => jetzig.colors.white(@tagName(level)), diff --git a/src/jetzig/loggers/JsonLogger.zig b/src/jetzig/loggers/JsonLogger.zig index 5beb3e3..eb960d3 100644 --- a/src/jetzig/loggers/JsonLogger.zig +++ b/src/jetzig/loggers/JsonLogger.zig @@ -101,6 +101,10 @@ pub fn logRequest(self: *const JsonLogger, request: *const jetzig.http.Request) try self.log_queue.print("{s}\n", .{stream.getWritten()}, .stdout); } +pub fn logError(self: *const JsonLogger, err: anyerror) !void { + try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)}); +} + fn getFile(self: JsonLogger, level: LogLevel) std.fs.File { return switch (level) { .TRACE, .DEBUG, .INFO => self.stdout, diff --git a/src/jetzig/middleware.zig b/src/jetzig/middleware.zig index 89b27a6..9c01558 100644 --- a/src/jetzig/middleware.zig +++ b/src/jetzig/middleware.zig @@ -2,6 +2,7 @@ const std = @import("std"); const jetzig = @import("../jetzig.zig"); pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig"); +pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig"); const RouteOptions = struct { content: ?[]const u8 = null, diff --git a/src/jetzig/middleware/CompressionMiddleware.zig b/src/jetzig/middleware/CompressionMiddleware.zig new file mode 100644 index 0000000..038f812 --- /dev/null +++ b/src/jetzig/middleware/CompressionMiddleware.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const jetzig = @import("../../jetzig.zig"); + +fn isCompressable(content_type: []const u8) bool { + const type_list = .{ + "text/html", + "application/xhtml+xml", + "application/xml", + "text/css", + "text/javascript", + "application/json", + "application/pdf", + "image/svg+xml", + }; + + inline for (type_list) |content| { + if (std.mem.eql(u8, content_type, content)) return true; + } + return false; +} + +const Encoding = enum { gzip, deflate }; +/// Parse accepted encoding, encode responses if possible, set appropriate headers, and +/// modify the response accordingly to decrease response size +pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void { + if (!isCompressable(response.content_type)) return; + const encoding = detectEncoding(request) orelse return; + + const compressed = switch (encoding) { + .gzip => jetzig.util.gzip(request.allocator, response.content, .{}) catch |err| + return request.server.logger.logError(err), + .deflate => jetzig.util.deflate(request.allocator, response.content, .{}) catch |err| + return request.server.logger.logError(err), + }; + + response.headers.append("Content-Encoding", @tagName(encoding)) catch |err| + return request.server.logger.logError(err); + + // Make caching work + response.headers.append("Vary", "Accept-Encoding") catch |err| + return request.server.logger.logError(err); + + response.content = compressed; +} + +fn detectEncoding(request: *const jetzig.http.Request) ?Encoding { + var headers_it = request.headers.getAllIterator("Accept-Encoding"); + while (headers_it.next()) |header| { + var it = std.mem.tokenizeScalar(u8, header.value, ','); + while (it.next()) |param| { + inline for (@typeInfo(Encoding).Enum.fields) |field| { + if (std.mem.eql(u8, field.name, jetzig.util.strip(param))) { + return std.enums.nameCast(Encoding, field.name); + } + } + } + } + + return null; +} diff --git a/src/jetzig/util.zig b/src/jetzig/util.zig index 23a0180..6b05d9f 100644 --- a/src/jetzig/util.zig +++ b/src/jetzig/util.zig @@ -33,6 +33,27 @@ pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]u8 { return ptr; } +pub fn gzip(allocator: std.mem.Allocator, content: []const u8, options: struct {}) ![]const u8 { + _ = options; // Allow setting compression options later if needed. + var compressed = std.ArrayList(u8).init(allocator); + var content_reader = std.io.fixedBufferStream(content); + try std.compress.gzip.compress(content_reader.reader(), compressed.writer(), .{ .level = .fast }); + return try compressed.toOwnedSlice(); +} + +pub fn deflate(allocator: std.mem.Allocator, content: []const u8, options: struct {}) ![]const u8 { + _ = options; // Allow setting compression options later if needed. + var compressed = std.ArrayList(u8).init(allocator); + var content_reader = std.io.fixedBufferStream(content); + try std.compress.flate.compress(content_reader.reader(), compressed.writer(), .{ .level = .fast }); + return try compressed.toOwnedSlice(); +} + +// Strip leading and trailing whitespace from a u8 slice. +pub inline fn strip(input: []const u8) []const u8 { + return std.mem.trim(u8, input, &std.ascii.whitespace); +} + /// Generate a secure random string of `len` characters (for cryptographic purposes). pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const u8 { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";