Refactor response interface

Create `jetzig.http.Response` when `std.http.Server.Response` is created
and delegate all management of `std.http.Server.Response` to
`jetzig.http.Response`. Using this internally means we use the same
internal interface for the `Response` as the user, which gives us things
like response headers management in views for free (user simply calls
`request.response.headers.append()` etc.).
This commit is contained in:
Bob Farrell 2024-02-25 10:32:34 +00:00
parent 2ebb644421
commit a07c71e725
6 changed files with 136 additions and 103 deletions

View File

@ -16,9 +16,9 @@ pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void {
self.my_data = 43; 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] 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 { pub fn deinit(self: *Self, request: *jetzig.http.Request) void {

View File

@ -1,20 +1,24 @@
const std = @import("std"); const std = @import("std");
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
headers: std.http.Headers, std_headers: std.http.Headers,
const Self = @This(); const Self = @This();
pub fn init(allocator: std.mem.Allocator, headers: std.http.Headers) Self { 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 { 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 { 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 { test {

View File

@ -16,6 +16,7 @@ headers: jetzig.http.Headers,
segments: std.ArrayList([]const u8), segments: std.ArrayList([]const u8),
server: *jetzig.http.Server, server: *jetzig.http.Server,
session: *jetzig.http.Session, session: *jetzig.http.Session,
response: *jetzig.http.Response,
status_code: jetzig.http.status_codes.StatusCode = undefined, status_code: jetzig.http.status_codes.StatusCode = undefined,
response_data: *jetzig.data.Data, response_data: *jetzig.data.Data,
query_data: *jetzig.data.Data, query_data: *jetzig.data.Data,
@ -26,10 +27,10 @@ body: []const u8,
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
server: *jetzig.http.Server, server: *jetzig.http.Server,
response: *std.http.Server.Response, response: *jetzig.http.Response,
body: []const u8, body: []const u8,
) !Self { ) !Self {
const method = switch (response.request.method) { const method = switch (response.std_response.request.method) {
.DELETE => Method.DELETE, .DELETE => Method.DELETE,
.GET => Method.GET, .GET => Method.GET,
.PATCH => Method.PATCH, .PATCH => Method.PATCH,
@ -42,14 +43,14 @@ pub fn init(
_ => return error.JetzigUnsupportedHttpMethod, _ => 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); var segments = std.ArrayList([]const u8).init(allocator);
while (it.next()) |segment| try segments.append(segment); while (it.next()) |segment| try segments.append(segment);
var cookies = try allocator.create(jetzig.http.Cookies); var cookies = try allocator.create(jetzig.http.Cookies);
cookies.* = jetzig.http.Cookies.init( cookies.* = jetzig.http.Cookies.init(
allocator, allocator,
response.request.headers.getFirstValue("Cookie") orelse "", response.std_response.request.headers.getFirstValue("Cookie") orelse "",
); );
try cookies.parse(); try cookies.parse();
@ -75,9 +76,9 @@ pub fn init(
return .{ return .{
.allocator = allocator, .allocator = allocator,
.path = response.request.target, .path = response.std_response.request.target,
.method = method, .method = method,
.headers = jetzig.http.Headers.init(allocator, response.request.headers), .headers = jetzig.http.Headers.init(allocator, response.std_response.request.headers),
.server = server, .server = server,
.segments = segments, .segments = segments,
.cookies = cookies, .cookies = cookies,
@ -86,6 +87,7 @@ pub fn init(
.query_data = query_data, .query_data = query_data,
.query = query, .query = query,
.body = body, .body = body,
.response = response,
}; };
} }

View File

@ -1,34 +1,82 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const http = @import("../http.zig"); const http = @import("../http.zig");
const Self = @This(); const Self = @This();
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
std_response: *std.http.Server.Response,
headers: *jetzig.http.Headers,
content: []const u8, content: []const u8,
status_code: http.status_codes.StatusCode, status_code: http.status_codes.StatusCode,
content_type: []const u8, content_type: []const u8,
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
content: []const u8, std_response: *std.http.Server.Response,
status_code: http.status_codes.StatusCode, ) !Self {
content_type: []const u8, const headers = try allocator.create(jetzig.http.Headers);
) Self { headers.* = jetzig.http.Headers.init(allocator, std_response.headers);
return .{ return .{
.status_code = status_code,
.content = content,
.content_type = content_type,
.allocator = allocator, .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 { pub fn deinit(self: *const Self) void {
_ = self; self.headers.deinit();
// self.allocator.free(self.content); // self.allocator.free(self.content);
// self.allocator.free(self.content_type); // 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 { pub fn dupe(self: *const Self) !Self {
return .{ return .{
.allocator = self.allocator, .allocator = self.allocator,

View File

@ -67,7 +67,12 @@ pub fn listen(self: *Self) !void {
fn processRequests(self: *Self) !void { fn processRequests(self: *Self) !void {
while (true) { 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(); errdefer response.deinit();
try response.headers.append("Connection", "close"); 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(); try response.wait();
self.start_time = std.time.nanoTimestamp(); 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); defer self.allocator.free(body);
var arena = std.heap.ArenaAllocator.init(self.allocator); 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 middleware_data = try jetzig.http.middleware.beforeMiddleware(&request);
var result = try self.pageContent(&request); try self.renderResponse(&request, response);
defer result.deinit();
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(); var cookie_it = request.cookies.headerIterator();
while (try cookie_it.next()) |header| { while (try cookie_it.next()) |header| {
// FIXME: Skip setting cookies that are already present ? // FIXME: Skip setting cookies that are already present ?
try response.headers.append("Set-Cookie", header); 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(); 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); defer self.allocator.free(log_message);
self.logger.debug("{s}", .{log_message}); self.logger.debug("{s}", .{log_message});
jetzig.http.middleware.deinit(&middleware_data, &request); jetzig.http.middleware.deinit(&middleware_data, &request);
} }
fn pageContent(self: *Self, request: *jetzig.http.Request) !jetzig.caches.Result { fn renderResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
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 {
const static = self.matchStaticResource(request) catch |err| { const static = self.matchStaticResource(request) catch |err| {
if (isUnhandledError(err)) return err; if (isUnhandledError(err)) return err;
const rendered = try self.renderInternalServerError(request, err); const rendered = try self.renderInternalServerError(request, err);
return .{
.allocator = self.allocator, response.content = rendered.content;
.status_code = .internal_server_error, response.status_code = .internal_server_error;
.content = rendered.content, response.content_type = "text/html";
.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); const route = try self.matchRoute(request, false);
switch (request.requestFormat()) { switch (request.requestFormat()) {
.HTML => return self.renderHTML(request, route), .HTML => try self.renderHTML(request, response, route),
.JSON => return self.renderJSON(request, route), .JSON => try self.renderJSON(request, response, route),
.UNKNOWN => return self.renderHTML(request, route), .UNKNOWN => try self.renderHTML(request, response, route),
} }
} }
fn renderStatic(self: *Self, request: *jetzig.http.Request, resource: StaticResource) !jetzig.http.Response { fn renderStatic(resource: StaticResource, response: *jetzig.http.Response) !void {
_ = request; response.status_code = .ok;
return .{ response.content = resource.content;
.allocator = self.allocator, response.content_type = resource.mime_type;
.status_code = .ok,
.content = resource.content,
.content_type = resource.mime_type,
};
} }
fn renderHTML( fn renderHTML(
self: *Self, self: *Self,
request: *jetzig.http.Request, request: *jetzig.http.Request,
response: *jetzig.http.Response,
route: ?jetzig.views.Route, route: ?jetzig.views.Route,
) !jetzig.http.Response { ) !void {
if (route) |matched_route| { if (route) |matched_route| {
for (self.templates) |template| { for (self.templates) |template| {
// TODO: Use a hashmap to avoid O(n) // TODO: Use a hashmap to avoid O(n)
if (std.mem.eql(u8, matched_route.template, template.name)) { if (std.mem.eql(u8, matched_route.template, template.name)) {
const rendered = try self.renderView(matched_route, request, template); const rendered = try self.renderView(matched_route, request, template);
return .{ response.content = rendered.content;
.allocator = self.allocator, response.status_code = rendered.view.status_code;
.content = rendered.content, response.content_type = "text/html";
.status_code = rendered.view.status_code, return;
.content_type = "text/html",
};
} }
} }
return .{ response.content = "";
.allocator = self.allocator, response.status_code = .not_found;
.content = "", response.content_type = "text/html";
.status_code = .not_found, return;
.content_type = "text/html",
};
} else { } else {
return .{ response.content = "";
.allocator = self.allocator, response.status_code = .not_found;
.content = "", response.content_type = "text/html";
.status_code = .not_found,
.content_type = "text/html",
};
} }
} }
fn renderJSON( fn renderJSON(
self: *Self, self: *Self,
request: *jetzig.http.Request, request: *jetzig.http.Request,
response: *jetzig.http.Response,
route: ?jetzig.views.Route, route: ?jetzig.views.Route,
) !jetzig.http.Response { ) !void {
if (route) |matched_route| { if (route) |matched_route| {
const rendered = try self.renderView(matched_route, request, null); const rendered = try self.renderView(matched_route, request, null);
var data = rendered.view.data; var data = rendered.view.data;
@ -222,18 +205,14 @@ fn renderJSON(
if (data.value) |_| {} else _ = try data.object(); if (data.value) |_| {} else _ = try data.object();
try request.headers.append("Content-Type", "application/json"); try request.headers.append("Content-Type", "application/json");
return .{ response.content = try data.toJson();
.allocator = self.allocator, response.status_code = rendered.view.status_code;
.content = try data.toJson(), response.content_type = "application/json";
.status_code = rendered.view.status_code, } else {
.content_type = "application/json", response.content = "";
}; response.status_code = .not_found;
} else return .{ response.content_type = "application/json";
.allocator = self.allocator, }
.content = "",
.status_code = .not_found,
.content_type = "application/json",
};
} }
const RenderedView = struct { view: jetzig.views.View, content: []const u8 }; 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)); try object.put("backtrace", request.response_data.string(array.items));
} }
fn requestLogMessage(self: *Self, request: *jetzig.http.Request, result: jetzig.caches.Result) ![]const u8 { fn requestLogMessage(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) ![]const u8 {
const status: jetzig.http.status_codes.TaggedStatusCode = switch (result.value.status_code) { const status: jetzig.http.status_codes.TaggedStatusCode = switch (response.status_code) {
inline else => |status_code| @unionInit( inline else => |status_code| @unionInit(
jetzig.http.status_codes.TaggedStatusCode, jetzig.http.status_codes.TaggedStatusCode,
@tagName(status_code), @tagName(status_code),

View File

@ -40,7 +40,7 @@ pub fn beforeMiddleware(request: *jetzig.http.Request) !MiddlewareData {
pub fn afterMiddleware( pub fn afterMiddleware(
middleware_data: *MiddlewareData, middleware_data: *MiddlewareData,
request: *jetzig.http.Request, request: *jetzig.http.Request,
result: *jetzig.caches.Result, response: *jetzig.http.Response,
) !void { ) !void {
inline for (middlewares, 0..) |middleware, index| { inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "afterRequest")) continue; if (comptime !@hasDecl(middleware, "afterRequest")) continue;
@ -49,10 +49,10 @@ pub fn afterMiddleware(
try @call( try @call(
.always_inline, .always_inline,
middleware.afterRequest, middleware.afterRequest,
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, result }, .{ @as(*middleware, @ptrCast(@alignCast(data))), request, response },
); );
} else { } else {
try @call(.always_inline, middleware.afterRequest, .{ request, result }); try @call(.always_inline, middleware.afterRequest, .{ request, response });
} }
} }
} }