mirror of
https://github.com/zigzap/zap.git
synced 2025-10-20 07:04: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:
parent
4591f4048b
commit
8078b96d3f
6 changed files with 267 additions and 31 deletions
|
@ -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
124
examples/app/errors.zig
Normal 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,
|
||||
});
|
||||
}
|
|
@ -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!";
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
80
src/App.zig
80
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 }),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue