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

Compare commits

...

23 commits

Author SHA1 Message Date
GitHub Action
905b376c21 Update README 2025-07-23 00:26:09 +00:00
renerocksai
87415e1686
fix zon version 2025-07-23 02:23:56 +02:00
renerocksai
6105c2d4ed
bump zap version to non-confusing value 2025-07-23 02:22:39 +02:00
GitHub Action
ddcf3899c1 Update README 2025-07-23 00:10:46 +00:00
GitHub Action
0fc425a95d Update README 2025-07-23 00:06:39 +00:00
renerocksai
ef523d7767
zap.App.Endpoint.Authenticating: don't require unauthorized handler, return 405 method not allowed for unimplemented HTTP methods 2025-07-23 01:46:39 +02:00
Rene Schallner
29d339892e
Merge pull request #171 from Tesseract22/master
Provide defaults to unimplemented methods in endpoints for App style api.
2025-07-23 00:51:38 +02:00
Rene Schallner
35634a97cd
Merge pull request #167 from yanis-fourel/master
Accept file with no specified Content-Type
2025-07-23 00:47:08 +02:00
Rene Schallner
041ca3e301
Merge pull request #168 from QSmally/log-newline
Log: remove additional newlines due to Zig's default log fn
2025-07-23 00:38:45 +02:00
Tesseract22
c0cc025eda Provide defaults to unprovided restful method handler
Make `get`, `post`, ... methods optional. Check whether these method
exist at comptime. When no corresponding method is provided,
the handler simply return immediately.

