mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
std.http overhaul
Andrew overhauled std.http to avoid allocations and put control of
connection + stream into hands of users. For now, do a barebones
implementation to restore compatibility, later we can do keepalive and
pipelining. For now, still getting decent enough performance the "slow"
way.
Relevant commit: 6395ba852a
Commit message copied here for posterity:
> Author: Andrew Kelley <andrew@ziglang.org>
> Date: Tue Feb 20 03:30:51 2024 -0700
> std.http.Server: rework the API entirely
> Mainly, this removes the poorly named `wait`, `send`, `finish`
> functions, which all operated on the same "Response" object, which was
> actually being used as the request.
>
> Now, it looks like this:
> 1. std.net.Server.accept() gives you a std.net.Server.Connection
> 2. std.http.Server.init() with the connection
> 3. Server.receiveHead() gives you a Request
> 4. Request.reader() gives you a body reader
> 5. Request.respond() is a one-shot, or Request.respondStreaming() creates
> a Response
> 6. Response.writer() gives you a body writer
> 7. Response.end() finishes the response; Response.endChunked() allows
> passing response trailers.
>
> In other words, the type system now guides the API user down the correct
> path.
>
> receiveHead allows extra bytes to be read into the read buffer, and then
> will reuse those bytes for the body or the next request upon connection
> reuse.
>
> respond(), the one-shot function, will send the entire response in one
> syscall.
>
> Streaming response bodies no longer wastefully wraps every call to write
> with a chunk header and trailer; instead it only sends the HTTP chunk
> wrapper when flushing. This means the user can still control when it
> happens but it also does not add unnecessary chunks.
>
> Empirically, in my example project that uses this API, the usage code is
> significantly less noisy, it has less error handling while handling
> errors more correctly, it's more obvious what is happening, and it is
> syscall-optimal.
>
> Additionally:
> * Uncouple std.http.HeadParser from protocol.zig
> * Delete std.Server.Connection; use std.net.Server.Connection instead.
> - The API user supplies the read buffer when initializing the
> http.Server, and it is used for the HTTP head as well as a buffer
> for reading the body into.
> * Replace and document the State enum. No longer is there both "start"
> and "first".
This commit is contained in:
parent
d855b9f703
commit
6ea210259d
@ -12,13 +12,13 @@ pub fn init(request: *jetzig.http.Request) !*Self {
|
||||
}
|
||||
|
||||
pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||
request.server.logger.debug("[middleware] Before request, custom data: {d}", .{self.my_data});
|
||||
request.server.logger.debug("[DemoMiddleware] Before request, custom data: {d}", .{self.my_data});
|
||||
self.my_data = 43;
|
||||
}
|
||||
|
||||
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}", .{response.content_type});
|
||||
request.server.logger.debug("[DemoMiddleware] After request, custom data: {d}", .{self.my_data});
|
||||
request.server.logger.debug("[DemoMiddleware] content-type: {s}", .{response.content_type});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
|
||||
|
@ -18,8 +18,10 @@ pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig
|
||||
}
|
||||
|
||||
pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
_ = data;
|
||||
var root = try data.object();
|
||||
const params = try request.params();
|
||||
try root.put("param", params.get("foo").?);
|
||||
|
||||
std.debug.print("{}\n", .{params});
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ pub const View = views.View;
|
||||
pub const config = struct {
|
||||
pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
|
||||
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 16);
|
||||
pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
|
||||
pub const public_content = .{ .path = "public" };
|
||||
};
|
||||
|
||||
|
@ -18,30 +18,46 @@ pub fn deinit(self: Self) void {
|
||||
/// Starts an application. `routes` should be `@import("routes").routes`, a generated file
|
||||
/// automatically created at build time. `templates` should be
|
||||
/// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time.
|
||||
pub fn start(self: Self, routes: []jetzig.views.Route, templates: []jetzig.TemplateFn) !void {
|
||||
pub fn start(self: Self, comptime_routes: []jetzig.views.Route, templates: []jetzig.TemplateFn) !void {
|
||||
var mime_map = jetzig.http.mime.MimeMap.init(self.allocator);
|
||||
defer mime_map.deinit();
|
||||
try mime_map.build();
|
||||
|
||||
var routes = std.ArrayList(*jetzig.views.Route).init(self.allocator);
|
||||
|
||||
for (comptime_routes) |*comptime_route| {
|
||||
var route = try self.allocator.create(jetzig.views.Route);
|
||||
route.* = jetzig.views.Route{
|
||||
.name = comptime_route.name,
|
||||
.action = comptime_route.action,
|
||||
.uri_path = comptime_route.uri_path,
|
||||
.view = comptime_route.view,
|
||||
.static_view = comptime_route.static_view,
|
||||
.static = comptime_route.static,
|
||||
.render = comptime_route.render,
|
||||
.renderStatic = comptime_route.renderStatic,
|
||||
.template = comptime_route.template,
|
||||
.json_params = comptime_route.json_params,
|
||||
};
|
||||
try route.initParams(self.allocator);
|
||||
try routes.append(route);
|
||||
}
|
||||
defer routes.deinit();
|
||||
defer for (routes.items) |route| {
|
||||
route.deinitParams();
|
||||
self.allocator.destroy(route);
|
||||
};
|
||||
|
||||
var server = jetzig.http.Server.init(
|
||||
self.allocator,
|
||||
self.host,
|
||||
self.port,
|
||||
self.server_options,
|
||||
routes,
|
||||
routes.items,
|
||||
templates,
|
||||
&mime_map,
|
||||
);
|
||||
|
||||
for (routes) |*route| {
|
||||
var mutable = @constCast(route); // FIXME
|
||||
try mutable.initParams(self.allocator);
|
||||
}
|
||||
defer for (routes) |*route| {
|
||||
var mutable = @constCast(route); // FIXME
|
||||
mutable.deinitParams();
|
||||
};
|
||||
|
||||
defer server.deinit();
|
||||
defer self.allocator.free(self.root_path);
|
||||
defer self.allocator.free(self.host);
|
||||
|
@ -1,35 +1,44 @@
|
||||
const std = @import("std");
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
std_headers: std.http.Headers,
|
||||
headers: HeadersArray,
|
||||
|
||||
const Self = @This();
|
||||
pub const max_headers = 25;
|
||||
const HeadersArray = std.ArrayListUnmanaged(std.http.Header);
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, headers: std.http.Headers) Self {
|
||||
return .{ .allocator = allocator, .std_headers = headers };
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.headers = HeadersArray.initCapacity(allocator, max_headers) catch @panic("OOM"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.std_headers.deinit();
|
||||
self.headers.deinit();
|
||||
}
|
||||
|
||||
// Gets the first value for a given header identified by `name`.
|
||||
pub fn getFirstValue(self: *Self, name: []const u8) ?[]const u8 {
|
||||
return self.std_headers.getFirstValue(name);
|
||||
for (self.headers.items) |header| {
|
||||
if (std.mem.eql(u8, header.name, name)) return header.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Appends `name` and `value` to headers.
|
||||
pub fn append(self: *Self, name: []const u8, value: []const u8) !void {
|
||||
try self.std_headers.append(name, value);
|
||||
self.headers.appendAssumeCapacity(.{ .name = name, .value = value });
|
||||
}
|
||||
|
||||
/// Returns an iterator which implements `next()` returning each name/value of the stored headers.
|
||||
pub fn iterator(self: *Self) Iterator {
|
||||
return Iterator{ .std_headers = self.std_headers };
|
||||
return Iterator{ .headers = self.headers };
|
||||
}
|
||||
|
||||
/// Iterates through stored headers yielidng a `Header` on each call to `next()`
|
||||
const Iterator = struct {
|
||||
std_headers: std.http.Headers,
|
||||
headers: HeadersArray,
|
||||
index: usize = 0,
|
||||
|
||||
const Header = struct {
|
||||
@ -39,8 +48,8 @@ const Iterator = struct {
|
||||
|
||||
/// Returns the next item in the current iteration of headers.
|
||||
pub fn next(self: *Iterator) ?Header {
|
||||
if (self.std_headers.list.items.len > self.index) {
|
||||
const std_header = self.std_headers.list.items[self.index];
|
||||
if (self.headers.items.len > self.index) {
|
||||
const std_header = self.headers.items[self.index];
|
||||
self.index += 1;
|
||||
return .{ .name = std_header.name, .value = std_header.value };
|
||||
} else {
|
||||
|
@ -15,22 +15,24 @@ method: Method,
|
||||
headers: jetzig.http.Headers,
|
||||
segments: std.ArrayList([]const u8),
|
||||
server: *jetzig.http.Server,
|
||||
session: *jetzig.http.Session,
|
||||
std_http_request: std.http.Server.Request,
|
||||
response: *jetzig.http.Response,
|
||||
status_code: jetzig.http.status_codes.StatusCode = undefined,
|
||||
response_data: *jetzig.data.Data,
|
||||
query_data: *jetzig.data.Data,
|
||||
query: *jetzig.http.Query,
|
||||
cookies: *jetzig.http.Cookies,
|
||||
body: []const u8,
|
||||
cookies: *jetzig.http.Cookies = undefined,
|
||||
session: *jetzig.http.Session = undefined,
|
||||
body: []const u8 = undefined,
|
||||
processed: bool = false,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
server: *jetzig.http.Server,
|
||||
std_http_request: std.http.Server.Request,
|
||||
response: *jetzig.http.Response,
|
||||
body: []const u8,
|
||||
) !Self {
|
||||
const method = switch (response.std_response.request.method) {
|
||||
const method = switch (std_http_request.head.method) {
|
||||
.DELETE => Method.DELETE,
|
||||
.GET => Method.GET,
|
||||
.PATCH => Method.PATCH,
|
||||
@ -50,7 +52,7 @@ pub fn init(
|
||||
// * Extension: "/foo/bar/baz/1.json" => ".json"
|
||||
// * Query params: "/foo/bar/baz?foo=bar&baz=qux" => .{ .foo = "bar", .baz => "qux" }
|
||||
// * Anything else ?
|
||||
var it = std.mem.splitScalar(u8, response.std_response.request.target, '/');
|
||||
var it = std.mem.splitScalar(u8, std_http_request.head.target, '/');
|
||||
var segments = std.ArrayList([]const u8).init(allocator);
|
||||
while (it.next()) |segment| {
|
||||
if (std.mem.indexOfScalar(u8, segment, '?')) |query_index| {
|
||||
@ -60,25 +62,6 @@ pub fn init(
|
||||
}
|
||||
}
|
||||
|
||||
var cookies = try allocator.create(jetzig.http.Cookies);
|
||||
cookies.* = jetzig.http.Cookies.init(
|
||||
allocator,
|
||||
response.std_response.request.headers.getFirstValue("Cookie") orelse "",
|
||||
);
|
||||
try cookies.parse();
|
||||
|
||||
var session = try allocator.create(jetzig.http.Session);
|
||||
session.* = jetzig.http.Session.init(allocator, cookies, server.options.secret);
|
||||
session.parse() catch |err| {
|
||||
switch (err) {
|
||||
error.JetzigInvalidSessionCookie => {
|
||||
server.logger.debug("Invalid session cookie detected. Resetting session.", .{});
|
||||
try session.reset();
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
const response_data = try allocator.create(jetzig.data.Data);
|
||||
response_data.* = jetzig.data.Data.init(allocator);
|
||||
|
||||
@ -89,26 +72,82 @@ pub fn init(
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.path = response.std_response.request.target,
|
||||
.path = std_http_request.head.target,
|
||||
.method = method,
|
||||
.headers = jetzig.http.Headers.init(allocator, response.std_response.request.headers),
|
||||
.headers = jetzig.http.Headers.init(allocator),
|
||||
.server = server,
|
||||
.segments = segments,
|
||||
.cookies = cookies,
|
||||
.session = session,
|
||||
.response = response,
|
||||
.response_data = response_data,
|
||||
.query_data = query_data,
|
||||
.query = query,
|
||||
.body = body,
|
||||
.response = response,
|
||||
.std_http_request = std_http_request,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.session.deinit();
|
||||
// self.session.deinit();
|
||||
self.segments.deinit();
|
||||
self.allocator.destroy(self.cookies);
|
||||
self.allocator.destroy(self.session);
|
||||
if (self.processed) self.allocator.free(self.body);
|
||||
}
|
||||
|
||||
/// Process request, read body if present, parse headers (TODO)
|
||||
pub fn process(self: *Self) !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;
|
||||
}
|
||||
|
||||
self.cookies = try self.allocator.create(jetzig.http.Cookies);
|
||||
self.cookies.* = jetzig.http.Cookies.init(
|
||||
self.allocator,
|
||||
cookie orelse "",
|
||||
);
|
||||
try self.cookies.parse();
|
||||
|
||||
self.session = try self.allocator.create(jetzig.http.Session);
|
||||
self.session.* = jetzig.http.Session.init(self.allocator, self.cookies, self.server.options.secret);
|
||||
self.session.parse() catch |err| {
|
||||
switch (err) {
|
||||
error.JetzigInvalidSessionCookie => {
|
||||
self.server.logger.debug("Invalid session cookie detected. Resetting session.", .{});
|
||||
try self.session.reset();
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
const reader = try self.std_http_request.reader();
|
||||
self.body = try reader.readAllAlloc(self.allocator, jetzig.config.max_bytes_request_body);
|
||||
self.processed = true;
|
||||
}
|
||||
|
||||
/// Set response headers, write response payload, and finalize the response.
|
||||
pub fn respond(self: *Self) !void {
|
||||
if (!self.processed) unreachable;
|
||||
|
||||
var cookie_it = self.cookies.headerIterator();
|
||||
while (try cookie_it.next()) |header| {
|
||||
// FIXME: Skip setting cookies that are already present ?
|
||||
try self.response.headers.append("Set-Cookie", header);
|
||||
}
|
||||
|
||||
// TODO: Move to jetzig.http.Response.stdHeaders()
|
||||
var std_response_headers = std.ArrayList(std.http.Header).init(self.allocator);
|
||||
var headers_it = self.response.headers.iterator();
|
||||
while (headers_it.next()) |header| try std_response_headers.append(
|
||||
.{ .name = header.name, .value = header.value },
|
||||
);
|
||||
|
||||
try self.std_http_request.respond(
|
||||
self.response.content,
|
||||
.{ .keep_alive = false, .extra_headers = std_response_headers.items },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn render(self: *Self, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
|
||||
@ -129,6 +168,8 @@ pub fn getHeader(self: *Self, key: []const u8) ?[]const u8 {
|
||||
/// otherwise the parsed JSON request body will take precedence and query parameters will be
|
||||
/// ignored.
|
||||
pub fn params(self: *Self) !*jetzig.data.Value {
|
||||
if (!self.processed) unreachable;
|
||||
|
||||
switch (self.requestFormat()) {
|
||||
.JSON => {
|
||||
if (self.body.len == 0) return self.queryParams();
|
||||
@ -137,7 +178,7 @@ pub fn params(self: *Self) !*jetzig.data.Value {
|
||||
data.* = jetzig.data.Data.init(self.allocator);
|
||||
data.fromJson(self.body) catch |err| {
|
||||
switch (err) {
|
||||
error.UnexpectedEndOfInput => return error.JetzigBodyParseError,
|
||||
error.SyntaxError, error.UnexpectedEndOfInput => return error.JetzigBodyParseError,
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
@ -253,22 +294,30 @@ pub fn resourceId(self: *Self) []const u8 {
|
||||
|
||||
// Determine if a given route matches the current request.
|
||||
pub fn match(self: *Self, route: jetzig.views.Route) !bool {
|
||||
switch (self.method) {
|
||||
.GET => {
|
||||
return switch (route.action) {
|
||||
.index => self.isMatch(.exact, route),
|
||||
.get => self.isMatch(.resource_id, route),
|
||||
else => false,
|
||||
};
|
||||
return switch (self.method) {
|
||||
.GET => switch (route.action) {
|
||||
.index => self.isMatch(.exact, route),
|
||||
.get => self.isMatch(.resource_id, route),
|
||||
else => false,
|
||||
},
|
||||
.POST => return self.isMatch(.exact, route),
|
||||
.PUT => return self.isMatch(.resource_id, route),
|
||||
.PATCH => return self.isMatch(.resource_id, route),
|
||||
.DELETE => return self.isMatch(.resource_id, route),
|
||||
else => return false,
|
||||
}
|
||||
|
||||
return false;
|
||||
.POST => switch (route.action) {
|
||||
.post => self.isMatch(.exact, route),
|
||||
else => false,
|
||||
},
|
||||
.PUT => switch (route.action) {
|
||||
.put => self.isMatch(.resource_id, route),
|
||||
else => false,
|
||||
},
|
||||
.PATCH => switch (route.action) {
|
||||
.patch => self.isMatch(.resource_id, route),
|
||||
else => false,
|
||||
},
|
||||
.DELETE => switch (route.action) {
|
||||
.delete => self.isMatch(.resource_id, route),
|
||||
else => false,
|
||||
},
|
||||
.HEAD, .CONNECT, .OPTIONS, .TRACE => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn isMatch(self: *Self, match_type: enum { exact, resource_id }, route: jetzig.views.Route) bool {
|
||||
|
@ -5,7 +5,6 @@ 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,
|
||||
@ -13,14 +12,12 @@ content_type: []const u8,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
std_response: *std.http.Server.Response,
|
||||
) !Self {
|
||||
const headers = try allocator.create(jetzig.http.Headers);
|
||||
headers.* = jetzig.http.Headers.init(allocator, std_response.headers);
|
||||
headers.* = jetzig.http.Headers.init(allocator);
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.std_response = std_response,
|
||||
.status_code = .no_content,
|
||||
.content_type = "application/octet-stream",
|
||||
.content = "",
|
||||
@ -33,53 +30,3 @@ pub fn deinit(self: *const Self) void {
|
||||
self.allocator.destroy(self.headers);
|
||||
self.std_response.deinit();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// Finalizes a request. Appends any stored headers, sets the response status code, and writes
|
||||
/// the response body.
|
||||
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)),
|
||||
};
|
||||
|
||||
var it = self.headers.iterator();
|
||||
while (it.next()) |header| {
|
||||
try self.std_response.headers.append(header.name, header.value);
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ pub const ServerOptions = struct {
|
||||
secret: []const u8,
|
||||
};
|
||||
|
||||
server: std.http.Server,
|
||||
allocator: std.mem.Allocator,
|
||||
port: u16,
|
||||
host: []const u8,
|
||||
@ -24,9 +23,10 @@ cache: jetzig.caches.Cache,
|
||||
logger: jetzig.loggers.Logger,
|
||||
options: ServerOptions,
|
||||
start_time: i128 = undefined,
|
||||
routes: []jetzig.views.Route,
|
||||
routes: []*jetzig.views.Route,
|
||||
templates: []jetzig.TemplateFn,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
std_net_server: std.net.Server = undefined,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
@ -35,14 +35,11 @@ pub fn init(
|
||||
host: []const u8,
|
||||
port: u16,
|
||||
options: ServerOptions,
|
||||
routes: []jetzig.views.Route,
|
||||
routes: []*jetzig.views.Route,
|
||||
templates: []jetzig.TemplateFn,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
) Self {
|
||||
const server = std.http.Server.init(.{ .reuse_address = true });
|
||||
|
||||
return .{
|
||||
.server = server,
|
||||
.allocator = allocator,
|
||||
.host = host,
|
||||
.port = port,
|
||||
@ -56,109 +53,93 @@ pub fn init(
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.server.deinit();
|
||||
self.std_net_server.deinit();
|
||||
}
|
||||
|
||||
pub fn listen(self: *Self) !void {
|
||||
const address = std.net.Address.parseIp(self.host, self.port) catch unreachable;
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 8080);
|
||||
self.std_net_server = try address.listen(.{ .reuse_port = true });
|
||||
|
||||
try self.server.listen(address);
|
||||
const cache_status = if (self.options.cache == .null_cache) "disabled" else "enabled";
|
||||
self.logger.debug("Listening on http://{s}:{} [cache:{s}]", .{ self.host, self.port, cache_status });
|
||||
try self.processRequests();
|
||||
}
|
||||
|
||||
fn processRequests(self: *Self) !void {
|
||||
// TODO: Keepalive
|
||||
while (true) {
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
errdefer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
var std_response = try self.server.accept(.{ .allocator = allocator });
|
||||
const connection = try self.std_net_server.accept();
|
||||
|
||||
var response = try jetzig.http.Response.init(
|
||||
allocator,
|
||||
&std_response,
|
||||
);
|
||||
errdefer response.deinit();
|
||||
errdefer arena.deinit();
|
||||
var buf: [jetzig.config.http_buffer_size]u8 = undefined;
|
||||
var std_http_server = std.http.Server.init(connection, &buf);
|
||||
errdefer std_http_server.connection.stream.close();
|
||||
|
||||
try response.headers.append("Connection", "close");
|
||||
self.processNextRequest(allocator, &std_http_server) catch |err| {
|
||||
if (isBadHttpError(err)) {
|
||||
std.debug.print("Encountered HTTP error: {s}\n", .{@errorName(err)});
|
||||
std_http_server.connection.stream.close();
|
||||
continue;
|
||||
} else return err;
|
||||
};
|
||||
|
||||
while (response.reset() != .closing) {
|
||||
self.processNextRequest(allocator, &response) catch |err| {
|
||||
switch (err) {
|
||||
error.EndOfStream, error.ConnectionResetByPeer => continue,
|
||||
error.UnknownHttpMethod => continue, // TODO: Render 400 Bad Request here ?
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
response.deinit();
|
||||
std_http_server.connection.stream.close();
|
||||
arena.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
fn processNextRequest(self: *Self, allocator: std.mem.Allocator, response: *jetzig.http.Response) !void {
|
||||
try response.wait();
|
||||
|
||||
fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void {
|
||||
self.start_time = std.time.nanoTimestamp();
|
||||
|
||||
const body = try response.read();
|
||||
defer self.allocator.free(body);
|
||||
const std_http_request = try std_http_server.receiveHead();
|
||||
if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError;
|
||||
|
||||
var request = try jetzig.http.Request.init(allocator, self, response, body);
|
||||
defer request.deinit();
|
||||
var response = try jetzig.http.Response.init(allocator);
|
||||
var request = try jetzig.http.Request.init(allocator, self, std_http_request, &response);
|
||||
|
||||
try request.process();
|
||||
|
||||
var middleware_data = try jetzig.http.middleware.beforeMiddleware(&request);
|
||||
|
||||
try self.renderResponse(&request, response);
|
||||
try self.renderResponse(&request);
|
||||
try request.response.headers.append("content-type", response.content_type);
|
||||
try request.respond();
|
||||
|
||||
try jetzig.http.middleware.afterMiddleware(&middleware_data, &request, response);
|
||||
try jetzig.http.middleware.afterMiddleware(&middleware_data, &request);
|
||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||
|
||||
response.setTransferEncoding(.{ .content_length = response.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", response.content_type);
|
||||
|
||||
try response.finish();
|
||||
|
||||
const log_message = try self.requestLogMessage(&request, response);
|
||||
const log_message = try self.requestLogMessage(&request);
|
||||
defer self.allocator.free(log_message);
|
||||
self.logger.debug("{s}", .{log_message});
|
||||
|
||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||
}
|
||||
|
||||
fn renderResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
const static = self.matchStaticResource(request) catch |err| {
|
||||
fn renderResponse(self: *Self, request: *jetzig.http.Request) !void {
|
||||
const static_resource = self.matchStaticResource(request) catch |err| {
|
||||
if (isUnhandledError(err)) return err;
|
||||
|
||||
const rendered = try self.renderInternalServerError(request, err);
|
||||
|
||||
response.content = rendered.content;
|
||||
response.status_code = .internal_server_error;
|
||||
response.content_type = "text/html";
|
||||
request.response.content = rendered.content;
|
||||
request.response.status_code = .internal_server_error;
|
||||
request.response.content_type = "text/html";
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
if (static) |resource| {
|
||||
try renderStatic(resource, response);
|
||||
if (static_resource) |resource| {
|
||||
try renderStatic(resource, request.response);
|
||||
return;
|
||||
}
|
||||
|
||||
const route = try self.matchRoute(request, false);
|
||||
|
||||
switch (request.requestFormat()) {
|
||||
.HTML => try self.renderHTML(request, response, route),
|
||||
.JSON => try self.renderJSON(request, response, route),
|
||||
.UNKNOWN => try self.renderHTML(request, response, route),
|
||||
.HTML => try self.renderHTML(request, route),
|
||||
.JSON => try self.renderJSON(request, route),
|
||||
.UNKNOWN => try self.renderHTML(request, route),
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,32 +152,30 @@ fn renderStatic(resource: StaticResource, response: *jetzig.http.Response) !void
|
||||
fn renderHTML(
|
||||
self: *Self,
|
||||
request: *jetzig.http.Request,
|
||||
response: *jetzig.http.Response,
|
||||
route: ?jetzig.views.Route,
|
||||
route: ?*jetzig.views.Route,
|
||||
) !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);
|
||||
response.content = rendered.content;
|
||||
response.status_code = rendered.view.status_code;
|
||||
response.content_type = "text/html";
|
||||
request.response.content = rendered.content;
|
||||
request.response.status_code = rendered.view.status_code;
|
||||
request.response.content_type = "text/html";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.content = "";
|
||||
response.status_code = .not_found;
|
||||
response.content_type = "text/html";
|
||||
request.response.content = "";
|
||||
request.response.status_code = .not_found;
|
||||
request.response.content_type = "text/html";
|
||||
}
|
||||
|
||||
fn renderJSON(
|
||||
self: *Self,
|
||||
request: *jetzig.http.Request,
|
||||
response: *jetzig.http.Response,
|
||||
route: ?jetzig.views.Route,
|
||||
route: ?*jetzig.views.Route,
|
||||
) !void {
|
||||
if (route) |matched_route| {
|
||||
const rendered = try self.renderView(matched_route, request, null);
|
||||
@ -205,13 +184,13 @@ fn renderJSON(
|
||||
if (data.value) |_| {} else _ = try data.object();
|
||||
try request.headers.append("Content-Type", "application/json");
|
||||
|
||||
response.content = try data.toJson();
|
||||
response.status_code = rendered.view.status_code;
|
||||
response.content_type = "application/json";
|
||||
request.response.content = try data.toJson();
|
||||
request.response.status_code = rendered.view.status_code;
|
||||
request.response.content_type = "application/json";
|
||||
} else {
|
||||
response.content = "";
|
||||
response.status_code = .not_found;
|
||||
response.content_type = "application/json";
|
||||
request.response.content = "";
|
||||
request.response.status_code = .not_found;
|
||||
request.response.content_type = "application/json";
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,11 +198,11 @@ const RenderedView = struct { view: jetzig.views.View, content: []const u8 };
|
||||
|
||||
fn renderView(
|
||||
self: *Self,
|
||||
route: jetzig.views.Route,
|
||||
route: *jetzig.views.Route,
|
||||
request: *jetzig.http.Request,
|
||||
template: ?jetzig.TemplateFn,
|
||||
) !RenderedView {
|
||||
const view = route.render(route, request) catch |err| {
|
||||
const view = route.render(route.*, request) catch |err| {
|
||||
self.logger.debug("Encountered error: {s}", .{@errorName(err)});
|
||||
if (isUnhandledError(err)) return err;
|
||||
if (isBadRequest(err)) return try self.renderBadRequest(request);
|
||||
@ -248,6 +227,24 @@ fn isUnhandledError(err: anyerror) bool {
|
||||
};
|
||||
}
|
||||
|
||||
fn isBadHttpError(err: anyerror) bool {
|
||||
return switch (err) {
|
||||
error.JetzigParseHeadError,
|
||||
error.UnknownHttpMethod,
|
||||
error.HttpHeadersInvalid,
|
||||
error.HttpHeaderContinuationsUnsupported,
|
||||
error.HttpTransferEncodingUnsupported,
|
||||
error.HttpConnectionHeaderUnsupported,
|
||||
error.InvalidContentLength,
|
||||
error.CompressionUnsupported,
|
||||
error.MissingFinalNewline,
|
||||
error.HttpConnectionClosing,
|
||||
error.ConnectionResetByPeer,
|
||||
=> true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn renderInternalServerError(self: *Self, request: *jetzig.http.Request, err: anyerror) !RenderedView {
|
||||
request.response_data.reset();
|
||||
|
||||
@ -292,8 +289,8 @@ fn logStackTrace(
|
||||
try object.put("backtrace", request.response_data.string(array.items));
|
||||
}
|
||||
|
||||
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) {
|
||||
fn requestLogMessage(self: *Self, request: *jetzig.http.Request) ![]const u8 {
|
||||
const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) {
|
||||
inline else => |status_code| @unionInit(
|
||||
jetzig.http.status_codes.TaggedStatusCode,
|
||||
@tagName(status_code),
|
||||
@ -316,14 +313,14 @@ fn duration(self: *Self) i64 {
|
||||
return @intCast(std.time.nanoTimestamp() - self.start_time);
|
||||
}
|
||||
|
||||
fn matchRoute(self: *Self, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
|
||||
fn matchRoute(self: *Self, request: *jetzig.http.Request, static: bool) !?*jetzig.views.Route {
|
||||
for (self.routes) |route| {
|
||||
// .index routes always take precedence.
|
||||
if (route.static == static and route.action == .index and try request.match(route)) return route;
|
||||
if (route.static == static and route.action == .index and try request.match(route.*)) return route;
|
||||
}
|
||||
|
||||
for (self.routes) |route| {
|
||||
if (route.static == static and try request.match(route)) return route;
|
||||
if (route.static == static and try request.match(route.*)) return route;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -398,7 +395,7 @@ fn matchStaticContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
|
||||
const matched_route = try self.matchRoute(request, true);
|
||||
|
||||
if (matched_route) |route| {
|
||||
const static_path = try staticPath(request, route);
|
||||
const static_path = try staticPath(request, route.*);
|
||||
|
||||
if (static_path) |capture| {
|
||||
return static_dir.readFileAlloc(
|
||||
|
@ -40,7 +40,6 @@ pub fn beforeMiddleware(request: *jetzig.http.Request) !MiddlewareData {
|
||||
pub fn afterMiddleware(
|
||||
middleware_data: *MiddlewareData,
|
||||
request: *jetzig.http.Request,
|
||||
response: *jetzig.http.Response,
|
||||
) !void {
|
||||
inline for (middlewares, 0..) |middleware, index| {
|
||||
if (comptime !@hasDecl(middleware, "afterRequest")) continue;
|
||||
@ -49,10 +48,10 @@ pub fn afterMiddleware(
|
||||
try @call(
|
||||
.always_inline,
|
||||
middleware.afterRequest,
|
||||
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, response },
|
||||
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, request.response },
|
||||
);
|
||||
} else {
|
||||
try @call(.always_inline, middleware.afterRequest, .{ request, response });
|
||||
try @call(.always_inline, middleware.afterRequest, .{ request, request.response });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,10 @@ pub fn initParams(self: *Self, allocator: std.mem.Allocator) !void {
|
||||
}
|
||||
|
||||
pub fn deinitParams(self: *const Self) void {
|
||||
for (self.params.items) |data| data.deinit();
|
||||
for (self.params.items) |data| {
|
||||
data.deinit();
|
||||
data._allocator.destroy(data);
|
||||
}
|
||||
self.params.deinit();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user