1
0
Fork 0
mirror of https://github.com/zigzap/zap.git synced 2025-10-20 15:14:08 +00:00

add on_uncaught_error behavior to zap.App, add on_error callbacks

zap.Endpont.Listener and zap.App now support on_error callbacks:

zap.Endpont.Listener.Settings contains an `on_error` optional callback
field.

zap.App supports those two callbacks:


/// ```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 {}
/// };
/// ```

The `endpoint` example has been updated to showcase `on_error`, and the
new example `app_errors` showcases `Context.unhandledError`.
This commit is contained in:
renerocksai 2025-04-02 08:26:09 +02:00
parent 4591f4048b
commit 8078b96d3f
6 changed files with 267 additions and 31 deletions

View file

@ -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" },

124
examples/app/errors.zig Normal file
View file

@ -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,
});
}

View file

@ -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!";
}

View file

@ -19,6 +19,11 @@ fn on_request(r: zap.Request) !void {
try r.sendBody("<html><body><h1>Hello from ZAP!!!</h1></body></html>");
}
// 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;

View file

@ -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 }),
}
};
}

View file

@ -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);
}
};
}
}