diff --git a/build.zig b/build.zig index 19456fa..d1a7486 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" }, diff --git a/build.zig.zon b/build.zig.zon index 56d2735..8bef8ae 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -19,8 +19,12 @@ .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/206a34c0ee35a07b89d000f630b2f1e0f7c98119.tar.gz", + .hash = "1220768b5925b4e13f73c036f1ca18b4a7d987ffaf5e825af6443d5d4ed8e37e7dfd", }, }, 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/main.zig b/demo/src/main.zig index 08213d1..691477e 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. diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig index 9bc812a..75d32fb 100644 --- a/src/jetzig/App.zig +++ b/src/jetzig/App.zig @@ -115,7 +115,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/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/Request.zig b/src/jetzig/http/Request.zig index e260a06..abd6284 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,7 +16,8 @@ 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, @@ -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), .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 }, @@ -134,12 +136,14 @@ pub fn deinit(self: *Request) void { /// Process request, read body if present, parse headers (TODO) 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; + var header_index: usize = 0; + while (header_index < self.httpz_request.headers.len) : (header_index += 1) { + const name = self.httpz_request.headers.keys[header_index]; + const value = self.httpz_request.headers.values[header_index]; + try self.headers.append(name, value); + if (std.mem.eql(u8, name, "Cookie")) cookie = value; } self.cookies = try self.allocator.create(jetzig.http.Cookies); @@ -161,13 +165,27 @@ pub fn process(self: *Request) !void { } }; - 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(); @@ -176,19 +194,14 @@ pub fn respond(self: *Request) !void { try self.response.headers.append("Set-Cookie", header); } - var std_response_headers = try self.response.headers.stdHeaders(); - defer std_response_headers.deinit(self.allocator); + for (self.response.headers.headers.items) |header| { + self.httpz_response.header(header.name, header.value); + } - 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 diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 9434bd6..eb6c470 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, @@ -59,53 +61,75 @@ 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 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, + }; + + // Regular arena deinit occurs in jetzig.http.Request.responseCompletCallback + errdefer state.arena.deinit(); + + const allocator = state.arena.allocator(); + 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; - var response = try jetzig.http.Response.init(allocator); - var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response); + var request = try jetzig.http.Request.init( + allocator, + self, + start_time, + httpz_request, + httpz_response, + &response, + ); try request.process(); @@ -116,7 +140,7 @@ fn processNextRequest(self: *Server, allocator: std.mem.Allocator, std_http_serv 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); 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/LogQueue.zig b/src/jetzig/loggers/LogQueue.zig index 0bdd238..26d8003 100644 --- a/src/jetzig/loggers/LogQueue.zig +++ b/src/jetzig/loggers/LogQueue.zig @@ -41,7 +41,6 @@ pub const Writer = struct { pub fn print(self: *Writer, comptime message: []const u8, args: anytype) !void { const output = try std.fmt.allocPrint(self.queue.allocator, message, args); - defer self.queue.allocator.free(output); try self.queue.append(output); } }; @@ -73,7 +72,7 @@ pub fn append(self: *LogQueue, message: []const u8) !void { defer self.read_write_mutex.unlock(); const node = try self.allocator.create(List.Node); - node.* = .{ .data = try self.allocator.dupe(u8, message) }; + node.* = .{ .data = message }; self.list.append(node); self.condition.signal(); } diff --git a/src/jetzig/windows.zig b/src/jetzig/windows.zig new file mode 100644 index 0000000..37085d3 --- /dev/null +++ b/src/jetzig/windows.zig @@ -0,0 +1,67 @@ +const std = @import("std"); +const Server = @import("http/Server.zig"); +const jetzig = @import("../jetzig.zig"); + +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 }); + + self.initialized = true; + + try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ + self.options.bind, + self.options.port, + @tagName(self.options.environment), + }); + try processRequests(self); +} + +fn processRequests(self: *Server) !void { + while (true) { + var arena = std.heap.ArenaAllocator.init(self.allocator); + errdefer arena.deinit(); + const allocator = arena.allocator(); + + 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(); + + processNextRequest(self, allocator, &std_http_server) catch |err| { + if (Server.isBadHttpError(err)) { + std_http_server.connection.stream.close(); + continue; + } else return err; + }; + + std_http_server.connection.stream.close(); + arena.deinit(); + } +} + +fn processNextRequest(self: *Server, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !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; + + var response = try jetzig.http.Response.init(allocator); + var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &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 jetzig.http.middleware.beforeResponse(&middleware_data, &request); + + try request.respond(); + + try jetzig.http.middleware.afterResponse(&middleware_data, &request); + jetzig.http.middleware.deinit(&middleware_data, &request); + + try self.logger.logRequest(&request); +}