Since it uses comptime, hopefully it should not add any checks at
runtime.
2025-07-13 15:02:35 +08:00
QSmally
fd567f3c29
Zap: remove newline for Zig's default log fn 2025-05-08 15:30:54 +02:00
Yanis Fourel
283e0d60d0 Add test_recvfile_notype.zig 2025-05-04 20:51:35 +08:00
Yanis Fourel
a7a904aea4 Add test_recvfile.zig 2025-05-04 20:51:35 +08:00
Yanis Fourel
4e1b50aca8 Accept file with no specified Content-Type 2025-05-04 14:31:39 +08:00
Rene Schallner
ec7cac6f6a
Merge pull request #164 from fwfurtado/hotfix/add-missing-head-handler-to-endpoints
fix: add missing head handler for EndpointType
2025-04-22 21:32:52 +02:00
Rene Schallner
f7cf3dd39f
Merge pull request #161 from unorsk/fix-readme-example
Fix hello world example in README.md
2025-04-22 21:25:58 +02:00
Mimi
b134f969f3 fix: add missing head handler for EndpointType 2025-04-21 18:36:46 -03:00
andrii
47ecb13d7b
Update README.md 2025-04-03 14:10:17 +02:00
renerocksai
8078b96d3f 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`.
2025-04-02 09:53:46 +02:00
renerocksai
4591f4048b change GH release workflow to produce non-prerelease releases 2025-04-01 16:46:23 +02:00
GitHub Action
295c525ec2 Update README 2025-04-01 14:41:53 +00:00
renerocksai
8d187310c7 harmonize callback function error handling, zap logging; doc updates 2025-04-01 16:30:24 +02:00
renerocksai
e93e45de42
fix spelling in README's shoutout to http.zig 2025-03-30 21:35:15 +02:00
35 changed files with 907 additions and 296 deletions

View file

@ -39,7 +39,7 @@ jobs:
tag_name: ${{ github.ref }}
release_name: Release ${{ steps.tag.outputs.version }} # Remove 'refs/tags/' from the tag name
body_path: relnotes.md
prerelease: true
prerelease: false
- name: Announce Release
env:

View file

@ -215,7 +215,7 @@ really promising.
### 📣 Shout-Outs
- [httpz](https://github.com/karlseguin/http.zig) : Pure Zig! Close to Zap's
- [http.zig](https://github.com/karlseguin/http.zig) : Pure Zig! Close to Zap's
model. Performance = good!
- [jetzig](https://github.com/jetzig-framework/jetzig) : Comfortably develop
modern web applications quickly, using http.zig under the hood
@ -298,7 +298,7 @@ In your zig project folder (where `build.zig` is located), run:
<!-- INSERT_DEP_BEGIN -->
```
zig fetch --save "git+https://github.com/zigzap/zap#v0.10.0"
zig fetch --save "git+https://github.com/zigzap/zap#v0.10.4"
```
<!-- INSERT_DEP_END -->
@ -376,7 +376,7 @@ $ zig build run-routes
const std = @import("std");
const zap = @import("zap");
fn on_request(r: zap.Request) void {
fn on_request(r: zap.Request) !void {
if (r.path) |the_path| {
std.debug.print("PATH: {s}\n", .{the_path});
}
@ -408,3 +408,7 @@ pub fn main() !void {

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" },
@ -185,6 +186,28 @@ pub fn build(b: *std.Build) !void {
const run_sendfile_tests = b.addRunArtifact(sendfile_tests);
const install_sendfile_tests = b.addInstallArtifact(sendfile_tests, .{});
const recvfile_tests = b.addTest(.{
.name = "recv_tests",
.root_source_file = b.path("src/tests/test_recvfile.zig"),
.target = target,
.optimize = optimize,
});
recvfile_tests.root_module.addImport("zap", zap_module);
const run_recvfile_tests = b.addRunArtifact(recvfile_tests);
const install_recvfile_tests = b.addInstallArtifact(recvfile_tests, .{});
const recvfile_notype_tests = b.addTest(.{
.name = "recv_tests",
.root_source_file = b.path("src/tests/test_recvfile_notype.zig"),
.target = target,
.optimize = optimize,
});
recvfile_notype_tests.root_module.addImport("zap", zap_module);
const run_recvfile_notype_tests = b.addRunArtifact(recvfile_notype_tests);
const install_recvfile_notype_tests = b.addInstallArtifact(recvfile_notype_tests, .{});
// test commands
const run_auth_test_step = b.step("test-authentication", "Run auth unit tests [REMOVE zig-cache!]");
run_auth_test_step.dependOn(&run_auth_tests.step);
@ -202,6 +225,14 @@ pub fn build(b: *std.Build) !void {
run_sendfile_test_step.dependOn(&run_sendfile_tests.step);
run_sendfile_test_step.dependOn(&install_sendfile_tests.step);
const run_recvfile_test_step = b.step("test-recvfile", "Run http param unit tests [REMOVE zig-cache!]");
run_recvfile_test_step.dependOn(&run_recvfile_tests.step);
run_recvfile_test_step.dependOn(&install_recvfile_tests.step);
const run_recvfile_notype_test_step = b.step("test-recvfile_notype", "Run http param unit tests [REMOVE zig-cache!]");
run_recvfile_notype_test_step.dependOn(&run_recvfile_notype_tests.step);
run_recvfile_notype_test_step.dependOn(&install_recvfile_notype_tests.step);
// Similar to creating the run step earlier, this exposes a `test` step to
// the `zig build --help` menu, providing a way for the participant to request
// running the unit tests.
@ -210,6 +241,8 @@ pub fn build(b: *std.Build) !void {
test_step.dependOn(&run_mustache_tests.step);
test_step.dependOn(&run_httpparams_tests.step);
test_step.dependOn(&run_sendfile_tests.step);
test_step.dependOn(&run_recvfile_tests.step);
test_step.dependOn(&run_recvfile_notype_tests.step);
//
// docserver

View file

@ -1,6 +1,6 @@
.{
.name = .zap,
.version = "0.9.1",
.version = "0.10.4",
.paths = .{
"build.zig",
"build.zig.zon",

View file

@ -58,13 +58,6 @@ const MyEndpoint = struct {
);
try r.sendBody(response);
}
// not implemented, don't care
pub fn post(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn put(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn delete(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn patch(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn options(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
};
pub fn main() !void {
@ -82,8 +75,8 @@ pub fn main() !void {
// App is the type
// app is the instance
const App = zap.App.Create(MyContext);
var app = try App.init(allocator, &my_context, .{});
defer app.deinit();
try App.init(allocator, &my_context, .{});
defer App.deinit();
// create mini endpoint
var ep: MyEndpoint = .{
@ -100,10 +93,10 @@ pub fn main() !void {
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
// make the authenticating endpoint known to the app
try app.register(&auth_ep);
try App.register(&auth_ep);
// listen
try app.listen(.{
try App.listen(.{
.interface = "0.0.0.0",
.port = 3000,
});

View file

@ -59,13 +59,6 @@ const SimpleEndpoint = struct {
try r.sendBody(response_text);
std.time.sleep(std.time.ns_per_ms * 300);
}
// empty stubs for all other request methods
pub fn post(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn put(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn delete(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn patch(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
pub fn options(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
};
const StopEndpoint = struct {
@ -81,12 +74,6 @@ const StopEndpoint = struct {
, .{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 {
@ -103,19 +90,19 @@ pub fn main() !void {
// create an App instance
const App = zap.App.Create(MyContext);
var app = try App.init(allocator, &my_context, .{});
defer app.deinit();
try App.init(allocator, &my_context, .{});
defer App.deinit();
// create the endpoints
var my_endpoint = SimpleEndpoint.init("/test", "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);
// register the endpoints with the App
try App.register(&my_endpoint);
try App.register(&stop_endpoint);
// listen on the network
try app.listen(.{
try App.listen(.{
.interface = "0.0.0.0",
.port = 3000,
});

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

@ -0,0 +1,125 @@
//!
//! 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 {}
pub fn head(_: *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);
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

@ -7,6 +7,14 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
const Handler = struct {
var alloc: std.mem.Allocator = undefined;
@ -17,7 +25,7 @@ const Handler = struct {
};
if (r.body) |body| {
std.log.info("Body length is {any}\n", .{body.len});
std.log.info("Body length is {any}", .{body.len});
}
// parse potential query params (for ?terminate=true)
@ -35,7 +43,7 @@ const Handler = struct {
for (params.items) |kv| {
if (kv.value) |v| {
std.debug.print("\n", .{});
std.log.info("Param `{s}` in owned list is {any}\n", .{ kv.key, v });
std.log.info("Param `{s}` in owned list is {any}", .{ kv.key, v });
switch (v) {
// single-file upload
zap.Request.HttpParam.Hash_Binfile => |*file| {
@ -43,9 +51,9 @@ const Handler = struct {
const mimetype = file.mimetype orelse "(no mimetype)";
const data = file.data orelse "";
std.log.debug(" filename: `{s}`\n", .{filename});
std.log.debug(" mimetype: {s}\n", .{mimetype});
std.log.debug(" contents: {any}\n", .{data});
std.log.debug(" filename: `{s}`", .{filename});
std.log.debug(" mimetype: {s}", .{mimetype});
std.log.debug(" contents: {any}", .{data});
},
// multi-file upload
zap.Request.HttpParam.Array_Binfile => |*files| {
@ -54,9 +62,9 @@ const Handler = struct {
const mimetype = file.mimetype orelse "(no mimetype)";
const data = file.data orelse "";
std.log.debug(" filename: `{s}`\n", .{filename});
std.log.debug(" mimetype: {s}\n", .{mimetype});
std.log.debug(" contents: {any}\n", .{data});
std.log.debug(" filename: `{s}`", .{filename});
std.log.debug(" mimetype: {s}", .{mimetype});
std.log.debug(" contents: {any}", .{data});
}
files.*.deinit();
},
@ -71,7 +79,7 @@ const Handler = struct {
// check if we received a terminate=true parameter
if (r.getParamSlice("terminate")) |str| {
std.log.info("?terminate={s}\n", .{str});
std.log.info("?terminate={s}", .{str});
if (std.mem.eql(u8, str, "true")) {
zap.stop();
}
@ -100,11 +108,11 @@ pub fn main() !void {
.public_folder = ".",
},
);
zap.enableDebugLog();
try listener.listen();
std.log.info("\n\nURL is http://localhost:3000\n", .{});
std.log.info("\ncurl -v --request POST -F img=@test012345.bin http://127.0.0.1:3000\n", .{});
std.log.info("\n\nTerminate with CTRL+C or by sending query param terminate=true\n", .{});
std.log.info("\n\nURL is http://localhost:3000", .{});
std.log.info("\ncurl -v --request POST -F img=@test012345.bin http://127.0.0.1:3000", .{});
std.log.info("\n\nTerminate with CTRL+C or by sending query param terminate=true", .{});
zap.start(.{
.threads = 1,

View file

@ -7,6 +7,14 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
// We send ourselves a request with a cookie
fn makeRequest(a: std.mem.Allocator, url: []const u8) !void {
const uri = try std.Uri.parse(url);
@ -79,8 +87,8 @@ pub fn main() !void {
std.log.info("Cookie ZIG_ZAP not found!", .{});
}
} else |err| {
std.log.err("ERROR!\n", .{});
std.log.err("cannot check for `ZIG_ZAP` cookie: {any}\n", .{err});
std.log.err("ERROR!", .{});
std.log.err("cannot check for `ZIG_ZAP` cookie: {any}", .{err});
}
r.setCookie(.{
@ -93,8 +101,8 @@ pub fn main() !void {
//
// check out other params: domain, path, secure, http_only
}) catch |err| {
std.log.err("ERROR!\n", .{});
std.log.err("cannot set cookie: {any}\n", .{err});
std.log.err("ERROR!", .{});
std.log.err("cannot set cookie: {any}", .{err});
};
r.sendBody("Hello") catch unreachable;
@ -113,7 +121,6 @@ pub fn main() !void {
.max_body_size = 1 * 1024,
},
);
zap.enableDebugLog();
try listener.listen();
std.log.info("\n\nTerminate with CTRL+C", .{});

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!";
}
@ -18,3 +20,4 @@ pub fn put(_: *ErrorEndpoint, _: zap.Request) !void {}
pub fn delete(_: *ErrorEndpoint, _: zap.Request) !void {}
pub fn patch(_: *ErrorEndpoint, _: zap.Request) !void {}
pub fn options(_: *ErrorEndpoint, _: zap.Request) !void {}
pub fn head(_: *ErrorEndpoint, _: zap.Request) !void {}

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;
@ -77,5 +86,5 @@ pub fn main() !void {
// show potential memory leaks when ZAP is shut down
const has_leaked = gpa.detectLeaks();
std.log.debug("Has leaked: {}\n", .{has_leaked});
std.log.debug("Has leaked: {}", .{has_leaked});
}

View file

@ -23,3 +23,4 @@ pub fn put(_: *StopEndpoint, _: zap.Request) !void {}
pub fn delete(_: *StopEndpoint, _: zap.Request) !void {}
pub fn patch(_: *StopEndpoint, _: zap.Request) !void {}
pub fn options(_: *StopEndpoint, _: zap.Request) !void {}
pub fn head(_: *StopEndpoint, _: zap.Request) !void {}

View file

@ -127,7 +127,12 @@ pub fn delete(self: *UserWeb, r: zap.Request) !void {
pub fn options(_: *UserWeb, r: zap.Request) !void {
try r.setHeader("Access-Control-Allow-Origin", "*");
try r.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
try r.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD");
r.setStatus(zap.http.StatusCode.no_content);
r.markAsFinished(true);
}
pub fn head(_: *UserWeb, r: zap.Request) !void {
r.setStatus(zap.http.StatusCode.no_content);
r.markAsFinished(true);
}

View file

@ -38,6 +38,7 @@ const Endpoint = struct {
pub fn delete(_: *Endpoint, _: zap.Request) !void {}
pub fn patch(_: *Endpoint, _: zap.Request) !void {}
pub fn options(_: *Endpoint, _: zap.Request) !void {}
pub fn head(_: *Endpoint, _: zap.Request) !void {}
};
pub fn main() !void {

View file

@ -7,6 +7,14 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
// We send ourselves a request
fn makeRequest(a: std.mem.Allocator, url: []const u8) !void {
const uri = try std.Uri.parse(url);
@ -103,7 +111,7 @@ pub fn main() !void {
std.log.info("Param one not found!", .{});
}
} else |err| {
std.log.err("cannot check for `one` param: {any}\n", .{err});
std.log.err("cannot check for `one` param: {any}", .{err});
}
// check if we received a terminate=true parameter
@ -127,9 +135,9 @@ pub fn main() !void {
.max_body_size = 1 * 1024,
},
);
zap.enableDebugLog();
try listener.listen();
std.log.info("\n\nTerminate with CTRL+C or by sending query param terminate=true\n", .{});
std.log.info("\n\nTerminate with CTRL+C or by sending query param terminate=true", .{});
const thread = try makeRequestThread(allocator, "http://127.0.0.1:3000/?one=1&two=2&string=hello+world&float=6.28&bool=true");
defer thread.join();

View file

@ -7,6 +7,14 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
// just a way to share our allocator via callback
const SharedAllocator = struct {
// static
@ -212,7 +220,7 @@ pub fn main() !void {
userHandler.getHandler(),
SharedAllocator.getAllocator,
);
zap.enableDebugLog();
listener.listen() catch |err| {
std.debug.print("\nLISTEN ERROR: {any}\n", .{err});
return;

View file

@ -7,6 +7,14 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
// just a way to share our allocator via callback
const SharedAllocator = struct {
// static
@ -149,6 +157,7 @@ const HtmlEndpoint = struct {
pub fn delete(_: *HtmlEndpoint, _: zap.Request) !void {}
pub fn patch(_: *HtmlEndpoint, _: zap.Request) !void {}
pub fn options(_: *HtmlEndpoint, _: zap.Request) !void {}
pub fn head(_: *HtmlEndpoint, _: zap.Request) !void {}
pub fn get(_: *HtmlEndpoint, r: zap.Request) !void {
var buf: [1024]u8 = undefined;
@ -224,7 +233,7 @@ pub fn main() !void {
userHandler.getHandler(),
SharedAllocator.getAllocator,
);
zap.enableDebugLog();
listener.listen() catch |err| {
std.debug.print("\nLISTEN ERROR: {any}\n", .{err});
return;

View file

@ -8,6 +8,14 @@ const std = @import("std");
const zap = @import("zap");
const Mustache = @import("zap").Mustache;
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
fn on_request(r: zap.Request) !void {
const template =
\\ {{=<< >>=}}
@ -63,12 +71,9 @@ pub fn main() !void {
});
try listener.listen();
// zap.enableDebugLog();
// zap.debug("ZAP debug logging is on\n", .{});
// we can also use facilio logging
// zap.Log.fio_set_log_level(zap.Log.fio_log_level_debug);
// zap.Log.fio_log_debug("hello from fio\n");
// zap.Logging.fio_set_log_level(zap.Log.fio_log_level_debug);
// zap.Logging.fio_log_debug("hello from fio\n");
std.debug.print("Listening on 0.0.0.0:3000\n", .{});

View file

@ -7,6 +7,14 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
var buffer: [1024]u8 = undefined;
var read_len: ?usize = null;
@ -43,7 +51,6 @@ pub fn main() !void {
},
);
zap.enableDebugLog();
try listener.listen();
std.debug.print("Visit me on http://127.0.0.1:3000\n", .{});

View file

@ -7,6 +7,14 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
const Lookup = std.StringHashMap([]const u8);
const auth_lock_pw_table = false;
@ -140,8 +148,6 @@ pub fn main() !void {
});
try listener.listen();
zap.enableDebugLog();
// Usernames -> Passwords for the /login page
// ------------------------------------------
var userpass = Lookup.init(allocator);

View file

@ -81,20 +81,17 @@ const ContextManager = struct {
//
// Websocket Callbacks
//
fn on_open_websocket(context: ?*Context, handle: WebSockets.WsHandle) void {
fn on_open_websocket(context: ?*Context, handle: WebSockets.WsHandle) !void {
if (context) |ctx| {
_ = WebsocketHandler.subscribe(handle, &ctx.subscribeArgs) catch |err| {
std.log.err("Error opening websocket: {any}", .{err});
return;
};
_ = try WebsocketHandler.subscribe(handle, &ctx.subscribeArgs);
// say hello
var buf: [128]u8 = undefined;
const message = std.fmt.bufPrint(
const message = try std.fmt.bufPrint(
&buf,
"{s} joined the chat.",
.{ctx.userName},
) catch unreachable;
);
// send notification to all others
WebsocketHandler.publish(.{ .channel = ctx.channel, .message = message });
@ -102,16 +99,16 @@ fn on_open_websocket(context: ?*Context, handle: WebSockets.WsHandle) void {
}
}
fn on_close_websocket(context: ?*Context, uuid: isize) void {
fn on_close_websocket(context: ?*Context, uuid: isize) !void {
_ = uuid;
if (context) |ctx| {
// say goodbye
var buf: [128]u8 = undefined;
const message = std.fmt.bufPrint(
const message = try std.fmt.bufPrint(
&buf,
"{s} left the chat.",
.{ctx.userName},
) catch unreachable;
);
// send notification to all others
WebsocketHandler.publish(.{ .channel = ctx.channel, .message = message });
@ -124,7 +121,7 @@ fn handle_websocket_message(
handle: WebSockets.WsHandle,
message: []const u8,
is_text: bool,
) void {
) !void {
_ = handle;
_ = is_text;
@ -145,11 +142,11 @@ fn handle_websocket_message(
if (message.len > max_msg_len) {
trimmed_message = message[0..max_msg_len];
}
const chat_message = std.fmt.bufPrint(
const chat_message = try std.fmt.bufPrint(
&buf,
format_string,
.{ ctx.userName, trimmed_message },
) catch unreachable;
);
// send notification to all others
WebsocketHandler.publish(
@ -169,7 +166,7 @@ fn handle_websocket_message(
// HTTP stuff
//
fn on_request(r: zap.Request) !void {
r.setHeader("Server", "zap.example") catch unreachable;
try r.setHeader("Server", "zap.example");
try r.sendBody(
\\ <html><body>
\\ <h1>This is a simple Websocket chatroom example</h1>
@ -177,7 +174,7 @@ fn on_request(r: zap.Request) !void {
);
}
fn on_upgrade(r: zap.Request, target_protocol: []const u8) void {
fn on_upgrade(r: zap.Request, target_protocol: []const u8) !void {
// make sure we're talking the right protocol
if (!std.mem.eql(u8, target_protocol, "websocket")) {
std.log.warn("received illegal protocol: {s}", .{target_protocol});
@ -190,10 +187,7 @@ fn on_upgrade(r: zap.Request, target_protocol: []const u8) void {
return;
};
WebsocketHandler.upgrade(r.h, &context.settings) catch |err| {
std.log.err("Error in websocketUpgrade(): {any}", .{err});
return;
};
try WebsocketHandler.upgrade(r.h, &context.settings);
std.log.info("connection upgrade OK", .{});
}

View file

@ -1,3 +1,16 @@
//! zap.App takes the zap.Endpoint concept one step further: instead of having
//! only per-endpoint instance data (fields of your Endpoint struct), endpoints
//! in a zap.App easily share a global 'App Context'.
//!
//! In addition to the global App Context, all Endpoint request handlers also
//! receive an arena allocator for easy, care-free allocations. There is one
//! arena allocator per thread, and arenas are reset after each request.
//!
//! Just like regular / legacy zap.Endpoints, returning errors from request
//! handlers is OK. It's decided on a per-endpoint basis how errors are dealt
//! with, via the ErrorStrategy enum field.
//!
//! See `App.Create()`.
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
@ -16,7 +29,20 @@ pub const AppOpts = struct {
};
/// creates an App with custom app context
pub fn Create(comptime Context: type) type {
///
/// 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,
) type {
return struct {
const App = @This();
@ -35,19 +61,21 @@ pub fn Create(comptime Context: type) type {
/// 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,
@ -82,13 +110,15 @@ pub fn Create(comptime Context: type) type {
}
pub fn onRequest(self: *Bound, arena: Allocator, app_context: *Context, r: Request) !void {
// TODO: simplitfy this with @tagName?
const ret = switch (r.methodAsEnum()) {
.GET => self.endpoint.*.get(arena, app_context, r),
.POST => self.endpoint.*.post(arena, app_context, r),
.PUT => self.endpoint.*.put(arena, app_context, r),
.DELETE => self.endpoint.*.delete(arena, app_context, r),
.PATCH => self.endpoint.*.patch(arena, app_context, r),
.OPTIONS => self.endpoint.*.options(arena, app_context, r),
.GET => callHandlerIfExist("get", self.endpoint, arena, app_context, r),
.POST => callHandlerIfExist("post", self.endpoint, arena, app_context, r),
.PUT => callHandlerIfExist("put", self.endpoint, arena, app_context, r),
.DELETE => callHandlerIfExist("delete", self.endpoint, arena, app_context, r),
.PATCH => callHandlerIfExist("patch", self.endpoint, arena, app_context, r),
.OPTIONS => callHandlerIfExist("options", self.endpoint, arena, app_context, r),
.HEAD => callHandlerIfExist("head", self.endpoint, arena, app_context, r),
else => error.UnsupportedHtmlRequestMethod,
};
if (ret) {
@ -97,7 +127,7 @@ pub fn Create(comptime Context: type) type {
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 },
),
@ -145,6 +175,7 @@ pub fn Create(comptime Context: type) type {
"delete",
"patch",
"options",
"head",
};
const params_to_check = [_]type{
*T,
@ -197,8 +228,6 @@ pub fn Create(comptime Context: type) type {
if (ret_info.error_union.payload != void) {
@compileError("Expected return type of method `" ++ @typeName(T) ++ "." ++ method ++ "` to be !void, got: !" ++ @typeName(ret_info.error_union.payload));
}
} else {
@compileError(@typeName(T) ++ " has no method named `" ++ method ++ "`");
}
}
}
@ -227,8 +256,8 @@ pub fn Create(comptime Context: type) type {
/// Authenticates GET requests using the Authenticator.
pub fn get(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => return self.ep.*.unauthorized(arena, context, request),
.AuthOK => self.ep.*.get(arena, context, request),
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("get", self.ep, arena, context, request),
.Handled => {},
};
}
@ -236,8 +265,8 @@ pub fn Create(comptime Context: type) type {
/// Authenticates POST requests using the Authenticator.
pub fn post(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => return self.ep.*.unauthorized(arena, context, request),
.AuthOK => self.ep.*.post(arena, context, request),
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("post", self.ep, arena, context, request),
.Handled => {},
};
}
@ -245,8 +274,8 @@ pub fn Create(comptime Context: type) type {
/// Authenticates PUT requests using the Authenticator.
pub fn put(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => return self.ep.*.unauthorized(arena, context, request),
.AuthOK => self.ep.*.put(arena, context, request),
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("put", self.ep, arena, context, request),
.Handled => {},
};
}
@ -254,8 +283,8 @@ pub fn Create(comptime Context: type) type {
/// Authenticates DELETE requests using the Authenticator.
pub fn delete(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => return self.ep.*.unauthorized(arena, context, request),
.AuthOK => self.ep.*.delete(arena, context, request),
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("delete", self.ep, arena, context, request),
.Handled => {},
};
}
@ -263,8 +292,8 @@ pub fn Create(comptime Context: type) type {
/// Authenticates PATCH requests using the Authenticator.
pub fn patch(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => return self.ep.*.unauthorized(arena, context, request),
.AuthOK => self.ep.*.patch(arena, context, request),
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("patch", self.ep, arena, context, request),
.Handled => {},
};
}
@ -272,8 +301,17 @@ pub fn Create(comptime Context: type) type {
/// Authenticates OPTIONS requests using the Authenticator.
pub fn options(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => return self.ep.*.unauthorized(arena, context, request),
.AuthOK => self.ep.*.put(arena, context, request),
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("options", self.ep, arena, context, request),
.Handled => {},
};
}
/// Authenticates HEAD requests using the Authenticator.
pub fn head(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("head", self.ep, arena, context, request),
.Handled => {},
};
}
@ -293,7 +331,7 @@ pub fn Create(comptime Context: type) type {
tls: ?zap.Tls = null,
};
pub fn init(gpa_: Allocator, context_: *Context, opts_: AppOpts) !App {
pub fn init(gpa_: Allocator, context_: *Context, opts_: AppOpts) !void {
if (_static.there_can_be_only_one) {
return error.OnlyOneAppAllowed;
}
@ -302,20 +340,28 @@ pub fn Create(comptime Context: type) type {
_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 .{};
}
pub fn deinit(_: *App) void {
pub fn deinit() void {
// we created endpoint wrappers but only tracked their interfaces
// hence, we need to destroy the wrappers through their interfaces
if (false) {
@ -336,12 +382,26 @@ pub fn Create(comptime Context: type) type {
var it = _static.track_arenas.valueIterator();
while (it.next()) |arena| {
// std.debug.print("deiniting arena: {*}\n", .{arena});
arena.deinit();
}
_static.track_arenas.deinit(_static.gpa);
}
// This can be resolved at comptime so *perhaps it does affect optimiazation
pub fn callHandlerIfExist(comptime fn_name: []const u8, e: anytype, arena: Allocator, ctx: *Context, r: Request) anyerror!void {
const EndPoint = @TypeOf(e.*);
if (@hasDecl(EndPoint, fn_name)) {
return @field(EndPoint, fn_name)(e, arena, ctx, r);
}
zap.log.debug(
"Unhandled `{s}` {s} request ({s} not implemented in {s})",
.{ r.method orelse "<unknown>", r.path orelse "", fn_name, @typeName(Endpoint) },
);
r.setStatus(.method_not_allowed);
try r.sendBody("405 - method not allowed\r\n");
return;
}
pub fn get_arena() !*ArenaAllocator {
const thread_id = std.Thread.getCurrentId();
_static.track_arena_lock.lockShared();
@ -364,7 +424,7 @@ pub fn Create(comptime Context: type) type {
/// If you try to register an endpoint whose path would shadow an
/// already registered one, you will receive an
/// EndpointPathShadowError.
pub fn register(_: *App, endpoint: anytype) !void {
pub fn register(endpoint: anytype) !void {
for (_static.endpoints.items) |other| {
if (std.mem.startsWith(
u8,
@ -385,7 +445,7 @@ pub fn Create(comptime Context: type) type {
try _static.endpoints.append(_static.gpa, &bound.interface);
}
pub fn listen(_: *App, l: ListenerSettings) !void {
pub fn listen(l: ListenerSettings) !void {
_static.listener = HttpListener.init(.{
.interface = l.interface,
.port = l.port,
@ -404,17 +464,34 @@ pub fn Create(comptime Context: type) type {
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

@ -1,23 +1,18 @@
const std = @import("std");
// TODO: rework logging in zap
debugOn: bool,
/// Access to facil.io's logging facilities
const Log = @This();
pub fn init(comptime debug: bool) Log {
return .{
.debugOn = debug,
};
}
pub fn log(self: *const Log, comptime fmt: []const u8, args: anytype) void {
if (self.debugOn) {
std.debug.print("[zap] - " ++ fmt, args);
}
}
//! Access to facil.io's logging facilities
//!
//! Zap uses Zig's standard logging facilities, which you can control like this:
//!
//! ```zig
//! pub const std_options: std.Options = .{
//! // general log level
//! .log_level = .info,
//! .log_scope_levels = &[_]std.log.ScopeLevel{
//! // log level specific to zap
//! .{ .scope = .zap, .level = .debug },
//! },
//! };
//! ```
const Logging = @This();
pub extern const fio_log_level_none: c_int;
pub extern const fio_log_level_fatal: c_int;
@ -34,12 +29,9 @@ pub extern fn fio_log_error(msg: [*c]const u8) void;
pub extern fn fio_log_fatal(msg: [*c]const u8) void;
pub extern fn fio_log_debug(msg: [*c]const u8) void;
// pub fn getDebugLogger(comptime debug: bool) type {
// return struct {
// pub fn log(comptime fmt: []const u8, args: anytype) void {
// if (debug) {
// std.debug.print("[zap] - " ++ fmt, args);
// }
// }
// };
// }
/// Error reporting of last resort
pub fn on_uncaught_error(comptime domain: []const u8, err: anyerror) void {
const std = @import("std");
const log = std.log.scoped(.zap);
log.err(domain ++ " : {}", .{err});
}

