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

Compare commits

...

14 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
15 changed files with 400 additions and 103 deletions

View file

@ -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.1"
zig fetch --save "git+https://github.com/zigzap/zap#v0.10.4"
```
<!-- INSERT_DEP_END -->
@ -409,3 +409,6 @@ pub fn main() !void {

View file

@ -186,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);
@ -203,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.
@ -211,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,14 +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 head(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
};
pub fn main() !void {
@ -83,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 = .{
@ -101,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,15 +59,7 @@ 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 {}
pub fn head(_: *SimpleEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {}
};
};
const StopEndpoint = struct {
path: []const u8,
@ -82,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 {
@ -104,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,
});

View file

@ -86,24 +86,24 @@ pub fn main() !void {
defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok});
const allocator = gpa.allocator();
// create an app context
// 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();
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);
// 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,
});

View file

@ -25,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)
@ -43,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| {
@ -51,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| {
@ -62,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();
},
@ -79,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();
}
@ -110,9 +110,9 @@ pub fn main() !void {
);
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

@ -87,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(.{
@ -101,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;

View file

@ -86,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

@ -111,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
@ -137,7 +137,7 @@ pub fn main() !void {
);
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

@ -110,14 +110,15 @@ pub fn Create(
}
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),
.HEAD => self.endpoint.*.head(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) {
@ -227,8 +228,6 @@ pub fn Create(
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 ++ "`");
}
}
}
@ -257,8 +256,8 @@ pub fn Create(
/// 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 => {},
};
}
@ -266,8 +265,8 @@ pub fn Create(
/// 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 => {},
};
}
@ -275,8 +274,8 @@ pub fn Create(
/// 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 => {},
};
}
@ -284,8 +283,8 @@ pub fn Create(
/// 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 => {},
};
}
@ -293,8 +292,8 @@ pub fn Create(
/// 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 => {},
};
}
@ -302,8 +301,8 @@ pub fn Create(
/// 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 => {},
};
}
@ -311,8 +310,8 @@ pub fn Create(
/// 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 => return self.ep.*.unauthorized(arena, context, request),
.AuthOK => self.ep.*.head(arena, context, request),
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("head", self.ep, arena, context, request),
.Handled => {},
};
}
@ -332,7 +331,7 @@ pub fn Create(
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;
}
@ -360,10 +359,9 @@ pub fn Create(
}
_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) {
@ -389,6 +387,21 @@ pub fn Create(
_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();
@ -411,7 +424,7 @@ pub fn Create(
/// 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,
@ -432,7 +445,7 @@ pub fn Create(
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,

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

@ -125,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)";
zap.log.warn("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) {
zap.log.warn("HTTP param binary file size negative: {d}\n", .{data_len});
zap.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) {
zap.log.warn("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 {
zap.log.warn("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)";
}
}
@ -221,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 {
@ -379,7 +386,7 @@ pub fn setContentType(self: *const Request, c: ContentType) HttpError!void {
.JSON => "application/json",
else => "text/html",
};
zap.log.debug("setting content-type to {s}\n", .{s});
zap.log.debug("setting content-type to {s}", .{s});
return self.setHeader("content-type", s);
}
@ -511,7 +518,7 @@ pub fn setHeader(self: *const Request, name: []const u8, value: []const u8) Http
// FIXME without the following if, we get errors in release builds
// at least we don't have to log unconditionally
if (ret == -1) {
zap.log.debug("***************** zap.zig:274\n", .{});
zap.log.debug("***************** zap.zig:274", .{});
}
if (ret == 0) return;
@ -680,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) {
zap.log.err("fio.http_set_cookie returned: {}\n", .{ret});
zap.log.err("fio.http_set_cookie returned: {}", .{ret});
return error.SetCookie;
}
}

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