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