SkillAgentSearch skills...

Websocket.zig

A websocket implementation for zig

Install / Use

/learn @karlseguin/Websocket.zig
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

A zig websocket server.

The master branch targets the latest stable of Zig (0.15.1). The dev branch targets the latest version of Zig. If you're looking for an older version, look for an zig-X.YZ branches.

Skip to the client section.

If you're upgrading from a previous version, check out the Server Migration and Client Migration wikis.

Server

const std = @import("std");
const ws = @import("websocket");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    var server = try ws.Server(Handler).init(allocator, .{
        .port = 9224,
        .address = "127.0.0.1",
        .handshake = .{
            .timeout = 3,
            .max_size = 1024,
            // since we aren't using hanshake.headers
            // we can set this to 0 to save a few bytes.
            .max_headers = 0,
        },
    });

    // Arbitrary (application-specific) data to pass into each handler
    // Pass void ({}) into listen if you have none
    var app = App{};

    // this blocks
    try server.listen(&app);
}

// This is your application-specific wrapper around a websocket connection
const Handler = struct {
    app: *App,
    conn: *ws.Conn,

    // You must define a public init function which takes
    pub fn init(h: *ws.Handshake, conn: *ws.Conn, app: *App) !Handler {
        // `h` contains the initial websocket "handshake" request
        // It can be used to apply application-specific logic to verify / allow
        // the connection (e.g. valid url, query string parameters, or headers)

        _ = h; // we're not using this in our simple case

        return .{
            .app = app,
            .conn = conn,
        };
    }

    // You must defined a public clientMessage method
    pub fn clientMessage(self: *Handler, data: []const u8) !void {
        try self.conn.write(data); // echo the message back
    }
};

// This is application-specific you want passed into your Handler's
// init function.
const App = struct {
  // maybe a db pool
  // maybe a list of rooms
};

Handler

When you create a websocket.Server(Handler), the specified Handler is your structure which will receive messages. It must have a public init function and clientMessage method. Other methods, such as close can optionally be defined.

init

The init method is called with a *websocket.Handshake, a *websocket.Conn and whatever app-specific value was passed into Server(H).init.

When init is called, the handshake response has not yet been sent to the client (this allows your init method to return an error which will cause websocket.zig to send an error response and close the connection). As such, you should not use/write to the *websocket.Conn at this point. Instead, use the afterInit method, described next.

The websocket specification requires the initial "handshake" to contain certain headers and values. The library validates these headers. However applications may have additional requirements before allowing the connection to be "upgraded" to a websocket connection. For example, a one-time-use token could be required in the querystring. Applications should use the provided websocket.Handshake to apply any application-specific verification and optionally return an error to terminate the connection.

The *websocket.Handshake exposes the following fields:

  • url: []const u8 - URL of the request in its original casing
  • method: []const u8 - Method of the request in its original casing
  • raw_headers: []const u8 - The raw "key1: value1\r\nkey2: value2\r\n" headers. Keys are lowercase.

If you set the max_headers configuration value to > 0, then you can use req.headers.get("HEADER_NAME") to extract a header value from the given name:

If you set the max_res_headers configuration value to > 0, then you can set headers to be sent in the handshake response:

pub fn init(h: *ws.Handshake, conn: *ws.Conn, app: *App) !Handler {
    h.res_headers.add("set-cookie", "delicious")
    //...
}

Note that, currently, the total length of the headers added to res_headers should not exceed 1024 characters, else you will exeperience an out of bounds segfault.

// the last parameter, an *App in this case, is an application-specific
// value that you passed into server.listen()
pub fn init(h: *websocket.Handshake, conn: websocket.Conn, app: *App) !Handler {
    // get returns a ?[]const u8
    // the name is lowercase
    // the value is in its original case
    const token = handshake.headers.get("authorization") orelse {
        return error.NotAuthorized;
    }

    return .{
        .app = app,
        .conn = conn,
    };
}

You can iterate through all the headers:

var it = handshake.headers.iterator();
while (it.next) |kv| {
    std.debug.print("{s} = {s}\n", .{kv.key, kv.value});
}

Memory referenced by the websocket.Handshake, including headers from handshake.headers will be freed after the call to init completes. Application that need these values to exist beyond the call to init must make a copy.

afterInit

If your handler defines a afterInit(handler: *Handler) !void method, the method is called after the handshake response has been sent. This is the first time the connection can safely be used.

afterInit supports two overloads:

pub fn afterInit(handler: *Handler) !void
pub fn afterInit(handler: *Handler, ctx: anytype) !void

The ctx is the same ctx passed into init. It is passed here for cases where the value is only needed once when the connection is established.

clientMessage

The clientMessage method is called whenever a text or binary message is received.

The clientMessage method can take one of four shapes. The simplest, shown in the first example, is:

// simple clientMessage
clientMessage(h: *Handler, data: []u8) !void

The Websocket specific has a distinct message type for text and binary. Text messages must be valid UTF-8. Websocket.zig does not do this validation (it's expensive and most apps don't care). However, if you do care about the distinction, your clientMessage can take another parameter:

// clientMessage that accepts a tpe to differentiate between messages
// sent as `text` vs those sent as `binary`. Either way, Websocket.zig
// does not validate that text data is valid UTF-8.
clientMessage(h: *Handler, data: []u8, tpe: ws.MessageTextType) !void

Finally, clientMessage can take an optional std.mem.Allocator. If you need to dynamically allocate memory within clientMessage, consider using this allocator. It is a fast thread-local buffer that fallsback to an arena allocator. Allocations made with this allocator are freed after clientMessage returns:

// clientMessage that takes an allocator
clientMessage(h: *Handler, allocator: Allocator, data: []u8) !void

// cilentMessage that takes an allocator AND a MessageTextType
clientMessage(h: *Handler, allocator: Allocator, data: []u8, tpe: ws.MessageTextType) !void`

If clientMessage returns an error, the connection is closed. You can also call conn.close() within the method.

close

If your handler defines a close(handler: *Handler) void method, the method is called whenever the connection is being closed. Guaranteed to be called exactly once, so it is safe to deinitialize the handler at this point. This is called no matter the reason for the closure (on shutdown, if the client closed the connection, if your code close the connection, ...)

The socket may or may not still be alive.

clientClose

If your handler defines a clientClose(handler: *Handler, data: []u8) !void method, the function will be called whenever a close message is received from the client.

You almost certainly do not want to define this method and instead want to use close(). When not defined, websocket.zig follows the websocket specific and replies with its own matching close message.

clientPong

If your handler defines a clientPong(handler: *Handler, data: []u8) !void method, the function will be called whenever a pong message is received from the client. When not defined, no action is taken.

clientPing

If your handler defines a clientPing(handler: *Handler, data: []u8) !void method, the function will be called whenever ping message is received from the client. When not defined, websocket.zig will write a corresponding pong reply.

websocket.Conn

The call to init includes a *websocket.Conn. It is expected that handlers will keep a reference to it. The main purpose of the *Conn is to write data via conn.write([]const u8) and conn.writeBin([]const u8). The websocket protocol differentiates between a "text" and "binary" message, with the only difference that "text" must be valid UTF-8. This library does not enforce this. Which you use really depends on what your client expects. For browsers, text messages appear as strings, and binary messages appear as a Blob or ArrayBuffer (depending on how the client is configured).

conn.close(.{}) can also be called to close the connection. Calling conn.close() will result in the handler's close callback being called.

close takes an optional value where you can specify the code and/or reason: conn.close(.{.code = 4000, .reason = "bye bye"}) Refer to RFC6455 for valid codes. The reason must be <= 123 bytes.

Writer

It's possible to get a *std.Io.Writer from a *Conn. Because websocket messages are framed, the writter will buffer the message in memory and requires an explicit "send". Buffering requires an allocator.

// .text or .binary
var wb = conn.writeBuffer(allocator, .text);
defer wb.deinit();
try wb.interface.print("it's over {d}!!!", .{9
View on GitHub
GitHub Stars481
CategoryDevelopment
Updated1d ago
Forks71

Languages

Zig

Security Score

100/100

Audited on Mar 26, 2026

No findings