diff --git a/demo/src/DemoMiddleware.zig b/demo/src/DemoMiddleware.zig index ad8f60e..b1d8f5c 100644 --- a/demo/src/DemoMiddleware.zig +++ b/demo/src/DemoMiddleware.zig @@ -16,9 +16,9 @@ pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void { self.my_data = 43; } -pub fn afterRequest(self: *Self, request: *jetzig.http.Request, result: *jetzig.caches.Result) !void { +pub fn afterRequest(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { request.server.logger.debug("[middleware] After request, custom data: {d}", .{self.my_data}); - request.server.logger.debug("[middleware] content-type: {s}", .{result.value.content_type}); + request.server.logger.debug("[middleware] content-type: {s}", .{response.content_type}); } pub fn deinit(self: *Self, request: *jetzig.http.Request) void { diff --git a/src/jetzig/http/Headers.zig b/src/jetzig/http/Headers.zig index b64bcf5..783d678 100644 --- a/src/jetzig/http/Headers.zig +++ b/src/jetzig/http/Headers.zig @@ -1,20 +1,24 @@ const std = @import("std"); allocator: std.mem.Allocator, -headers: std.http.Headers, +std_headers: std.http.Headers, const Self = @This(); pub fn init(allocator: std.mem.Allocator, headers: std.http.Headers) Self { - return .{ .allocator = allocator, .headers = headers }; + return .{ .allocator = allocator, .std_headers = headers }; +} + +pub fn deinit(self: *Self) void { + self.std_headers.deinit(); } pub fn getFirstValue(self: *Self, key: []const u8) ?[]const u8 { - return self.headers.getFirstValue(key); + return self.std_headers.getFirstValue(key); } pub fn append(self: *Self, key: []const u8, value: []const u8) !void { - try self.headers.append(key, value); + try self.std_headers.append(key, value); } test { diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index cda2934..4236b9e 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -16,6 +16,7 @@ headers: jetzig.http.Headers, segments: std.ArrayList([]const u8), server: *jetzig.http.Server, session: *jetzig.http.Session, +response: *jetzig.http.Response, status_code: jetzig.http.status_codes.StatusCode = undefined, response_data: *jetzig.data.Data, query_data: *jetzig.data.Data, @@ -26,10 +27,10 @@ body: []const u8, pub fn init( allocator: std.mem.Allocator, server: *jetzig.http.Server, - response: *std.http.Server.Response, + response: *jetzig.http.Response, body: []const u8, ) !Self { - const method = switch (response.request.method) { + const method = switch (response.std_response.request.method) { .DELETE => Method.DELETE, .GET => Method.GET, .PATCH => Method.PATCH, @@ -42,14 +43,14 @@ pub fn init( _ => return error.JetzigUnsupportedHttpMethod, }; - var it = std.mem.splitScalar(u8, response.request.target, '/'); + var it = std.mem.splitScalar(u8, response.std_response.request.target, '/'); var segments = std.ArrayList([]const u8).init(allocator); while (it.next()) |segment| try segments.append(segment); var cookies = try allocator.create(jetzig.http.Cookies); cookies.* = jetzig.http.Cookies.init( allocator, - response.request.headers.getFirstValue("Cookie") orelse "", + response.std_response.request.headers.getFirstValue("Cookie") orelse "", ); try cookies.parse(); @@ -75,9 +76,9 @@ pub fn init( return .{ .allocator = allocator, - .path = response.request.target, + .path = response.std_response.request.target, .method = method, - .headers = jetzig.http.Headers.init(allocator, response.request.headers), + .headers = jetzig.http.Headers.init(allocator, response.std_response.request.headers), .server = server, .segments = segments, .cookies = cookies, @@ -86,6 +87,7 @@ pub fn init( .query_data = query_data, .query = query, .body = body, + .response = response, }; } diff --git a/src/jetzig/http/Response.zig b/src/jetzig/http/Response.zig index e0cc617..848e6c2 100644 --- a/src/jetzig/http/Response.zig +++ b/src/jetzig/http/Response.zig @@ -1,34 +1,82 @@ const std = @import("std"); - +const jetzig = @import("../../jetzig.zig"); const http = @import("../http.zig"); const Self = @This(); allocator: std.mem.Allocator, +std_response: *std.http.Server.Response, +headers: *jetzig.http.Headers, content: []const u8, status_code: http.status_codes.StatusCode, content_type: []const u8, pub fn init( allocator: std.mem.Allocator, - content: []const u8, - status_code: http.status_codes.StatusCode, - content_type: []const u8, -) Self { + std_response: *std.http.Server.Response, +) !Self { + const headers = try allocator.create(jetzig.http.Headers); + headers.* = jetzig.http.Headers.init(allocator, std_response.headers); + return .{ - .status_code = status_code, - .content = content, - .content_type = content_type, .allocator = allocator, + .std_response = std_response, + .status_code = .no_content, + .content_type = "application/octet-stream", + .content = "", + .headers = headers, }; } pub fn deinit(self: *const Self) void { - _ = self; + self.headers.deinit(); // self.allocator.free(self.content); // self.allocator.free(self.content_type); } +const ResetState = enum { reset, closing }; + +/// Resets the current connection. +pub fn reset(self: *const Self) ResetState { + return switch (self.std_response.reset()) { + .reset => .reset, + .closing => .closing, + }; +} + +/// Waits for the current request to finish sending. +pub fn wait(self: *const Self) !void { + try self.std_response.wait(); +} + +pub fn finish(self: *const Self) !void { + self.std_response.status = switch (self.status_code) { + inline else => |status_code| @field(std.http.Status, @tagName(status_code)), + }; + + try self.std_response.send(); + try self.std_response.writeAll(self.content); + try self.std_response.finish(); +} + +/// Reads the current request body. Caller owns memory. +pub fn read(self: *const Self) ![]const u8 { + return try self.std_response.reader().readAllAlloc(self.allocator, jetzig.config.max_bytes_request_body); +} + +const TransferEncodingOptions = struct { + content_length: usize, +}; + +/// Sets the transfer encoding for the current response (content length/chunked encoding). +/// ``` +/// setTransferEncoding(.{ .content_length = 1000 }); +/// ``` +pub fn setTransferEncoding(self: *const Self, transfer_encoding: TransferEncodingOptions) void { + // TODO: Chunked encoding + self.std_response.transfer_encoding = .{ .content_length = transfer_encoding.content_length }; +} + pub fn dupe(self: *const Self) !Self { return .{ .allocator = self.allocator, diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 670f7a5..e3aa3f2 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -67,7 +67,12 @@ pub fn listen(self: *Self) !void { fn processRequests(self: *Self) !void { while (true) { - var response = try self.server.accept(.{ .allocator = self.allocator }); + var std_response = try self.server.accept(.{ .allocator = self.allocator }); + + var response = try jetzig.http.Response.init( + self.allocator, + &std_response, + ); errdefer response.deinit(); try response.headers.append("Connection", "close"); @@ -86,12 +91,12 @@ fn processRequests(self: *Self) !void { } } -fn processNextRequest(self: *Self, response: *std.http.Server.Response) !void { +fn processNextRequest(self: *Self, response: *jetzig.http.Response) !void { try response.wait(); self.start_time = std.time.nanoTimestamp(); - const body = try response.reader().readAllAlloc(self.allocator, jetzig.config.max_bytes_request_body); + const body = try response.read(); defer self.allocator.free(body); var arena = std.heap.ArenaAllocator.init(self.allocator); @@ -102,119 +107,97 @@ fn processNextRequest(self: *Self, response: *std.http.Server.Response) !void { var middleware_data = try jetzig.http.middleware.beforeMiddleware(&request); - var result = try self.pageContent(&request); - defer result.deinit(); + try self.renderResponse(&request, response); - try jetzig.http.middleware.afterMiddleware(&middleware_data, &request, &result); + try jetzig.http.middleware.afterMiddleware(&middleware_data, &request, response); + + response.setTransferEncoding(.{ .content_length = response.content.len }); - response.transfer_encoding = .{ .content_length = result.value.content.len }; var cookie_it = request.cookies.headerIterator(); while (try cookie_it.next()) |header| { // FIXME: Skip setting cookies that are already present ? try response.headers.append("Set-Cookie", header); } - try response.headers.append("Content-Type", result.value.content_type); + try response.headers.append("Content-Type", response.content_type); - response.status = switch (result.value.status_code) { - inline else => |status_code| @field(std.http.Status, @tagName(status_code)), - }; - - try response.send(); - try response.writeAll(result.value.content); try response.finish(); - const log_message = try self.requestLogMessage(&request, result); + const log_message = try self.requestLogMessage(&request, response); defer self.allocator.free(log_message); self.logger.debug("{s}", .{log_message}); jetzig.http.middleware.deinit(&middleware_data, &request); } -fn pageContent(self: *Self, request: *jetzig.http.Request) !jetzig.caches.Result { - const cache_key = try request.hash(); - - if (self.cache.get(cache_key)) |item| { - return item; - } else { - const response = try self.renderResponse(request); - return try self.cache.put(cache_key, response); - } -} - -fn renderResponse(self: *Self, request: *jetzig.http.Request) !jetzig.http.Response { +fn renderResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { const static = self.matchStaticResource(request) catch |err| { if (isUnhandledError(err)) return err; + const rendered = try self.renderInternalServerError(request, err); - return .{ - .allocator = self.allocator, - .status_code = .internal_server_error, - .content = rendered.content, - .content_type = "text/html", - }; + + response.content = rendered.content; + response.status_code = .internal_server_error; + response.content_type = "text/html"; + + return; }; - if (static) |resource| return try self.renderStatic(request, resource); + if (static) |resource| { + try renderStatic(resource, response); + return; + } const route = try self.matchRoute(request, false); switch (request.requestFormat()) { - .HTML => return self.renderHTML(request, route), - .JSON => return self.renderJSON(request, route), - .UNKNOWN => return self.renderHTML(request, route), + .HTML => try self.renderHTML(request, response, route), + .JSON => try self.renderJSON(request, response, route), + .UNKNOWN => try self.renderHTML(request, response, route), } } -fn renderStatic(self: *Self, request: *jetzig.http.Request, resource: StaticResource) !jetzig.http.Response { - _ = request; - return .{ - .allocator = self.allocator, - .status_code = .ok, - .content = resource.content, - .content_type = resource.mime_type, - }; +fn renderStatic(resource: StaticResource, response: *jetzig.http.Response) !void { + response.status_code = .ok; + response.content = resource.content; + response.content_type = resource.mime_type; } fn renderHTML( self: *Self, request: *jetzig.http.Request, + response: *jetzig.http.Response, route: ?jetzig.views.Route, -) !jetzig.http.Response { +) !void { if (route) |matched_route| { for (self.templates) |template| { // TODO: Use a hashmap to avoid O(n) if (std.mem.eql(u8, matched_route.template, template.name)) { const rendered = try self.renderView(matched_route, request, template); - return .{ - .allocator = self.allocator, - .content = rendered.content, - .status_code = rendered.view.status_code, - .content_type = "text/html", - }; + response.content = rendered.content; + response.status_code = rendered.view.status_code; + response.content_type = "text/html"; + return; } } - return .{ - .allocator = self.allocator, - .content = "", - .status_code = .not_found, - .content_type = "text/html", - }; + response.content = ""; + response.status_code = .not_found; + response.content_type = "text/html"; + return; } else { - return .{ - .allocator = self.allocator, - .content = "", - .status_code = .not_found, - .content_type = "text/html", - }; + response.content = ""; + response.status_code = .not_found; + response.content_type = "text/html"; } } fn renderJSON( self: *Self, request: *jetzig.http.Request, + response: *jetzig.http.Response, route: ?jetzig.views.Route, -) !jetzig.http.Response { +) !void { if (route) |matched_route| { const rendered = try self.renderView(matched_route, request, null); var data = rendered.view.data; @@ -222,18 +205,14 @@ fn renderJSON( if (data.value) |_| {} else _ = try data.object(); try request.headers.append("Content-Type", "application/json"); - return .{ - .allocator = self.allocator, - .content = try data.toJson(), - .status_code = rendered.view.status_code, - .content_type = "application/json", - }; - } else return .{ - .allocator = self.allocator, - .content = "", - .status_code = .not_found, - .content_type = "application/json", - }; + response.content = try data.toJson(); + response.status_code = rendered.view.status_code; + response.content_type = "application/json"; + } else { + response.content = ""; + response.status_code = .not_found; + response.content_type = "application/json"; + } } const RenderedView = struct { view: jetzig.views.View, content: []const u8 }; @@ -313,8 +292,8 @@ fn logStackTrace( try object.put("backtrace", request.response_data.string(array.items)); } -fn requestLogMessage(self: *Self, request: *jetzig.http.Request, result: jetzig.caches.Result) ![]const u8 { - const status: jetzig.http.status_codes.TaggedStatusCode = switch (result.value.status_code) { +fn requestLogMessage(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) ![]const u8 { + const status: jetzig.http.status_codes.TaggedStatusCode = switch (response.status_code) { inline else => |status_code| @unionInit( jetzig.http.status_codes.TaggedStatusCode, @tagName(status_code), diff --git a/src/jetzig/http/middleware.zig b/src/jetzig/http/middleware.zig index f0f817f..8729fa0 100644 --- a/src/jetzig/http/middleware.zig +++ b/src/jetzig/http/middleware.zig @@ -40,7 +40,7 @@ pub fn beforeMiddleware(request: *jetzig.http.Request) !MiddlewareData { pub fn afterMiddleware( middleware_data: *MiddlewareData, request: *jetzig.http.Request, - result: *jetzig.caches.Result, + response: *jetzig.http.Response, ) !void { inline for (middlewares, 0..) |middleware, index| { if (comptime !@hasDecl(middleware, "afterRequest")) continue; @@ -49,10 +49,10 @@ pub fn afterMiddleware( try @call( .always_inline, middleware.afterRequest, - .{ @as(*middleware, @ptrCast(@alignCast(data))), request, result }, + .{ @as(*middleware, @ptrCast(@alignCast(data))), request, response }, ); } else { - try @call(.always_inline, middleware.afterRequest, .{ request, result }); + try @call(.always_inline, middleware.afterRequest, .{ request, response }); } } }