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_basic", .src = "examples/app/basic.zig" },
.{ .name = "app_auth", .src = "examples/app/auth.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 = "hello", .src = "examples/hello/hello.zig" },
.{ .name = "https", .src = "examples/https/https.zig" }, .{ .name = "https", .src = "examples/https/https.zig" },
.{ .name = "hello2", .src = "examples/hello2/hello2.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, error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
pub fn get(_: *ErrorEndpoint, _: zap.Request) !void { 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!"; 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>"); 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 { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{ var gpa = std.heap.GeneralPurposeAllocator(.{
.thread_safe = true, .thread_safe = true,
@ -33,6 +38,8 @@ pub fn main() !void {
.{ .{
.port = 3000, .port = 3000,
.on_request = on_request, .on_request = on_request,
// optional
.on_error = on_error,
.log = true, .log = true,
.public_folder = "examples/endpoint/html", .public_folder = "examples/endpoint/html",
.max_clients = 100000, .max_clients = 100000,
@ -47,11 +54,13 @@ pub fn main() !void {
var stopEp = StopEndpoint.init("/stop"); var stopEp = StopEndpoint.init("/stop");
var errorEp: ErrorEndpoint = .{}; var errorEp: ErrorEndpoint = .{};
var unhandledErrorEp: ErrorEndpoint = .{ .error_strategy = .raise, .path = "/unhandled" };
// register endpoints with the listener // register endpoints with the listener
try listener.register(&userWeb); try listener.register(&userWeb);
try listener.register(&stopEp); try listener.register(&stopEp);
try listener.register(&errorEp); try listener.register(&errorEp);
try listener.register(&unhandledErrorEp);
// fake some users // fake some users
var uid: usize = undefined; var uid: usize = undefined;

View file

@ -29,6 +29,16 @@ pub const AppOpts = struct {
}; };
/// creates an App with custom app context /// 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( pub fn Create(
/// Your user-defined "Global App Context" type /// Your user-defined "Global App Context" type
comptime Context: type, comptime Context: type,
@ -51,19 +61,21 @@ pub fn Create(
/// the internal http listener /// the internal http listener
listener: HttpListener = undefined, listener: HttpListener = undefined,
/// function pointer to handler for otherwise unhandled requests /// function pointer to handler for otherwise unhandled requests.
/// Will automatically be set if your Context provides an unhandled /// Will automatically be set if your Context provides an
/// function of type `fn(*Context, Allocator, Request)` /// `unhandledRequest` function of type `fn(*Context, Allocator,
/// /// Request) !void`.
unhandled: ?*const fn (*Context, Allocator, Request) anyerror!void = null, 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 = .{}; 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 Endpoint = struct {
pub const Interface = struct { pub const Interface = struct {
call: *const fn (*Interface, Request) anyerror!void = undefined, call: *const fn (*Interface, Request) anyerror!void = undefined,
@ -113,7 +125,7 @@ pub fn Create(
switch (self.endpoint.*.error_strategy) { switch (self.endpoint.*.error_strategy) {
.raise => return err, .raise => return err,
.log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505), .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} : {}", "Error in {} {s} : {}",
.{ Bound, r.method orelse "(no method)", err }, .{ Bound, r.method orelse "(no method)", err },
), ),
@ -318,15 +330,24 @@ pub fn Create(
_static.opts = opts_; _static.opts = opts_;
_static.there_can_be_only_one = true; _static.there_can_be_only_one = true;
// set unhandled callback if provided by Context // set unhandled_request callback if provided by Context
if (@hasDecl(Context, "unhandled")) { if (@hasDecl(Context, "unhandledRequest")) {
// try if we can use it // 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; const Expected = fn (_: *Context, _: Allocator, _: Request) anyerror!void;
if (Unhandled != Expected) { 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 .{}; return .{};
} }
@ -419,17 +440,34 @@ pub fn Create(
if (r.path) |p| { if (r.path) |p| {
for (_static.endpoints.items) |interface| { for (_static.endpoints.items) |interface| {
if (std.mem.startsWith(u8, p, interface.path)) { 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(); 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) { 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_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, listener: HttpListener,
allocator: std.mem.Allocator, 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 /// Internal static interface struct of member endpoints
var endpoints: std.ArrayListUnmanaged(*Binder.Interface) = .empty; var endpoints: std.ArrayListUnmanaged(*Binder.Interface) = .empty;
@ -321,23 +348,46 @@ pub const Listener = struct {
/// a request. /// a request.
var on_request: ?zap.HttpRequestFn = null; 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` /// Initialize a new endpoint listener. Note, if you pass an `on_request`
/// callback in the provided ListenerSettings, this request callback will be /// callback in the provided ListenerSettings, this request callback will be
/// called every time a request arrives that no endpoint matches. /// 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 // reset the global in case init is called multiple times, as is the
// case in the authentication tests // case in the authentication tests
endpoints = .empty; endpoints = .empty;
// take copy of listener settings before modifying the callback field var ls: zap.HttpListenerSettings = .{
var ls = l; .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 // override the settings with our internal, actual callback function
// so that "we" will be called on request // so that "we" will be called on request
ls.on_request = Listener.onRequest; ls.on_request = Listener.onRequest;
// store the settings-provided request callback for later use // store the settings-provided request callbacks for later use
on_request = l.on_request; on_request = settings.on_request;
on_error = settings.on_error;
return .{ return .{
.listener = HttpListener.init(ls), .listener = HttpListener.init(ls),
.allocator = a, .allocator = a,
@ -390,10 +440,16 @@ pub const Listener = struct {
for (endpoints.items) |interface| { for (endpoints.items) |interface| {
if (std.mem.startsWith(u8, p, interface.path)) { if (std.mem.startsWith(u8, p, interface.path)) {
return interface.call(interface, r) catch |err| { return interface.call(interface, r) catch |err| {
zap.log.err( // if error is not dealt with in the entpoint, e.g.
"Endpoint onRequest error {} in endpoint interface {}\n", // if error strategy is .raise:
.{ err, interface }, 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 set, call the user-provided default callback
if (on_request) |foo| { if (on_request) |foo| {
foo(r) catch |err| { 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);
}
}; };
} }
} }