diff --git a/build.zig b/build.zig
index e020e26..ad14efa 100644
--- a/build.zig
+++ b/build.zig
@@ -52,6 +52,7 @@ pub fn build(b: *std.Build) !void {
}{
.{ .name = "app_basic", .src = "examples/app/basic.zig" },
.{ .name = "app_auth", .src = "examples/app/auth.zig" },
+ .{ .name = "app_errors", .src = "examples/app/errors.zig" },
.{ .name = "hello", .src = "examples/hello/hello.zig" },
.{ .name = "https", .src = "examples/https/https.zig" },
.{ .name = "hello2", .src = "examples/hello2/hello2.zig" },
diff --git a/examples/app/errors.zig b/examples/app/errors.zig
new file mode 100644
index 0000000..0bd3b83
--- /dev/null
+++ b/examples/app/errors.zig
@@ -0,0 +1,124 @@
+//!
+//! Part of the Zap examples.
+//!
+//! Build me with `zig build app_errors`.
+//! Run me with `zig build run-app_errors`.
+//!
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const zap = @import("zap");
+
+// The global Application Context
+const MyContext = struct {
+ db_connection: []const u8,
+
+ // we don't use this
+ pub fn unhandledRequest(_: *MyContext, _: Allocator, _: zap.Request) anyerror!void {}
+
+ pub fn unhandledError(_: *MyContext, _: zap.Request, err: anyerror) void {
+ std.debug.print("\n\n\nUNHANDLED ERROR: {} !!! \n\n\n", .{err});
+ }
+};
+
+// A very simple endpoint handling only GET requests
+const ErrorEndpoint = struct {
+
+ // zap.App.Endpoint Interface part
+ path: []const u8,
+ error_strategy: zap.Endpoint.ErrorStrategy = .raise,
+
+ // data specific for this endpoint
+ some_data: []const u8,
+
+ pub fn init(path: []const u8, data: []const u8) ErrorEndpoint {
+ return .{
+ .path = path,
+ .some_data = data,
+ };
+ }
+
+ // handle GET requests
+ pub fn get(_: *ErrorEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {
+
+ // we just return an error
+ // our error_strategy = .raise
+ // -> error will be raised and dispatched to MyContext.unhandledError
+ return error.@"Oh-No!";
+ }
+
+ // empty stubs for all other request methods
+ pub fn post(_: *ErrorEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+ pub fn put(_: *ErrorEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+ pub fn delete(_: *ErrorEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+ pub fn patch(_: *ErrorEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+ pub fn options(_: *ErrorEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+};
+
+const StopEndpoint = struct {
+ path: []const u8,
+ error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
+
+ pub fn get(_: *StopEndpoint, _: Allocator, context: *MyContext, _: zap.Request) !void {
+ std.debug.print(
+ \\Before I stop, let me dump the app context:
+ \\db_connection='{s}'
+ \\
+ \\
+ , .{context.*.db_connection});
+ zap.stop();
+ }
+
+ pub fn post(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+ pub fn put(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+ pub fn delete(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+ pub fn patch(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+ pub fn options(_: *StopEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
+};
+
+pub fn main() !void {
+ // setup allocations
+ var gpa: std.heap.GeneralPurposeAllocator(.{
+ // just to be explicit
+ .thread_safe = true,
+ }) = .{};
+ defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok});
+ const allocator = gpa.allocator();
+
+ // create an app context
+ var my_context: MyContext = .{ .db_connection = "db connection established!" };
+
+ // create an App instance
+ const App = zap.App.Create(MyContext);
+ var app = try App.init(allocator, &my_context, .{});
+ defer app.deinit();
+
+ // create the endpoints
+ var my_endpoint = ErrorEndpoint.init("/error", "some endpoint specific data");
+ var stop_endpoint: StopEndpoint = .{ .path = "/stop" };
+ //
+ // register the endpoints with the app
+ try app.register(&my_endpoint);
+ try app.register(&stop_endpoint);
+
+ // listen on the network
+ try app.listen(.{
+ .interface = "0.0.0.0",
+ .port = 3000,
+ });
+ std.debug.print("Listening on 0.0.0.0:3000\n", .{});
+
+ std.debug.print(
+ \\ Try me via:
+ \\ curl http://localhost:3000/error
+ \\ Stop me via:
+ \\ curl http://localhost:3000/stop
+ \\
+ , .{});
+
+ // start worker threads -- only 1 process!!!
+ zap.start(.{
+ .threads = 2,
+ .workers = 1,
+ });
+}
diff --git a/examples/endpoint/error.zig b/examples/endpoint/error.zig
index 178be67..2dafa45 100644
--- a/examples/endpoint/error.zig
+++ b/examples/endpoint/error.zig
@@ -9,6 +9,8 @@ path: []const u8 = "/error",
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
pub fn get(_: *ErrorEndpoint, _: zap.Request) !void {
+ // error_strategy is set to .log_to_response
+ // --> this error will be shown in the browser, with a nice error trace
return error.@"Oh-no!";
}
diff --git a/examples/endpoint/main.zig b/examples/endpoint/main.zig
index 1f30a61..787fd18 100644
--- a/examples/endpoint/main.zig
+++ b/examples/endpoint/main.zig
@@ -19,6 +19,11 @@ fn on_request(r: zap.Request) !void {
try r.sendBody("
Hello from ZAP!!!
");
}
+// this is just to demo that we could catch arbitrary errors as fallback
+fn on_error(_: zap.Request, err: anyerror) void {
+ std.debug.print("\n\n\nOh no!!! We didn't chatch this error: {}\n\n\n", .{err});
+}
+
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{
.thread_safe = true,
@@ -33,6 +38,8 @@ pub fn main() !void {
.{
.port = 3000,
.on_request = on_request,
+ // optional
+ .on_error = on_error,
.log = true,
.public_folder = "examples/endpoint/html",
.max_clients = 100000,
@@ -47,11 +54,13 @@ pub fn main() !void {
var stopEp = StopEndpoint.init("/stop");
var errorEp: ErrorEndpoint = .{};
+ var unhandledErrorEp: ErrorEndpoint = .{ .error_strategy = .raise, .path = "/unhandled" };
// register endpoints with the listener
try listener.register(&userWeb);
try listener.register(&stopEp);
try listener.register(&errorEp);
+ try listener.register(&unhandledErrorEp);
// fake some users
var uid: usize = undefined;
diff --git a/src/App.zig b/src/App.zig
index cbc3f44..5068e42 100644
--- a/src/App.zig
+++ b/src/App.zig
@@ -29,6 +29,16 @@ pub const AppOpts = struct {
};
/// creates an App with custom app context
+///
+/// About App Contexts:
+///
+/// ```zig
+/// const MyContext = struct {
+/// // You may (optionally) define the following global handlers:
+/// pub fn unhandledRequest(_: *MyContext, _: Allocator, _: Request) anyerror!void {}
+/// pub fn unhandledError(_: *MyContext, _: Request, _: anyerror) void {}
+/// };
+/// ```
pub fn Create(
/// Your user-defined "Global App Context" type
comptime Context: type,
@@ -51,19 +61,21 @@ pub fn Create(
/// the internal http listener
listener: HttpListener = undefined,
- /// function pointer to handler for otherwise unhandled requests
- /// Will automatically be set if your Context provides an unhandled
- /// function of type `fn(*Context, Allocator, Request)`
- ///
- unhandled: ?*const fn (*Context, Allocator, Request) anyerror!void = null,
+ /// function pointer to handler for otherwise unhandled requests.
+ /// Will automatically be set if your Context provides an
+ /// `unhandledRequest` function of type `fn(*Context, Allocator,
+ /// Request) !void`.
+ unhandled_request: ?*const fn (*Context, Allocator, Request) anyerror!void = null,
+
+ /// function pointer to handler for unhandled errors.
+ /// Errors are unhandled if they are not logged but raised by the
+ /// ErrorStrategy. Will automatically be set if your Context
+ /// provides an `unhandledError` function of type `fn(*Context,
+ /// Allocator, Request, anyerror) void`.
+ unhandled_error: ?*const fn (*Context, Request, anyerror) void = null,
};
var _static: InstanceData = .{};
- /// Internal, static request handler callback. Will be set to the optional,
- /// user-defined request callback that only gets called if no endpoints match
- /// a request.
- var on_request: ?*const fn (Allocator, *Context, Request) anyerror!void = null;
-
pub const Endpoint = struct {
pub const Interface = struct {
call: *const fn (*Interface, Request) anyerror!void = undefined,
@@ -113,7 +125,7 @@ pub fn Create(
switch (self.endpoint.*.error_strategy) {
.raise => return err,
.log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505),
- .log_to_console => zap.debug(
+ .log_to_console => zap.log.err(
"Error in {} {s} : {}",
.{ Bound, r.method orelse "(no method)", err },
),
@@ -318,15 +330,24 @@ pub fn Create(
_static.opts = opts_;
_static.there_can_be_only_one = true;
- // set unhandled callback if provided by Context
- if (@hasDecl(Context, "unhandled")) {
+ // set unhandled_request callback if provided by Context
+ if (@hasDecl(Context, "unhandledRequest")) {
// try if we can use it
- const Unhandled = @TypeOf(@field(Context, "unhandled"));
+ const Unhandled = @TypeOf(@field(Context, "unhandledRequest"));
const Expected = fn (_: *Context, _: Allocator, _: Request) anyerror!void;
if (Unhandled != Expected) {
- @compileError("`unhandled` method of " ++ @typeName(Context) ++ " has wrong type:\n" ++ @typeName(Unhandled) ++ "\nexpected:\n" ++ @typeName(Expected));
+ @compileError("`unhandledRequest` method of " ++ @typeName(Context) ++ " has wrong type:\n" ++ @typeName(Unhandled) ++ "\nexpected:\n" ++ @typeName(Expected));
}
- _static.unhandled = Context.unhandled;
+ _static.unhandled_request = Context.unhandledRequest;
+ }
+ if (@hasDecl(Context, "unhandledError")) {
+ // try if we can use it
+ const Unhandled = @TypeOf(@field(Context, "unhandledError"));
+ const Expected = fn (_: *Context, _: Request, _: anyerror) void;
+ if (Unhandled != Expected) {
+ @compileError("`unhandledError` method of " ++ @typeName(Context) ++ " has wrong type:\n" ++ @typeName(Unhandled) ++ "\nexpected:\n" ++ @typeName(Expected));
+ }
+ _static.unhandled_error = Context.unhandledError;
}
return .{};
}
@@ -419,17 +440,34 @@ pub fn Create(
if (r.path) |p| {
for (_static.endpoints.items) |interface| {
if (std.mem.startsWith(u8, p, interface.path)) {
- return try interface.call(interface, r);
+ return interface.call(interface, r) catch |err| {
+ // if error is not dealt with in the interface, e.g.
+ // if error strategy is .raise:
+ if (_static.unhandled_error) |error_cb| {
+ error_cb(_static.context, r, err);
+ } else {
+ zap.log.err(
+ "App.Endpoint onRequest error {} in endpoint interface {}\n",
+ .{ err, interface },
+ );
+ }
+ };
}
}
}
- if (on_request) |foo| {
+
+ // this is basically the "not found" handler
+ if (_static.unhandled_request) |foo| {
var arena = try get_arena();
- foo(arena.allocator(), _static.context, r) catch |err| {
+ foo(_static.context, arena.allocator(), r) catch |err| {
switch (_static.opts.default_error_strategy) {
- .raise => return err,
+ .raise => if (_static.unhandled_error) |error_cb| {
+ error_cb(_static.context, r, err);
+ } else {
+ zap.Logging.on_uncaught_error("App on_request", err);
+ },
.log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505),
- .log_to_console => zap.debug("Error in {} {s} : {}", .{ App, r.method orelse "(no method)", err }),
+ .log_to_console => zap.log.err("Error in {} {s} : {}", .{ App, r.method orelse "(no method)", err }),
}
};
}
diff --git a/src/endpoint.zig b/src/endpoint.zig
index 7b8169d..da38a3e 100644
--- a/src/endpoint.zig
+++ b/src/endpoint.zig
@@ -313,6 +313,33 @@ pub const Listener = struct {
listener: HttpListener,
allocator: std.mem.Allocator,
+ pub const Settings = struct {
+ port: usize,
+ interface: [*c]const u8 = null,
+
+ /// User-defined request callback that only gets called if no endpoints
+ /// match a request.
+ on_request: ?zap.HttpRequestFn,
+ on_response: ?zap.HttpRequestFn = null,
+ on_upgrade: ?zap.HttpUpgradeFn = null,
+ on_finish: ?zap.HttpFinishFn = null,
+
+ /// Callback, called if an error is raised and not caught by the
+ /// ErrorStrategy
+ on_error: ?*const fn (Request, anyerror) void = null,
+
+ // provide any pointer in there for "user data". it will be passed pack in
+ // on_finish()'s copy of the struct_http_settings_s
+ udata: ?*anyopaque = null,
+ public_folder: ?[]const u8 = null,
+ max_clients: ?isize = null,
+ max_body_size: ?usize = null,
+ timeout: ?u8 = null,
+ log: bool = false,
+ ws_timeout: u8 = 40,
+ ws_max_msg_size: usize = 262144,
+ tls: ?zap.Tls = null,
+ };
/// Internal static interface struct of member endpoints
var endpoints: std.ArrayListUnmanaged(*Binder.Interface) = .empty;
@@ -321,23 +348,46 @@ pub const Listener = struct {
/// a request.
var on_request: ?zap.HttpRequestFn = null;
+ /// Callback, called if an error is raised and not caught by the ErrorStrategy
+ var on_error: ?*const fn (Request, anyerror) void = null;
+
/// Initialize a new endpoint listener. Note, if you pass an `on_request`
/// callback in the provided ListenerSettings, this request callback will be
/// called every time a request arrives that no endpoint matches.
- pub fn init(a: std.mem.Allocator, l: ListenerSettings) Listener {
+ pub fn init(a: std.mem.Allocator, settings: Settings) Listener {
// reset the global in case init is called multiple times, as is the
// case in the authentication tests
endpoints = .empty;
- // take copy of listener settings before modifying the callback field
- var ls = l;
+ var ls: zap.HttpListenerSettings = .{
+ .port = settings.port,
+ .interface = settings.interface,
+
+ // we set to our own handler
+ .on_request = onRequest,
+
+ .on_response = settings.on_response,
+ .on_upgrade = settings.on_upgrade,
+ .on_finish = settings.on_finish,
+ .udata = settings.udata,
+ .public_folder = settings.public_folder,
+ .max_clients = settings.max_clients,
+ .max_body_size = settings.max_body_size,
+ .timeout = settings.timeout,
+ .log = settings.log,
+ .ws_timeout = settings.ws_timeout,
+ .ws_max_msg_size = settings.ws_max_msg_size,
+ .tls = settings.tls,
+ };
// override the settings with our internal, actual callback function
// so that "we" will be called on request
ls.on_request = Listener.onRequest;
- // store the settings-provided request callback for later use
- on_request = l.on_request;
+ // store the settings-provided request callbacks for later use
+ on_request = settings.on_request;
+ on_error = settings.on_error;
+
return .{
.listener = HttpListener.init(ls),
.allocator = a,
@@ -390,10 +440,16 @@ pub const Listener = struct {
for (endpoints.items) |interface| {
if (std.mem.startsWith(u8, p, interface.path)) {
return interface.call(interface, r) catch |err| {
- zap.log.err(
- "Endpoint onRequest error {} in endpoint interface {}\n",
- .{ err, interface },
- );
+ // if error is not dealt with in the entpoint, e.g.
+ // if error strategy is .raise:
+ if (on_error) |error_cb| {
+ error_cb(r, err);
+ } else {
+ zap.log.err(
+ "Endpoint onRequest error {} in endpoint interface {}\n",
+ .{ err, interface },
+ );
+ }
};
}
}
@@ -401,7 +457,13 @@ pub const Listener = struct {
// if set, call the user-provided default callback
if (on_request) |foo| {
foo(r) catch |err| {
- zap.Logging.on_uncaught_error("Endpoint on_request", err);
+ // if error is not dealt with in the entpoint, e.g.
+ // if error strategy is .raise:
+ if (on_error) |error_cb| {
+ error_cb(r, err);
+ } else {
+ zap.Logging.on_uncaught_error("Endpoint on_request", err);
+ }
};
}
}