From 7339641401a0304c204da837e67f05db81e0c9cb Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Wed, 29 May 2024 19:10:16 +0100 Subject: [PATCH] Refactoring --- cli/util.zig | 2 +- demo/src/main.zig | 11 +-- src/jetzig/http/Server.zig | 12 +-- src/jetzig/loggers.zig | 6 ++ src/jetzig/loggers/DevelopmentLogger.zig | 13 +++ src/jetzig/loggers/JsonLogger.zig | 4 + .../middleware/CompressionMiddleware.zig | 97 ++++++++----------- src/jetzig/util.zig | 21 ++++ 8 files changed, 92 insertions(+), 74 deletions(-) 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 2c249bf..013d928 100644 --- a/demo/src/main.zig +++ b/demo/src/main.zig @@ -11,14 +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, - // Compression middleware compresses every request to decrease transfer size - jetzig.middleware.CompressionMiddleware, - // 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/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/CompressionMiddleware.zig b/src/jetzig/middleware/CompressionMiddleware.zig index 74406b8..5417495 100644 --- a/src/jetzig/middleware/CompressionMiddleware.zig +++ b/src/jetzig/middleware/CompressionMiddleware.zig @@ -1,72 +1,59 @@ const std = @import("std"); -const jetzig = @import("jetzig"); +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", + }; -fn checkType(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 { - None, - Gzip, - Deflate, -}; -const err_msg = "Response was not compressed due to error: {s}"; +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 { - // Only some file types need compressions, skip the others - if (!checkType(response.content_type)) return; + if (!isCompressable(response.content_type)) return; + const encoding = detectEncoding(request) orelse return; - // Find matching encoding - var encoding = Encoding.None; - for (request.headers.getAll("Accept-Encoding")) |encodings| find_encoding: { - var buffer: [64]u8 = undefined; - var encodings_stream = std.io.fixedBufferStream(encodings); - var encodings_reader = encodings_stream.reader(); - while (try encodings_reader.readUntilDelimiterOrEof(&buffer, ',')) |encoding_str| { - const encoding_trimmed = encoding_str[if (encoding_str[0] == ' ') 1 else 0..]; - const encoding_list = .{ "gzip", "deflate" }; - inline for (encoding_list, 0..) |encoding_compare, i| { - if (std.mem.eql(u8, encoding_compare, encoding_trimmed)) { - encoding = @enumFromInt(i + 1); - break :find_encoding; - } - } - std.debug.print("Encoding: {s}\n", .{encoding_str}); - } - } - if (encoding == .None) 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); - // Compress data - var compressed = std.ArrayList(u8).init(request.allocator); - var content_reader = std.io.fixedBufferStream(response.content); - switch (encoding) { - .Gzip => { - std.compress.gzip.compress(content_reader.reader(), compressed.writer(), .{ .level = .fast }) catch |err| - return request.server.logger.ERROR(err_msg, .{@errorName(err)}); - response.headers.append("Content-Encoding", "gzip") catch |err| - return request.server.logger.ERROR(err_msg, .{@errorName(err)}); - }, - .Deflate => { - std.compress.flate.compress(content_reader.reader(), compressed.writer(), .{ .level = .fast }) catch |err| - return request.server.logger.ERROR(err_msg, .{@errorName(err)}); - response.headers.append("Content-Encoding", "deflate") catch |err| - return request.server.logger.ERROR(err_msg, .{@errorName(err)}); - }, - else => { - // The compression is not supported - // TODO: Can add zstd and br in the future, but gzip / deflate - // support through the std is good enough - return; - }, - } // Make caching work response.headers.append("Vary", "Accept-Encoding") catch |err| - return request.server.logger.ERROR(err_msg, .{@errorName(err)}); + return request.server.logger.logError(err); - response.content = compressed.items; + response.content = compressed; +} + +fn detectEncoding(request: *const jetzig.http.Request) ?Encoding { + for (request.headers.getAll("Accept-Encoding")) |encodings| { + var it = std.mem.tokenizeScalar(u8, encodings, ','); + 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";