View file

@ -10,17 +10,19 @@
//! ```zig
//! /// The http request path / slug of the endpoint
//! path: []const u8,
//! error_strategy: zap.Endpoint.ErrorStrategy,
//!
//! /// Handlers by request method:
//! pub fn get(_: *Self, _: zap.Request) void {}
//! pub fn post(_: *Self, _: zap.Request) void {}
//! pub fn put(_: *Self, _: zap.Request) void {}
//! pub fn delete(_: *Self, _: zap.Request) void {}
//! pub fn patch(_: *Self, _: zap.Request) void {}
//! pub fn options(_: *Self, _: zap.Request) void {}
//! pub fn get(_: *Self, _: zap.Request) !void {}
//! pub fn post(_: *Self, _: zap.Request) !void {}
//! pub fn put(_: *Self, _: zap.Request) !void {}
//! pub fn delete(_: *Self, _: zap.Request) !void {}
//! pub fn patch(_: *Self, _: zap.Request) !void {}
//! pub fn options(_: *Self, _: zap.Request) !void {}
//! pub fn head(_: *Self, _: zap.Request) !void {}
//!
//! // optional, if auth stuff is used:
//! pub fn unauthorized(_: *Self, _: zap.Request) void {}
//! pub fn unauthorized(_: *Self, _: zap.Request) !void {}
//! ```
//!
//! Example:
@ -30,22 +32,25 @@
//!
//! ```zig
//! const StopEndpoint = struct {
//! path: []const u8,
//! error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
//!
//! pub fn init( path: []const u8,) StopEndpoint {
//! pub fn init(path: []const u8) StopEndpoint {
//! return .{
//! .path = path,
//! };
//! }
//!
//! pub fn post(_: *StopEndpoint, _: zap.Request) void {}
//! pub fn put(_: *StopEndpoint, _: zap.Request) void {}
//! pub fn delete(_: *StopEndpoint, _: zap.Request) void {}
//! pub fn patch(_: *StopEndpoint, _: zap.Request) void {}
//! pub fn options(_: *StopEndpoint, _: zap.Request) void {}
//!
//! pub fn get(_: *StopEndpoint, _: zap.Request) void {
//! pub fn get(_: *StopEndpoint, _: zap.Request) !void {
//! zap.stop();
//! }
//!
//! pub fn post(_: *StopEndpoint, _: zap.Request) !void {}
//! pub fn put(_: *StopEndpoint, _: zap.Request) !void {}
//! pub fn delete(_: *StopEndpoint, _: zap.Request) !void {}
//! pub fn patch(_: *StopEndpoint, _: zap.Request) !void {}
//! pub fn options(_: *StopEndpoint, _: zap.Request) !void {}
//! pub fn head(_: *StopEndpoint, _: zap.Request) !void {}
//! };
//! ```
@ -57,9 +62,11 @@ const auth = @import("http_auth.zig");
pub const ErrorStrategy = enum {
/// log errors to console
log_to_console,
/// log errors to console AND generate a HTML response
/// send an HTML response containing an error trace
log_to_response,
/// raise errors -> TODO: clarify: where can they be caught? in App.run()
/// raise errors.
/// raised errors, if kept unhandled, will ultimately be logged by
/// zap.Logging.on_uncaught_error()
raise,
};
@ -92,6 +99,7 @@ pub fn checkEndpointType(T: type) void {
"delete",
"patch",
"options",
"head",
};
const params_to_check = [_]type{
@ -187,6 +195,7 @@ pub const Binder = struct {
.DELETE => self.endpoint.*.delete(r),
.PATCH => self.endpoint.*.patch(r),
.OPTIONS => self.endpoint.*.options(r),
.HEAD => self.endpoint.*.head(r),
else => error.UnsupportedHtmlRequestMethod,
};
if (ret) {
@ -195,7 +204,7 @@ pub const Binder = struct {
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("Error in {} {s} : {}", .{ Bound, r.method orelse "(no method)", err }),
.log_to_console => zap.log.err("Error in {} {s} : {}", .{ Bound, r.method orelse "(no method)", err }),
}
}
}
@ -290,6 +299,15 @@ pub fn Authenticating(EndpointType: type, Authenticator: type) type {
.Handled => {},
};
}
/// Authenticates HEAD requests using the Authenticator.
pub fn head(self: *AuthenticatingEndpoint, r: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&r)) {
.AuthFailed => return self.ep.*.unauthorized(r),
.AuthOK => self.ep.*.head(r),
.Handled => {},
};
}
};
}
@ -308,6 +326,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;
@ -316,23 +361,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,
@ -384,13 +452,32 @@ pub const Listener = struct {
if (r.path) |p| {
for (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 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 },
);
}
};
}
}
}
// if set, call the user-provided default callback
if (on_request) |foo| {
try foo(r);
foo(r) catch |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);
}
};
}
}
};

