Skip to content

Release v0.10.0

Compare
Choose a tag to compare
@github-actions github-actions released this 30 Mar 19:15
a01c614

ZAP Release v0.10.0

Updates

What's new in ZAP?

  • Upgraded to Zig 0.14!
  • Breaking Changes for zap.Endpoint
  • New zap.App!
  • Updated README
  • Contributions!

After a break, I'm about to work a lot more with Zap, and in preparation made a few improvements which might also work in favor of newcomers.

BTW newcomers: please, also check out these other, pure-zig (which Zap is not) HTTP server projects:

  • http.zig : Pure Zig! Close to Zap's model. Performance = good!
  • jetzig : Comfortably develop modern web applications quickly, using http.zig under the hood
  • zzz : Super promising, super-fast, especially for IO-heavy tasks, io_uring support - need I say more?

I can't wait for the day that Zap becomes obsolete. It would be a very good sign for the Zig HTTP server space!

Breaking Changes for zap.Endpoint

These breaking changes are meant to be improvements.

  • no @fieldParentPtr: Endpoints now directly get their @This() pointer passed into their methods
  • request handlers are allowed to return errors!
  • the .error_strategy decides if errors are logged to console or reported as HTML to the client (for debugging in the browser)
  • no "Settings":
    • path and error_strategy are required for Endpoints
    • all http method handlers must be present, but of course may be empty
    • all of the above are checked at comptime, with meaningful compile error messages
  • you register your custom Endpoint instances directly with the
    zap.Endpoint.Listener, no need to provide an .endpoint() method.

It's best illustrated by example of error.zig (of the updated endpoints example) which creates the ErrorEndpoint:

//!
//! An ErrorEndpoint
//!
const std = @import("std");
const zap = @import("zap");

/// A simple endpoint listening on the /error route that causes an error on GET
/// requests, which gets logged to the response (=browser) by default
pub const ErrorEndpoint = @This();

path: []const u8 = "/error",
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,

pub fn get(_: *ErrorEndpoint, _: zap.Request) !void {
    return error.@"Oh-no!";
}

// unused:
pub fn post(_: *ErrorEndpoint, _: zap.Request) !void {}
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 {}

All relevant examples have been updated accordingly.

The New zap.App

In a way, 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.

Here is a complete zap.App example:

//!
//! Part of the Zap examples.
//!
//! Build me with `zig build     app_basic`.
//! Run   me with `zig build run-app_basic`.
//!
const std = @import("std");
const Allocator = std.mem.Allocator;

const zap = @import("zap");

// The global Application Context
const MyContext = struct {
    db_connection: []const u8,

    pub fn init(connection: []const u8) MyContext {
        return .{
            .db_connection = connection,
        };
    }
};

// A very simple endpoint handling only GET requests
const SimpleEndpoint = struct {

    // zap.App.Endpoint Interface part
    path: []const u8,
    error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,

    // data specific for this endpoint
    some_data: []const u8,

    pub fn init(path: []const u8, data: []const u8) SimpleEndpoint {
        return .{
            .path = path,
            .some_data = data,
        };
    }

    // handle GET requests
    pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) !void {
        const thread_id = std.Thread.getCurrentId();

        r.setStatus(.ok);

        // look, we use the arena allocator here -> no need to free the response_text later!
        // and we also just `try` it, not worrying about errors
        const response_text = try std.fmt.allocPrint(
            arena,
            \\Hello!
            \\context.db_connection: {s}
            \\endpoint.data: {s}
            \\arena: {}
            \\thread_id: {}
            \\
        ,
            .{ context.db_connection, e.some_data, arena.ptr, thread_id },
        );
        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 {
    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.init("db connection established!");

    // create an App instance
    const App = zap.App.Create(MyContext);
    var app = try App.init(allocator, &my_context, .{});
    defer app.deinit();

    // create the endpoints
    var my_endpoint = 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);

    // 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/test
        \\ Stop me via:
        \\ curl http://localhost:3000/stop
        \\
    , .{});

    // start worker threads -- only 1 process!!!
    zap.start(.{
        .threads = 2,
        .workers = 1,
    });
}

Updated README

  • restructured the examples section a bit
  • got rid of all the microbenchmark stuff
  • shout-outs to great Zap alternatives (http.zig, Jetzig, zzz)

Contributions!

Special thanks to:

  • Victor Moin (vctrmn): Fix deprecated warning in facil.io #154
  • Joshua B. (OsakiTsukiko): updated .gitignore, Endpoint improvements
  • Thom Dickson (cosmicboots): Add type checking to simple_router's handle_func #125

What's coming up...?

I am contemplating upgrading the underlying facil.io library to the new and improved version 0.8!

Thanks for reading and helping out 😊!

Using it

In your zig project folder (where build.zig is located), run:

zig fetch --save "git+https://github.com/zigzap/zap#v0.10.0"

Then, in your build.zig's build function, add the following before
b.installArtifact(exe):

    const zap = b.dependency("zap", .{
        .target = target,
        .optimize = optimize,
        .openssl = false, // set to true to enable TLS support
    });
    exe.root_module.addImport("zap", zap.module("zap"));