View file

@ -440,9 +440,9 @@ pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) ty
.value = "invalid",
.max_age_s = -1,
})) {
zap.debug("logout ok\n", .{});
zap.debug("logout ok", .{});
} else |err| {
zap.debug("logout cookie setting failed: {any}\n", .{err});
zap.debug("logout cookie setting failed: {any}", .{err});
}
r.parseCookies(false);

View file

@ -109,6 +109,7 @@ pub fn EndpointHandler(comptime HandlerType: anytype, comptime EndpointType: any
.DELETE => try self.endpoint.*.delete(r),
.PATCH => try self.endpoint.*.patch(r),
.OPTIONS => try self.endpoint.*.options(r),
.HEAD => try self.endpoint.*.head(r),
else => {},
}
}

View file

@ -194,18 +194,13 @@ fn fiobjectify(
.@"struct" => |S| {
// create a new fio hashmap
const m = fio.fiobj_hash_new();
// std.debug.print("new struct\n", .{});
inline for (S.fields) |Field| {
// don't include void fields
if (Field.type == void) continue;
// std.debug.print(" new field: {s}\n", .{Field.name});
const fname = fio.fiobj_str_new(util.toCharPtr(Field.name), Field.name.len);
// std.debug.print(" fiobj name : {any}\n", .{fname});
const v = @field(value, Field.name);
// std.debug.print(" value: {any}\n", .{v});
const fvalue = fiobjectify(v);
// std.debug.print(" fiobj value: {any}\n", .{fvalue});
_ = fio.fiobj_hash_set(m, fname, fvalue);
fio.fiobj_free_wrapped(fname);
}
@ -225,7 +220,6 @@ fn fiobjectify(
},
// TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972)
.slice => {
// std.debug.print("new slice\n", .{});
if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) {
return fio.fiobj_str_new(util.toCharPtr(value), value.len);
}

View file

@ -1,7 +1,6 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Log = @import("log.zig");
const http = @import("http.zig");
const fio = @import("fio.zig");
@ -126,36 +125,43 @@ fn parseBinfilesFrom(a: Allocator, o: fio.FIOBJ) !HttpParam {
fio.fiobj_free_wrapped(key_name);
fio.fiobj_free_wrapped(key_data);
fio.fiobj_free_wrapped(key_type);
} // files: they should have "data", "type", and "filename" keys
if (fio.fiobj_hash_haskey(o, key_data) == 1 and fio.fiobj_hash_haskey(o, key_type) == 1 and fio.fiobj_hash_haskey(o, key_name) == 1) {
} // files: they should have "data" and "filename" keys
if (fio.fiobj_hash_haskey(o, key_data) == 1 and fio.fiobj_hash_haskey(o, key_name) == 1) {
const filename = fio.fiobj_obj2cstr(fio.fiobj_hash_get(o, key_name));
const mimetype = fio.fiobj_obj2cstr(fio.fiobj_hash_get(o, key_type));
const data = fio.fiobj_hash_get(o, key_data);
var mimetype: []const u8 = undefined;
if (fio.fiobj_hash_haskey(o, key_type) == 1) {
const mt = fio.fiobj_obj2cstr(fio.fiobj_hash_get(o, key_type));
mimetype = mt.data[0..mt.len];
} else {
mimetype = &"application/octet-stream".*;
}
var data_slice: ?[]const u8 = null;
switch (fio.fiobj_type(data)) {
fio.FIOBJ_T_DATA => {
if (fio.is_invalid(data) == 1) {
data_slice = "(zap: invalid data)";
std.log.warn("WARNING: HTTP param binary file is not a data object\n", .{});
zap.log.warn("HTTP param binary file is not a data object", .{});
} else {
// the data
const data_len = fio.fiobj_data_len(data);
var data_buf = fio.fiobj_data_read(data, data_len);
if (data_len < 0) {
std.log.warn("WARNING: HTTP param binary file size negative: {d}\n", .{data_len});
std.log.warn("FIOBJ_TYPE of data is: {d}\n", .{fio.fiobj_type(data)});
zap.log.warn("HTTP param binary file size negative: {d}", .{data_len});
zap.log.warn("FIOBJ_TYPE of data is: {d}", .{fio.fiobj_type(data)});
} else {
if (data_buf.len != data_len) {
std.log.warn("WARNING: HTTP param binary file size mismatch: should {d}, is: {d}\n", .{ data_len, data_buf.len });
zap.log.warn("HTTP param binary file size mismatch: should {d}, is: {d}", .{ data_len, data_buf.len });
}
if (data_buf.len > 0) {
data_slice = data_buf.data[0..data_buf.len];
} else {
std.log.warn("WARNING: HTTP param binary file buffer size negative: {d}\n", .{data_buf.len});
zap.log.warn("HTTP param binary file buffer size negative: {d}", .{data_buf.len});
data_slice = "(zap: invalid data: negative BUFFER size)";
}
}
@ -165,7 +171,7 @@ fn parseBinfilesFrom(a: Allocator, o: fio.FIOBJ) !HttpParam {
const fiostr = fio.fiobj_obj2cstr(data);
if (fiostr.len == 0) {
data_slice = "(zap: empty string data)";
std.log.warn("WARNING: HTTP param binary file has empty string object\n", .{});
zap.log.warn("WARNING: HTTP param binary file has empty string object\n", .{});
} else {
data_slice = fiostr.data[0..fiostr.len];
}
@ -185,15 +191,15 @@ fn parseBinfilesFrom(a: Allocator, o: fio.FIOBJ) !HttpParam {
const file_mimetype_obj = fio.fiobj_ary_entry(mt_ary, i);
var has_error: bool = false;
if (fio.is_invalid(file_data_obj) == 1) {
std.log.debug("file data invalid in array", .{});
zap.log.debug("file data invalid in array", .{});
has_error = true;
}
if (fio.is_invalid(file_name_obj) == 1) {
std.log.debug("file name invalid in array", .{});
zap.log.debug("file name invalid in array", .{});
has_error = true;
}
if (fio.is_invalid(file_mimetype_obj) == 1) {
std.log.debug("file mimetype invalid in array", .{});
zap.log.debug("file mimetype invalid in array", .{});
has_error = true;
}
if (has_error) {
@ -222,7 +228,7 @@ fn parseBinfilesFrom(a: Allocator, o: fio.FIOBJ) !HttpParam {
return .{ .Hash_Binfile = .{
.filename = filename.data[0..filename.len],
.mimetype = mimetype.data[0..mimetype.len],
.mimetype = mimetype,
.data = data_slice,
} };
} else {
@ -358,7 +364,6 @@ pub fn sendBody(self: *const Request, body: []const u8) HttpError!void {
*anyopaque,
@ptrFromInt(@intFromPtr(body.ptr)),
), body.len);
zap.debug("Request.sendBody(): ret = {}\n", .{ret});
if (ret == -1) return error.HttpSendBody;
self.markAsFinished(true);
}
@ -381,7 +386,7 @@ pub fn setContentType(self: *const Request, c: ContentType) HttpError!void {
.JSON => "application/json",
else => "text/html",
};
zap.debug("setting content-type to {s}\n", .{s});
zap.log.debug("setting content-type to {s}", .{s});
return self.setHeader("content-type", s);
}
@ -393,21 +398,6 @@ pub fn redirectTo(self: *const Request, path: []const u8, code: ?http.StatusCode
self.markAsFinished(true);
}
/// shows how to use the logger
pub fn setContentTypeWithLogger(
self: *const Request,
c: ContentType,
logger: *const Log,
) HttpError!void {
const s = switch (c) {
.TEXT => "text/plain",
.JSON => "application/json",
else => "text/html",
};
logger.log("setting content-type to {s}\n", .{s});
return self.setHeader("content-type", s);
}
/// Tries to determine the content type by file extension of request path, and sets it.
pub fn setContentTypeFromPath(self: *const Request) !void {
const t = fio.http_mimetype_find2(self.h.*.path);
@ -518,21 +508,18 @@ pub fn setHeader(self: *const Request, name: []const u8, value: []const u8) Http
.capa = name.len,
};
zap.debug("setHeader: hname = {s}\n", .{name});
const vname: fio.fio_str_info_s = .{
.data = util.toCharPtr(value),
.len = value.len,
.capa = value.len,
};
zap.debug("setHeader: vname = {s}\n", .{value});
const ret = fio.http_set_header2(self.h, hname, vname);
// FIXME without the following if, we get errors in release builds
// at least we don't have to log unconditionally
if (ret == -1) {
std.debug.print("***************** zap.zig:274\n", .{});
zap.log.debug("***************** zap.zig:274", .{});
}
zap.debug("setHeader: ret = {}\n", .{ret});
if (ret == 0) return;
return error.HttpSetHeader;
@ -700,7 +687,7 @@ pub fn setCookie(self: *const Request, args: CookieArgs) HttpError!void {
// TODO: still happening?
const ret = fio.http_set_cookie(self.h, c);
if (ret == -1) {
std.log.err("fio.http_set_cookie returned: {}\n", .{ret});
zap.log.err("fio.http_set_cookie returned: {}", .{ret});
return error.SetCookie;
}
}

View file

@ -170,6 +170,7 @@ pub const Endpoint = struct {
pub fn delete(_: *Endpoint, _: zap.Request) !void {}
pub fn patch(_: *Endpoint, _: zap.Request) !void {}
pub fn options(_: *Endpoint, _: zap.Request) !void {}
pub fn head(_: *Endpoint, _: zap.Request) !void {}
};
//
// end of http client code
@ -209,10 +210,6 @@ test "BearerAuthSingle authenticateRequest OK" {
try listener.register(&auth_ep);
listener.listen() catch {};
// std.debug.print("\n\n*******************************************\n", .{});
// std.debug.print("\n\nPlease run the following:\n", .{});
// std.debug.print("./zig-out/bin/http_client_runner\n", .{});
// std.debug.print("\n\n*******************************************\n", .{});
const thread = try makeRequestThread(a, "http://127.0.0.1:3000/test", .{ .auth = .Bearer, .token = token });
defer thread.join();
@ -266,8 +263,6 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" {
try listener.register(&auth_ep);
try listener.listen();
// std.debug.print("Waiting for the following:\n", .{});
// std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Bearer invalid\r", .{});
const thread = try makeRequestThread(a, "http://127.0.0.1:3000/test", .{ .auth = .Bearer, .token = "invalid" });
defer thread.join();
@ -279,7 +274,6 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" {
});
try std.testing.expectEqualStrings("UNAUTHORIZED", received_response);
std.debug.print("\nI made it\n", .{});
}
test "BearerAuthMulti authenticateRequest OK" {
@ -316,8 +310,6 @@ test "BearerAuthMulti authenticateRequest OK" {
try listener.register(&auth_ep);
try listener.listen();
// std.debug.print("Waiting for the following:\n", .{});
// std.debug.print("./zig-out/bin/http_client_runner http://127.0.0.1:3000/test Bearer " ++ token ++ "\r", .{});
const thread = try makeRequestThread(a, "http://127.0.0.1:3000/test", .{ .auth = .Bearer, .token = token });
defer thread.join();
@ -365,8 +357,6 @@ test "BearerAuthMulti authenticateRequest test-unauthorized" {
try listener.register(&auth_ep);
listener.listen() catch {};
// std.debug.print("Waiting for the following:\n", .{});
// std.debug.print("./zig-out/bin/http_client_runner http://127.0.0.1:3000/test Bearer invalid\r", .{});
const thread = try makeRequestThread(a, "http://127.0.0.1:3000/test", .{ .auth = .Bearer, .token = "invalid" });
defer thread.join();
@ -419,8 +409,6 @@ test "BasicAuth Token68 authenticateRequest" {
try listener.register(&auth_ep);
listener.listen() catch {};
// std.debug.print("Waiting for the following:\n", .{});
// std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic " ++ token ++ "\r", .{});
const thread = try makeRequestThread(a, "http://127.0.0.1:3000/test", .{ .auth = .Basic, .token = token });
defer thread.join();
@ -473,8 +461,6 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" {
try listener.register(&auth_ep);
listener.listen() catch {};
// std.debug.print("Waiting for the following:\n", .{});
// std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic " ++ "invalid\r", .{});
const thread = try makeRequestThread(a, "http://127.0.0.1:3000/test", .{ .auth = .Basic, .token = "invalid" });
defer thread.join();
@ -537,8 +523,6 @@ test "BasicAuth UserPass authenticateRequest" {
try listener.register(&auth_ep);
listener.listen() catch {};
// std.debug.print("Waiting for the following:\n", .{});
// std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic {s}\r", .{encoded});
const thread = try makeRequestThread(a, "http://127.0.0.1:3000/test", .{ .auth = .Basic, .token = encoded });
defer thread.join();
@ -604,8 +588,6 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" {
try listener.register(&auth_ep);
listener.listen() catch {};
// std.debug.print("Waiting for the following:\n", .{});
// std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic invalid\r", .{});
const thread = try makeRequestThread(a, "http://127.0.0.1:3000/test", .{ .auth = .Basic, .token = "invalid" });
defer thread.join();

View file

@ -1,6 +1,14 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
fn makeRequest(a: std.mem.Allocator, url: []const u8) !void {
var http_client: std.http.Client = .{ .allocator = a };
defer http_client.deinit();
@ -64,7 +72,6 @@ test "http parameters" {
.max_body_size = 1 * 1024,
},
);
zap.enableDebugLog();
try listener.listen();
const thread = try makeRequestThread(allocator, "http://127.0.0.1:3001/?one=1&two=2&string=hello+world&float=6.28&bool=true");

133
src/tests/test_recvfile.zig Normal file
View file

@ -0,0 +1,133 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
const BOUNDARY = "---XcPmntPm3EGd9NaxNUPFFL";
const PARAM_NAME = "file";
const EXPECTED_CONTENT = "Hello, this is a test file.";
const EXPECTED_MIMETYPE = "text/plain";
const EXPECTED_FILENAME = "myfile.txt";
var test_error: ?anyerror = null;
fn makeRequest(allocator: std.mem.Allocator, url: []const u8) !void {
var http_client: std.http.Client = .{ .allocator = allocator };
defer http_client.deinit();
const payload_wrong_line_ending = try std.fmt.allocPrint(allocator,
\\--{s}
\\Content-Disposition: form-data; name="{s}"; filename="{s}"
\\Content-Type: {s}
\\
\\{s}
\\--{s}--
\\
, .{ BOUNDARY, PARAM_NAME, EXPECTED_FILENAME, EXPECTED_MIMETYPE, EXPECTED_CONTENT.*, BOUNDARY });
defer allocator.free(payload_wrong_line_ending);
const payload = try std.mem.replaceOwned(u8, allocator, payload_wrong_line_ending, "\n", "\r\n");
defer allocator.free(payload);
const request_content_type = try std.fmt.allocPrint(
allocator,
"multipart/form-data; boundary={s}",
.{BOUNDARY},
);
defer allocator.free(request_content_type);
// Allocate a buffer for server headers
var buf: [4096]u8 = undefined;
_ = try http_client.fetch(.{
.method = .POST,
.location = .{ .url = url },
.headers = .{
.content_type = .{ .override = request_content_type },
},
.payload = payload,
.server_header_buffer = &buf,
});
zap.stop();
}
pub fn on_request(r: zap.Request) !void {
on_request_inner(r) catch |err| {
test_error = err;
return;
};
}
pub fn on_request_inner(r: zap.Request) !void {
try r.parseBody();
const params = try r.parametersToOwnedList(std.testing.allocator);
defer params.deinit();
std.testing.expect(params.items.len == 1) catch |err| {
std.debug.print("Expected exactly one parameter, got {}\n", .{params.items.len});
return err;
};
const param = params.items[0];
std.testing.expect(param.value != null) catch |err| {
std.debug.print("Expected parameter value to be non-null, got null\n", .{});
return err;
};
const value = param.value.?;
std.testing.expect(value == .Hash_Binfile) catch |err| {
std.debug.print("Expected parameter type to be Hash_Binfile, got {}\n", .{value});
return err;
};
const file = value.Hash_Binfile;
std.testing.expect(file.data != null) catch |err| {
std.debug.print("Expected file data to be non-null, got null\n", .{});
return err;
};
std.testing.expect(file.mimetype != null) catch |err| {
std.debug.print("Expected file mimetype to be non-null, got null\n", .{});
return err;
};
std.testing.expect(file.filename != null) catch |err| {
std.debug.print("Expected file filename to be non-null, got null\n", .{});
return err;
};
// These will print the error if the test fails
try std.testing.expectEqualStrings(file.data.?, &EXPECTED_CONTENT.*);
try std.testing.expectEqualStrings(file.mimetype.?, &EXPECTED_MIMETYPE.*);
try std.testing.expectEqualStrings(file.filename.?, &EXPECTED_FILENAME.*);
}
test "recv file" {
const allocator = std.testing.allocator;
var listener = zap.HttpListener.init(
.{
.port = 3003,
.on_request = on_request,
.log = false,
.max_clients = 10,
.max_body_size = 1 * 1024,
},
);
try listener.listen();
const t1 = try std.Thread.spawn(.{}, makeRequest, .{ allocator, "http://127.0.0.1:3003" });
defer t1.join();
zap.start(.{
.threads = 1,
.workers = 1,
});
if (test_error) |err| {
return err;
}
}

View file

@ -0,0 +1,131 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
const BOUNDARY = "---XcPmntPm3EGd9NaxNUPFFL";
const PARAM_NAME = "file";
const EXPECTED_CONTENT = "Hello, this is a test file.";
const EXPECTED_MIMETYPE = "application/octet-stream";
const EXPECTED_FILENAME = "myfile.txt";
var test_error: ?anyerror = null;
fn makeRequest(allocator: std.mem.Allocator, url: []const u8) !void {
var http_client: std.http.Client = .{ .allocator = allocator };
defer http_client.deinit();
const payload_wrong_line_ending = try std.fmt.allocPrint(allocator,
\\--{s}
\\Content-Disposition: form-data; name={s}"; filename="{s}"
\\
\\{s}
\\--{s}--
\\
, .{ BOUNDARY, PARAM_NAME, EXPECTED_FILENAME, EXPECTED_CONTENT.*, BOUNDARY });
defer allocator.free(payload_wrong_line_ending);
const payload = try std.mem.replaceOwned(u8, allocator, payload_wrong_line_ending, "\n", "\r\n");
defer allocator.free(payload);
const request_content_type = try std.fmt.allocPrint(
allocator,
"multipart/form-data; boundary={s}",
.{BOUNDARY},
);
defer allocator.free(request_content_type);
// Allocate a buffer for server headers
var buf: [4096]u8 = undefined;
_ = try http_client.fetch(.{
.method = .POST,
.location = .{ .url = url },
.headers = .{
.content_type = .{ .override = request_content_type },
},
.payload = payload,
.server_header_buffer = &buf,
});
zap.stop();
}
pub fn on_request(r: zap.Request) !void {
on_request_inner(r) catch |err| {
test_error = err;
return;
};
}
pub fn on_request_inner(r: zap.Request) !void {
try r.parseBody();
const params = try r.parametersToOwnedList(std.testing.allocator);
defer params.deinit();
std.testing.expect(params.items.len == 1) catch |err| {
std.debug.print("Expected exactly one parameter, got {}\n", .{params.items.len});
return err;
};
const param = params.items[0];
std.testing.expect(param.value != null) catch |err| {
std.debug.print("Expected parameter value to be non-null, got null\n", .{});
return err;
};
const value = param.value.?;
std.testing.expect(value == .Hash_Binfile) catch |err| {
std.debug.print("Expected parameter type to be Hash_Binfile, got {}\n", .{value});
return err;
};
const file = value.Hash_Binfile;
std.testing.expect(file.data != null) catch |err| {
std.debug.print("Expected file data to be non-null, got null\n", .{});
return err;
};
std.testing.expect(file.mimetype != null) catch |err| {
std.debug.print("Expected file mimetype to be non-null, got null\n", .{});
return err;
};
std.testing.expect(file.filename != null) catch |err| {
std.debug.print("Expected file filename to be non-null, got null\n", .{});
return err;
};
// These will print the error if the test fails
try std.testing.expectEqualStrings(file.data.?, &EXPECTED_CONTENT.*);
try std.testing.expectEqualStrings(file.mimetype.?, &EXPECTED_MIMETYPE.*);
try std.testing.expectEqualStrings(file.filename.?, &EXPECTED_FILENAME.*);
}
test "recv file" {
const allocator = std.testing.allocator;
var listener = zap.HttpListener.init(
.{
.port = 3003,
.on_request = on_request,
.log = false,
.max_clients = 10,
.max_body_size = 1 * 1024,
},
);
try listener.listen();
const t1 = try std.Thread.spawn(.{}, makeRequest, .{ allocator, "http://127.0.0.1:3003" });
defer t1.join();
zap.start(.{
.threads = 1,
.workers = 1,
});
if (test_error) |err| {
return err;
}
}

View file

@ -1,6 +1,14 @@
const std = @import("std");
const zap = @import("zap");
// set default log level to .info and ZAP log level to .debug
pub const std_options: std.Options = .{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .zap, .level = .debug },
},
};
var buffer: [1024]u8 = undefined;
var read_len: ?usize = null;
@ -41,7 +49,6 @@ test "send file" {
.max_body_size = 1 * 1024,
},
);
zap.enableDebugLog();
try listener.listen();
const thread = try makeRequestThread(allocator, "http://127.0.0.1:3002/?file=src/tests/testfile.txt");

View file

@ -21,15 +21,15 @@ pub fn Handler(comptime ContextType: type) type {
message: []const u8,
/// indicator if message is text or binary
is_text: bool,
) void;
) anyerror!void;
/// Callback (type) when websocket is closed. uuid is a connection identifier,
/// it is -1 if a connection could not be established
pub const WsOnCloseFn = *const fn (context: ?*ContextType, uuid: isize) void;
pub const WsOnCloseFn = *const fn (context: ?*ContextType, uuid: isize) anyerror!void;
/// A websocket callback function type. provides the context passed in at
/// websocketHttpUpgrade().
pub const WsFn = *const fn (context: ?*ContextType, handle: WsHandle) void;
pub const WsFn = *const fn (context: ?*ContextType, handle: WsHandle) anyerror!void;
/// Websocket connection handler creation settings. Provide the callbacks you need,
/// and an optional context.
@ -68,7 +68,9 @@ pub fn Handler(comptime ContextType: type) type {
const message = msg.data[0..msg.len];
if (user_provided_settings) |settings| {
if (settings.on_message) |on_message| {
on_message(settings.context, handle, message, is_text == 1);
on_message(settings.context, handle, message, is_text == 1) catch |err| {
zap.Logging.on_uncaught_error("WebSocket Handler on_message", err);
};
}
}
}
@ -77,7 +79,9 @@ pub fn Handler(comptime ContextType: type) type {
const user_provided_settings: ?*WebSocketSettings = @as(?*WebSocketSettings, @ptrCast(@alignCast(fio.websocket_udata_get(handle))));
if (user_provided_settings) |settings| {
if (settings.on_open) |on_open| {
on_open(settings.context, handle);
on_open(settings.context, handle) catch |err| {
zap.Logging.on_uncaught_error("WebSocket Handler on_open", err);
};
}
}
}
@ -86,7 +90,9 @@ pub fn Handler(comptime ContextType: type) type {
const user_provided_settings: ?*WebSocketSettings = @as(?*WebSocketSettings, @ptrCast(@alignCast(fio.websocket_udata_get(handle))));
if (user_provided_settings) |settings| {
if (settings.on_ready) |on_ready| {
on_ready(settings.context, handle);
on_ready(settings.context, handle) catch |err| {
zap.Logging.on_uncaught_error("WebSocket Handler on_ready", err);
};
}
}
}
@ -95,7 +101,9 @@ pub fn Handler(comptime ContextType: type) type {
const user_provided_settings: ?*WebSocketSettings = @as(?*WebSocketSettings, @ptrCast(@alignCast(fio.websocket_udata_get(handle))));
if (user_provided_settings) |settings| {
if (settings.on_shutdown) |on_shutdown| {
on_shutdown(settings.context, handle);
on_shutdown(settings.context, handle) catch |err| {
zap.Logging.on_uncaught_error("WebSocket Handler on_shutdown", err);
};
}
}
}
@ -104,7 +112,9 @@ pub fn Handler(comptime ContextType: type) type {
const user_provided_settings: ?*WebSocketSettings = @as(?*WebSocketSettings, @ptrCast(@alignCast(udata)));
if (user_provided_settings) |settings| {
if (settings.on_close) |on_close| {
on_close(settings.context, uuid);
on_close(settings.context, uuid) catch |err| {
zap.Logging.on_uncaught_error("WebSocket Handler on_close", err);
};
}
}
}
@ -155,10 +165,10 @@ pub fn Handler(comptime ContextType: type) type {
}
/// Type for callback on subscription message.
pub const SubscriptionOnMessageFn = *const fn (context: ?*ContextType, handle: WsHandle, channel: []const u8, message: []const u8) void;
pub const SubscriptionOnMessageFn = *const fn (context: ?*ContextType, handle: WsHandle, channel: []const u8, message: []const u8) anyerror!void;
/// Type for callback on unsubscribe message.
pub const SubscriptionOnUnsubscribeFn = *const fn (context: ?*ContextType) void;
pub const SubscriptionOnUnsubscribeFn = *const fn (context: ?*ContextType) anyerror!void;
/// Settings for subscribing to a channel.
pub const SubscribeArgs = struct {
@ -213,7 +223,9 @@ pub fn Handler(comptime ContextType: type) type {
if (udata) |p| {
const args = @as(*SubscribeArgs, @ptrCast(@alignCast(p)));
if (args.on_message) |on_message| {
on_message(args.context, handle, channel.data[0..channel.len], message.data[0..message.len]);
on_message(args.context, handle, channel.data[0..channel.len], message.data[0..message.len]) catch |err| {
zap.Logging.on_uncaught_error("WebSocket Subscription on_message", err);
};
}
}
}
@ -221,7 +233,9 @@ pub fn Handler(comptime ContextType: type) type {
if (udata) |p| {
const args = @as(*SubscribeArgs, @ptrCast(@alignCast(p)));
if (args.on_unsubscribe) |on_unsubscribe| {
on_unsubscribe(args.context);
on_unsubscribe(args.context) catch |err| {
zap.Logging.on_uncaught_error("WebSocket Subscription on_unsubscribe", err);
};
}
}
}

View file

@ -37,13 +37,12 @@ pub const Middleware = @import("middleware.zig");
/// Websocket API
pub const WebSockets = @import("websockets.zig");
pub const Log = @import("log.zig");
pub const Logging = @import("Logging.zig");
pub const log = std.log.scoped(.zap);
pub const http = @import("http.zig");
pub const util = @import("util.zig");
// TODO: replace with comptime debug logger like in log.zig
var _debug: bool = false;
/// Start the IO reactor
///
/// Will start listeners etc.
@ -60,23 +59,9 @@ pub fn stop() void {
fio.fio_stop();
}
/// Extremely simplistic zap debug function.
/// TODO: re-wwrite logging or use std.log
/// Extremely simplistic zap debug function, to save a few keystrokes
pub fn debug(comptime fmt: []const u8, args: anytype) void {
if (_debug) {
std.debug.print("[zap] - " ++ fmt, args);
}
}
/// Enable zap debug logging
pub fn enableDebugLog() void {
_debug = true;
}
/// start Zap with debug logging on
pub fn startWithLogging(args: fio.fio_start_args) void {
_debug = true;
fio.fio_start(args);
log.debug("[zap] - " ++ fmt, args);
}
/// Registers a new mimetype to be used for files ending with the given extension.
@ -138,14 +123,14 @@ pub const HttpRequestFn = *const fn (Request) anyerror!void;
/// websocket connection upgrade callback type
/// fn(request, targetstring)
pub const HttpUpgradeFn = *const fn (r: Request, target_protocol: []const u8) void;
pub const HttpUpgradeFn = *const fn (r: Request, target_protocol: []const u8) anyerror!void;
/// http finish, called when zap finishes. You get your udata back in the
/// HttpFinishSetting struct.
pub const HttpFinishSettings = [*c]fio.struct_http_settings_s;
/// Http finish callback type
pub const HttpFinishFn = *const fn (HttpFinishSettings) void;
pub const HttpFinishFn = *const fn (HttpFinishSettings) anyerror!void;
/// Listener settings
pub const HttpListenerSettings = struct {
@ -204,8 +189,7 @@ pub const HttpListener = struct {
std.debug.assert(l.settings.on_request != null);
if (l.settings.on_request) |on_request| {
on_request(req) catch |err| {
// TODO: log / handle the error in a better way
std.debug.print("zap on_request error: {}", .{err});
Logging.on_uncaught_error("HttpListener on_request", err);
};
}
}
@ -229,8 +213,7 @@ pub const HttpListener = struct {
req._user_context = &user_context;
l.settings.on_response.?(req) catch |err| {
// TODO: log / handle the error in a better way
std.debug.print("zap on_response error: {}", .{err});
Logging.on_uncaught_error("HttpListener on_response", err);
};
}
}
@ -253,14 +236,18 @@ pub const HttpListener = struct {
var user_context: Request.UserContext = .{};
req._user_context = &user_context;
l.settings.on_upgrade.?(req, zigtarget);
l.settings.on_upgrade.?(req, zigtarget) catch |err| {
Logging.on_uncaught_error("HttpListener on_upgrade", err);
};
}
}
/// Used internally: the listener's facilio finish callback
pub fn theOneAndOnlyFinishCallBack(s: [*c]fio.struct_http_settings_s) callconv(.C) void {
if (the_one_and_only_listener) |l| {
l.settings.on_finish.?(s);
l.settings.on_finish.?(s) catch |err| {
Logging.on_uncaught_error("HttpListener on_finish", err);
};
}
}
@ -390,7 +377,6 @@ pub const LowLevel = struct {
*anyopaque,
@ptrFromInt(@intFromPtr(body.ptr)),
), body.len);
debug("sendBody(): ret = {}\n", .{ret});
if (ret != -1) return error.HttpSendBody;
}
};