forked from
karlseguin.tngl.sh/http.zig
An HTTP/1.1 server for zig
1const std = @import("std");
2const builtin = @import("builtin");
3
4pub const testing = @import("testing.zig");
5pub const websocket = @import("websocket");
6
7pub const routing = @import("router.zig");
8pub const request = @import("request.zig");
9pub const response = @import("response.zig");
10pub const key_value = @import("key_value.zig");
11pub const middleware = @import("middleware/middleware.zig");
12
13pub const Router = routing.Router;
14pub const Request = request.Request;
15pub const Response = response.Response;
16pub const Url = @import("url.zig").Url;
17pub const Config = @import("config.zig").Config;
18
19const Thread = std.Thread;
20const net = std.net;
21const posix = std.posix;
22const Allocator = std.mem.Allocator;
23const FixedBufferAllocator = std.heap.FixedBufferAllocator;
24
25const log = std.log.scoped(.httpz);
26
27const worker = @import("worker.zig");
28const HTTPConn = worker.HTTPConn;
29
30const build = @import("build");
31const force_blocking: bool = if (@hasDecl(build, "httpz_blocking")) build.httpz_blocking else false;
32
33const MAX_REQUEST_COUNT = std.math.maxInt(usize);
34
35pub fn writeMetrics(writer: anytype) !void {
36 return @import("metrics.zig").write(writer);
37}
38
39pub const Protocol = enum {
40 HTTP10,
41 HTTP11,
42};
43
44pub const Method = enum {
45 GET,
46 HEAD,
47 POST,
48 PUT,
49 PATCH,
50 DELETE,
51 OPTIONS,
52 CONNECT,
53 OTHER,
54};
55
56pub const ContentType = enum {
57 BINARY,
58 CSS,
59 CSV,
60 EOT,
61 EVENTS,
62 GIF,
63 GZ,
64 HTML,
65 ICO,
66 JPG,
67 JS,
68 JSON,
69 OTF,
70 PDF,
71 PNG,
72 SVG,
73 TAR,
74 TEXT,
75 TTF,
76 WASM,
77 WEBP,
78 WOFF,
79 WOFF2,
80 XML,
81 UNKNOWN,
82
83 const asUint = @import("url.zig").asUint;
84
85 pub fn forExtension(ext: []const u8) ContentType {
86 if (ext.len == 0) return .UNKNOWN;
87 const temp = if (ext[0] == '.') ext[1..] else ext;
88 if (temp.len > 5) return .UNKNOWN;
89
90 var normalized: [5]u8 = undefined;
91 for (temp, 0..) |c, i| {
92 normalized[i] = std.ascii.toLower(c);
93 }
94
95 switch (temp.len) {
96 2 => {
97 switch (@as(u16, @bitCast(normalized[0..2].*))) {
98 asUint("js") => return .JS,
99 asUint("gz") => return .GZ,
100 else => return .UNKNOWN,
101 }
102 },
103 3 => {
104 switch (@as(u24, @bitCast(normalized[0..3].*))) {
105 asUint("css") => return .CSS,
106 asUint("csv") => return .CSV,
107 asUint("eot") => return .EOT,
108 asUint("gif") => return .GIF,
109 asUint("htm") => return .HTML,
110 asUint("ico") => return .ICO,
111 asUint("jpg") => return .JPG,
112 asUint("otf") => return .OTF,
113 asUint("pdf") => return .PDF,
114 asUint("png") => return .PNG,
115 asUint("svg") => return .SVG,
116 asUint("tar") => return .TAR,
117 asUint("ttf") => return .TTF,
118 asUint("xml") => return .XML,
119 else => return .UNKNOWN,
120 }
121 },
122 4 => {
123 switch (@as(u32, @bitCast(normalized[0..4].*))) {
124 asUint("jpeg") => return .JPG,
125 asUint("json") => return .JSON,
126 asUint("html") => return .HTML,
127 asUint("text") => return .TEXT,
128 asUint("wasm") => return .WASM,
129 asUint("woff") => return .WOFF,
130 asUint("webp") => return .WEBP,
131 else => return .UNKNOWN,
132 }
133 },
134 5 => {
135 switch (@as(u40, @bitCast(normalized[0..5].*))) {
136 asUint("woff2") => return .WOFF2,
137 else => return .UNKNOWN,
138 }
139 },
140 else => return .UNKNOWN,
141 }
142 return .UNKNOWN;
143 }
144
145 pub fn forFile(file_name: []const u8) ContentType {
146 return forExtension(std.fs.path.extension(file_name));
147 }
148};
149
150// When we initialize our Server(handler: type) with a non-void handler,
151// the ActionContext will either be defined by the handler or it'll be the
152// handler itself. So, for this type, "ActionContext" can be either
153// the Handler or ActionContext from the Server.
154pub fn Action(comptime ActionContext: type) type {
155 if (ActionContext == void) {
156 return *const fn (*Request, *Response) anyerror!void;
157 }
158 return *const fn (ActionContext, *Request, *Response) anyerror!void;
159}
160
161pub fn Dispatcher(comptime Handler: type, comptime ActionArg: type) type {
162 if (Handler == void) {
163 return *const fn (Action(void), *Request, *Response) anyerror!void;
164 }
165 return *const fn (Handler, ActionArg, *Request, *Response) anyerror!void;
166}
167
168pub fn DispatchableAction(comptime Handler: type, comptime ActionArg: type) type {
169 return struct {
170 data: ?*const anyopaque,
171 handler: Handler,
172 action: ActionArg,
173 dispatcher: Dispatcher(Handler, ActionArg),
174 middlewares: []const Middleware(Handler) = &.{},
175 };
176}
177
178pub fn Middleware(comptime H: type) type {
179 return struct {
180 ptr: *anyopaque,
181 deinitFn: *const fn (ptr: *anyopaque) void,
182 executeFn: *const fn (ptr: *anyopaque, req: *Request, res: *Response, executor: *Server(H).Executor) anyerror!void,
183
184 const Self = @This();
185
186 pub fn init(ptr: anytype) Self {
187 const T = @TypeOf(ptr);
188 const ptr_info = @typeInfo(T);
189
190 const gen = struct {
191 pub fn deinit(pointer: *anyopaque) void {
192 const self: T = @ptrCast(@alignCast(pointer));
193 if (std.meta.hasMethod(T, "deinit")) {
194 return ptr_info.pointer.child.deinit(self);
195 }
196 }
197
198 pub fn execute(pointer: *anyopaque, req: *Request, res: *Response, executor: *Server(H).Executor) anyerror!void {
199 const self: T = @ptrCast(@alignCast(pointer));
200 return ptr_info.pointer.child.execute(self, req, res, executor);
201 }
202 };
203
204 return .{
205 .ptr = ptr,
206 .deinitFn = gen.deinit,
207 .executeFn = gen.execute,
208 };
209 }
210
211 pub fn deinit(self: Self) void {
212 self.deinitFn(self.ptr);
213 }
214
215 pub fn execute(self: Self, req: *Request, res: *Response, executor: *Server(H).Executor) !void {
216 return self.executeFn(self.ptr, req, res, executor);
217 }
218 };
219}
220
221// When no WebsocketHandler is specified, we give it a dummy handler just to get
222// the code to compile.
223pub const DummyWebsocketHandler = struct {
224 pub fn clientMessage(_: DummyWebsocketHandler, _: []const u8) !void {}
225};
226
227pub const MiddlewareConfig = struct {
228 arena: Allocator,
229 allocator: Allocator,
230};
231
232pub fn Server(comptime H: type) type {
233 const Handler = switch (@typeInfo(H)) {
234 .@"struct" => H,
235 .pointer => |ptr| ptr.child,
236 .void => void,
237 else => @compileError("Server handler must be a struct, got: " ++ @tagName(@typeInfo(H))),
238 };
239
240 const ActionArg = if (comptime std.meta.hasFn(Handler, "dispatch")) @typeInfo(@TypeOf(Handler.dispatch)).@"fn".params[1].type.? else Action(H);
241
242 const has_websocket = Handler != void and @hasDecl(Handler, "WebsocketHandler");
243 const WebsocketHandler = if (has_websocket) Handler.WebsocketHandler else DummyWebsocketHandler;
244
245 const RouterConfig = struct {
246 middlewares: []const Middleware(H) = &.{},
247 };
248
249 const MiddlewareItem = struct {
250 middleware: Middleware(H),
251 node: std.SinglyLinkedList.Node = .{},
252 };
253
254 return struct {
255 handler: H,
256 config: Config,
257 arena: Allocator,
258 allocator: Allocator,
259 _router: Router(H, ActionArg),
260 _mut: Thread.Mutex,
261 _workers: []Worker,
262 _cond: Thread.Condition,
263 _listener: ?posix.socket_t,
264 _max_request_per_connection: usize,
265 _middlewares: []const Middleware(H),
266 _websocket_state: websocket.server.WorkerState,
267 _middleware_registry: std.SinglyLinkedList,
268
269 const Self = @This();
270 const Worker = if (blockingMode()) worker.Blocking(*Self, WebsocketHandler) else worker.NonBlocking(*Self, WebsocketHandler);
271
272 pub fn init(allocator: Allocator, config: Config, handler: H) !Self {
273 // Be mindful about where we pass this arena. Most things are able to
274 // do dynamic allocation, and need to be able to free when they're
275 // done with their memory. Only use this for stuff that's created on
276 // startup and won't dynamically need to grow/shrink.
277 const arena = try allocator.create(std.heap.ArenaAllocator);
278 errdefer allocator.destroy(arena);
279 arena.* = std.heap.ArenaAllocator.init(allocator);
280 errdefer arena.deinit();
281
282 const default_dispatcher = if (comptime Handler == void) defaultDispatcher else defaultDispatcherWithHandler;
283
284 // do not pass arena.allocator to WorkerState, it needs to be able to
285 // allocate and free at will.
286 const ws_config = config.websocket;
287 var websocket_state = try websocket.server.WorkerState.init(allocator, .{
288 .max_message_size = ws_config.max_message_size,
289 .buffers = .{
290 .small_size = if (has_websocket) ws_config.small_buffer_size else 0,
291 .small_pool = if (has_websocket) ws_config.small_buffer_pool else 0,
292 .large_size = if (has_websocket) ws_config.large_buffer_size else 0,
293 .large_pool = if (has_websocket) ws_config.large_buffer_pool else 0,
294 },
295 // disable handshake memory allocation since httpz is handling
296 // the handshake request directly
297 .handshake = .{
298 .count = 0,
299 .max_size = 0,
300 .max_headers = 0,
301 },
302 .compression = if (ws_config.compression) .{
303 .write_threshold = ws_config.compression_write_treshold,
304 .retain_write_buffer = ws_config.compression_retain_writer,
305 } else null,
306 });
307 errdefer websocket_state.deinit();
308
309 const workers = try arena.allocator().alloc(Worker, config.workerCount());
310
311 return .{
312 .config = config,
313 .handler = handler,
314 .allocator = allocator,
315 .arena = arena.allocator(),
316 ._mut = .{},
317 ._cond = .{},
318 ._workers = workers,
319 ._listener = null,
320 ._middlewares = &.{},
321 ._middleware_registry = .{},
322 ._websocket_state = websocket_state,
323 ._router = try Router(H, ActionArg).init(arena.allocator(), default_dispatcher, handler),
324 ._max_request_per_connection = config.timeout.request_count orelse MAX_REQUEST_COUNT,
325 };
326 }
327
328 pub fn deinit(self: *Self) void {
329 self._websocket_state.deinit();
330
331 var node = self._middleware_registry.first;
332 while (node) |n| {
333 const item: *MiddlewareItem = @fieldParentPtr("node", n);
334 item.middleware.deinit();
335 node = n.next;
336 }
337
338 const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.arena.ptr));
339 arena.deinit();
340 self.allocator.destroy(arena);
341 }
342
343 pub fn listen(self: *Self) !void {
344 // incase "stop" is waiting
345 defer self._cond.signal();
346 self._mut.lock();
347
348 const config = self.config;
349
350 var no_delay = true;
351 const address = blk: {
352 if (config.unix_path) |unix_path| {
353 if (comptime std.net.has_unix_sockets == false) {
354 return error.UnixPathNotSupported;
355 }
356 no_delay = false;
357 std.fs.deleteFileAbsolute(unix_path) catch {};
358 break :blk try net.Address.initUnix(unix_path);
359 } else {
360 const listen_port = config.port orelse 5882;
361 const listen_address = config.address orelse "127.0.0.1";
362 break :blk try net.Address.parseIp(listen_address, listen_port);
363 }
364 };
365
366 const listener = blk: {
367 var sock_flags: u32 = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
368 if (blockingMode() == false) sock_flags |= posix.SOCK.NONBLOCK;
369
370 const proto = if (address.any.family == posix.AF.UNIX) @as(u32, 0) else posix.IPPROTO.TCP;
371 break :blk try posix.socket(address.any.family, sock_flags, proto);
372 };
373
374 if (no_delay) {
375 // TODO: Broken on darwin:
376 // https://github.com/ziglang/zig/issues/17260
377 // if (@hasDecl(os.TCP, "NODELAY")) {
378 // try os.setsockopt(socket.sockfd.?, os.IPPROTO.TCP, os.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1)));
379 // }
380 try posix.setsockopt(listener, posix.IPPROTO.TCP, 1, &std.mem.toBytes(@as(c_int, 1)));
381 }
382
383 try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
384
385 if (config.unix_path == null and self._workers.len > 1) {
386 if (@hasDecl(posix.SO, "REUSEPORT_LB")) {
387 try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEPORT_LB, &std.mem.toBytes(@as(c_int, 1)));
388 } else if (@hasDecl(posix.SO, "REUSEPORT")) {
389 try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEPORT, &std.mem.toBytes(@as(c_int, 1)));
390 }
391 }
392
393 {
394 const socklen = address.getOsSockLen();
395 try posix.bind(listener, &address.any, socklen);
396 try posix.listen(listener, 1024); // kernel backlog
397 }
398
399 self._listener = listener;
400
401 var workers = self._workers;
402 const allocator = self.allocator;
403
404 if (comptime blockingMode()) {
405 workers[0] = try worker.Blocking(*Self, WebsocketHandler).init(allocator, self, &config);
406 defer workers[0].deinit();
407
408 const thrd = try Thread.spawn(.{}, worker.Blocking(*Self, WebsocketHandler).listen, .{ &workers[0], listener });
409
410 // incase listenInNewThread was used and is waiting for us to start
411 self._cond.signal();
412 self._mut.unlock();
413
414 // This will unblock when server.stop() is called and the listening
415 // socket is closed.
416 thrd.join();
417 } else {
418 var started: usize = 0;
419 defer for (0..started) |i| {
420 workers[i].deinit();
421 };
422
423 errdefer for (0..started) |i| {
424 workers[i].stop();
425 };
426
427 var ready_sem = std.Thread.Semaphore{};
428 const threads = try self.arena.alloc(Thread, workers.len);
429 for (0..workers.len) |i| {
430 workers[i] = try Worker.init(allocator, self, &config);
431 errdefer {
432 workers[i].stop();
433 workers[i].deinit();
434 }
435 threads[i] = try Thread.spawn(.{}, Worker.run, .{ &workers[i], listener, &ready_sem });
436 started += 1;
437 }
438
439 for (0..workers.len) |_| {
440 ready_sem.wait();
441 }
442
443 // incase listenInNewThread was used and is waiting for us to start
444 self._cond.signal();
445 self._mut.unlock();
446
447 for (threads) |thrd| {
448 thrd.join();
449 }
450 }
451 }
452
453 pub fn listenInNewThread(self: *Self) !std.Thread {
454 self._mut.lock();
455 defer self._mut.unlock();
456 const thrd = try std.Thread.spawn(.{}, listen, .{self});
457
458 // we don't return until listen() signals us that the server is up
459 self._cond.wait(&self._mut);
460
461 return thrd;
462 }
463
464 pub fn stop(self: *Self) void {
465 self._mut.lock();
466 defer self._mut.unlock();
467
468 for (self._workers) |*w| {
469 w.stop();
470 }
471
472 if (self._listener) |l| {
473 if (comptime blockingMode()) {
474 // necessary to unblock accept on linux
475 // (which might not be that necessary since, on Linux,
476 // NonBlocking should be used)
477 posix.shutdown(l, .recv) catch {};
478 }
479 posix.close(l);
480 }
481 }
482
483 pub fn router(self: *Self, config: RouterConfig) !*Router(H, ActionArg) {
484 // we store this in self for us when no route is found (these will
485 // still be executed).
486
487 const owned = try self.arena.dupe(Middleware(H), config.middlewares);
488 self._middlewares = owned;
489
490 // we store this in router to append to add/append to created routes
491 self._router.middlewares = owned;
492
493 return &self._router;
494 }
495
496 fn defaultDispatcher(action: ActionArg, req: *Request, res: *Response) !void {
497 return action(req, res);
498 }
499
500 fn defaultDispatcherWithHandler(handler: H, action: ActionArg, req: *Request, res: *Response) !void {
501 if (comptime std.meta.hasFn(Handler, "dispatch")) {
502 return handler.dispatch(action, req, res);
503 }
504 return action(handler, req, res);
505 }
506
507 // This is always called from within a threadpool thread. For nonblocking,
508 // notifyingHandler (above) was the threadpool's main entry and it called this.
509 // For blocking, the threadpool was directed to the worker's handleConnection
510 // which eventually called this.
511 // thread_buf is a thread-specific configurable-sized buffer that we're
512 // free to use as we want. This is, by far, the most efficient memory
513 // we can use because it's allocated on server start and re-used on
514 // each request (which is safe, because, in blocking or nonblocking, once
515 // a request reaches this point, processing is blocking from the point
516 // of view of the server).
517 // We'll use thread_buf as part of a FallBackAllocator with the conn
518 // arena for our request ONLY. We cannot use thread_buf for the response
519 // because the response data must outlive the execution of this function
520 // (and thus, in nonblocking, outlives this threadpool's execution unit).
521 pub fn handleRequest(self: *Self, conn: *HTTPConn, thread_buf: []u8) void {
522 const aa = conn.req_arena.allocator();
523
524 var fba = FixedBufferAllocator.init(thread_buf);
525 var fb = FallbackAllocator{
526 .fba = &fba,
527 .fallback = aa,
528 .fixed = fba.allocator(),
529 };
530
531 const allocator = fb.allocator();
532 var req = Request.init(allocator, conn);
533 var res = Response.init(allocator, conn);
534
535 defer std.debug.assert(res.written == true);
536
537 if (comptime std.meta.hasFn(Handler, "handle")) {
538 if (comptime @typeInfo(@TypeOf(Handler.handle)).@"fn".return_type != void) {
539 @compileError(@typeName(Handler) ++ ".handle must return 'void'");
540 }
541 self.handler.handle(&req, &res);
542 } else {
543 const dispatchable_action = self._router.route(req.method, req.method_string, req.url.path, req.params);
544
545 var executor = Executor{
546 .index = 0,
547 .req = &req,
548 .res = &res,
549 .handler = self.handler,
550 .middlewares = undefined,
551 .dispatchable_action = dispatchable_action,
552 };
553
554 if (dispatchable_action) |da| {
555 req.route_data = da.data;
556 executor.middlewares = da.middlewares;
557 } else {
558 req.route_data = null;
559 executor.middlewares = self._middlewares;
560 }
561
562 executor.next() catch |err| {
563 if (comptime std.meta.hasFn(Handler, "uncaughtError")) {
564 self.handler.uncaughtError(&req, &res, err);
565 } else {
566 res.status = 500;
567 res.body = "Internal Server Error";
568 std.log.warn("httpz: unhandled exception for request: {s}\nErr: {}", .{ req.url.raw, err });
569 }
570 };
571 }
572
573 if (conn.handover == .unknown) {
574 // close is the default
575 conn.handover = if (req.canKeepAlive() and conn.request_count < self._max_request_per_connection) .keepalive else .close;
576 }
577
578 res.write() catch {
579 conn.handover = .close;
580 };
581
582 if (req.unread_body > 0 and conn.handover == .keepalive) {
583 drain(&req) catch {
584 conn.handover = .close;
585 };
586 }
587 }
588
589 pub fn middleware(self: *Self, comptime M: type, config: M.Config) !Middleware(H) {
590 const arena = self.arena;
591
592 const node = try arena.create(MiddlewareItem);
593 errdefer arena.destroy(node);
594
595 const m = try arena.create(M);
596 errdefer arena.destroy(m);
597 switch (comptime @typeInfo(@TypeOf(M.init)).@"fn".params.len) {
598 1 => m.* = try M.init(config),
599 2 => m.* = try M.init(config, MiddlewareConfig{
600 .arena = arena,
601 .allocator = self.allocator,
602 }),
603 else => @compileError(@typeName(M) ++ ".init should accept 1 or 2 parameters"),
604 }
605
606 const iface = Middleware(H).init(m);
607 node.*.middleware = iface;
608 self._middleware_registry.prepend(&node.node);
609
610 return iface;
611 }
612
613 pub const Executor = struct {
614 index: usize,
615 req: *Request,
616 res: *Response,
617 handler: H,
618 // pull this out of da since we'll access it a lot (not really, but w/e)
619 middlewares: []const Middleware(H),
620 dispatchable_action: ?*const DispatchableAction(H, ActionArg),
621
622 pub fn next(self: *Executor) !void {
623 const index = self.index;
624 const middlewares = self.middlewares;
625
626 if (index < middlewares.len) {
627 self.index = index + 1;
628 return middlewares[index].execute(self.req, self.res, self);
629 }
630
631 // done executing our middlewares, now we either execute the
632 // dispatcher or not found.
633 if (self.dispatchable_action) |da| {
634 if (comptime H == void) {
635 return da.dispatcher(da.action, self.req, self.res);
636 }
637 return da.dispatcher(da.handler, da.action, self.req, self.res);
638 }
639
640 if (comptime std.meta.hasFn(Handler, "notFound")) {
641 return self.handler.notFound(self.req, self.res);
642 }
643 self.res.status = 404;
644 self.res.body = "Not Found";
645 return;
646 }
647 };
648 };
649}
650
651pub fn blockingMode() bool {
652 if (force_blocking) {
653 return true;
654 }
655 return switch (builtin.os.tag) {
656 .linux, .macos, .ios, .tvos, .watchos, .freebsd, .netbsd, .dragonfly, .openbsd => false,
657 else => true,
658 };
659}
660
661pub fn upgradeWebsocket(comptime H: type, req: *Request, res: *Response, ctx: anytype) !bool {
662 const upgrade = req.header("upgrade") orelse return false;
663 if (std.ascii.eqlIgnoreCase(upgrade, "websocket") == false) {
664 return false;
665 }
666
667 const version = req.header("sec-websocket-version") orelse return false;
668 if (std.ascii.eqlIgnoreCase(version, "13") == false) {
669 return false;
670 }
671
672 // firefox will send multiple values for this header
673 const connection = req.header("connection") orelse return false;
674 if (std.ascii.indexOfIgnoreCase(connection, "upgrade") == null) {
675 return false;
676 }
677
678 const key = req.header("sec-websocket-key") orelse return false;
679
680 const http_conn = res.conn;
681 const ws_worker: *websocket.server.Worker(H) = @ptrCast(@alignCast(http_conn.ws_worker));
682
683 var hc = try ws_worker.createConn(http_conn.stream.handle, http_conn.address, worker.timestamp(0));
684 errdefer ws_worker.cleanupConn(hc);
685
686 hc.handler = try H.init(&hc.conn, ctx);
687
688 var compression = false;
689 if (ws_worker.canCompress()) {
690 if (req.header("sec-websocket-extensions")) |ext| {
691 compression = try websocket.Handshake.parseExtension(ext) != null;
692 }
693 }
694
695 var reply_buf: [512]u8 = undefined;
696 const reply = try websocket.Handshake.createReply(key, null, compression, &reply_buf);
697 try http_conn.stream.writeAll(reply);
698 if (comptime std.meta.hasFn(H, "afterInit")) {
699 const params = @typeInfo(@TypeOf(H.afterInit)).@"fn".params;
700 try if (comptime params.len == 1) hc.handler.?.afterInit() else hc.handler.?.afterInit(ctx);
701 }
702 try ws_worker.setupConnection(hc);
703 res.written = true;
704 http_conn.handover = .{ .websocket = hc };
705 return true;
706}
707
708// std.heap.StackFallbackAllocator is very specific. It's really _stack_ as it
709// requires a comptime size. Also, it uses non-public calls from the FixedBufferAllocator.
710// There should be a more generic FallbackAllocator that just takes 2 allocators...
711// which is what this is.
712const FallbackAllocator = struct {
713 fixed: Allocator,
714 fallback: Allocator,
715 fba: *FixedBufferAllocator,
716
717 pub fn allocator(self: *FallbackAllocator) Allocator {
718 return .{
719 .ptr = self,
720 .vtable = &.{
721 .alloc = alloc,
722 .resize = resize,
723 .free = free,
724 .remap = remap,
725 },
726 };
727 }
728
729 fn alloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ra: usize) ?[*]u8 {
730 const self: *FallbackAllocator = @ptrCast(@alignCast(ctx));
731 return self.fixed.rawAlloc(len, alignment, ra) orelse self.fallback.rawAlloc(len, alignment, ra);
732 }
733
734 fn resize(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) bool {
735 const self: *FallbackAllocator = @ptrCast(@alignCast(ctx));
736 if (self.fba.ownsPtr(buf.ptr)) {
737 if (self.fixed.rawResize(buf, alignment, new_len, ra)) {
738 return true;
739 }
740 }
741 return self.fallback.rawResize(buf, alignment, new_len, ra);
742 }
743
744 fn free(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, ra: usize) void {
745 _ = ctx;
746 _ = buf;
747 _ = alignment;
748 _ = ra;
749 // hack.
750 // Always noop since, in our specific usage, we know fallback is an arena.
751 }
752
753 fn remap(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
754 if (resize(ctx, memory, alignment, new_len, ret_addr)) {
755 return memory.ptr;
756 }
757 return null;
758 }
759};
760
761// Called when we have unread bytes on the request and want to keepalive the
762// connection. Only happens when lazy_read_size is configured and the client
763// didn't read the [whole] body
764// There should already be a receive timeout on the socket since the only
765// way for this to be
766fn drain(req: *Request) !void {
767 var r = try req.reader(2000);
768 var buf: [4096]u8 = undefined;
769 while (true) {
770 if (try r.read(&buf) == 0) {
771 return;
772 }
773 }
774}
775
776const t = @import("t.zig");
777var global_test_allocator = std.heap.GeneralPurposeAllocator(.{}){};
778
779var test_handler_dispatch = TestHandlerDispatch{ .state = 10 };
780var test_handler_disaptch_context = TestHandlerDispatchContext{ .state = 20 };
781var test_handler_default_dispatch1 = TestHandlerDefaultDispatch{ .state = 3 };
782var test_handler_default_dispatch2 = TestHandlerDefaultDispatch{ .state = 99 };
783var test_handler_default_dispatch3 = TestHandlerDefaultDispatch{ .state = 20 };
784
785var default_server: Server(void) = undefined;
786var dispatch_default_server: Server(*TestHandlerDefaultDispatch) = undefined;
787var dispatch_server: Server(*TestHandlerDispatch) = undefined;
788var dispatch_action_context_server: Server(*TestHandlerDispatchContext) = undefined;
789var reuse_server: Server(void) = undefined;
790var handle_server: Server(TestHandlerHandle) = undefined;
791var websocket_server: Server(TestWebsocketHandler) = undefined;
792
793var test_server_threads: [7]Thread = undefined;
794
795test "tests:beforeAll" {
796 // this will leak since the server will run until the process exits. If we use
797 // our testing allocator, it'll report the leak.
798 const ga = global_test_allocator.allocator();
799
800 {
801 default_server = try Server(void).init(ga, .{ .port = 5992, .request = .{
802 .lazy_read_size = 4_096,
803 .max_body_size = 1_048_576,
804 } }, {});
805
806 // only need to do this because we're using listenInNewThread instead
807 // of blocking here. So the array to hold the middleware needs to outlive
808 // this function.
809 var cors = try default_server.arena.alloc(Middleware(void), 1);
810 cors[0] = try default_server.middleware(middleware.Cors, .{
811 .max_age = "300",
812 .methods = "GET,POST",
813 .origin = "httpz.local",
814 .headers = "content-type",
815 });
816
817 var middlewares = try default_server.arena.alloc(Middleware(void), 2);
818 middlewares[0] = try default_server.middleware(TestMiddleware, .{ .id = 100 });
819 middlewares[1] = cors[0];
820
821 var router = try default_server.router(.{});
822 // router.get("/test/ws", testWS);
823 router.get("/fail", TestDummyHandler.fail, .{});
824 router.get("/test/json", TestDummyHandler.jsonRes, .{});
825 router.get("/test/method", TestDummyHandler.method, .{});
826 router.put("/test/method", TestDummyHandler.method, .{});
827 router.method("TEA", "/test/method", TestDummyHandler.method, .{});
828 router.method("PING", "/test/method", TestDummyHandler.method, .{});
829 router.get("/test/query", TestDummyHandler.reqQuery, .{});
830 router.get("/test/stream", TestDummyHandler.eventStream, .{});
831 router.get("/test/streamsync", TestDummyHandler.eventStreamSync, .{});
832 router.get("/test/req_reader", TestDummyHandler.reqReader, .{});
833 router.get("/test/chunked", TestDummyHandler.chunked, .{});
834 router.get("/test/route_data", TestDummyHandler.routeData, .{ .data = &TestDummyHandler.RouteData{ .power = 12345 } });
835 router.all("/test/cors", TestDummyHandler.jsonRes, .{ .middlewares = cors });
836 router.all("/test/middlewares", TestDummyHandler.middlewares, .{ .middlewares = middlewares });
837 router.all("/test/dispatcher", TestDummyHandler.dispatchedAction, .{ .dispatcher = TestDummyHandler.routeSpecificDispacthcer });
838 test_server_threads[0] = try default_server.listenInNewThread();
839 }
840
841 {
842 dispatch_default_server = try Server(*TestHandlerDefaultDispatch).init(ga, .{ .port = 5993 }, &test_handler_default_dispatch1);
843 var router = try dispatch_default_server.router(.{});
844 router.get("/", TestHandlerDefaultDispatch.echo, .{});
845 router.get("/write/*", TestHandlerDefaultDispatch.echoWrite, .{});
846 router.get("/fail", TestHandlerDefaultDispatch.fail, .{});
847 router.post("/login", TestHandlerDefaultDispatch.echo, .{});
848 router.get("/test/body/cl", TestHandlerDefaultDispatch.clBody, .{});
849 router.get("/test/headers", TestHandlerDefaultDispatch.headers, .{});
850 router.all("/api/:version/users/:UserId", TestHandlerDefaultDispatch.params, .{});
851
852 var admin_routes = router.group("/admin/", .{ .dispatcher = TestHandlerDefaultDispatch.dispatch2, .handler = &test_handler_default_dispatch2 });
853 admin_routes.get("/users", TestHandlerDefaultDispatch.echo, .{});
854 admin_routes.put("/users/:id", TestHandlerDefaultDispatch.echo, .{});
855
856 var debug_routes = router.group("/debug", .{ .dispatcher = TestHandlerDefaultDispatch.dispatch3, .handler = &test_handler_default_dispatch3 });
857 debug_routes.head("/ping", TestHandlerDefaultDispatch.echo, .{});
858 debug_routes.options("/stats", TestHandlerDefaultDispatch.echo, .{});
859
860 test_server_threads[1] = try dispatch_default_server.listenInNewThread();
861 }
862
863 {
864 dispatch_server = try Server(*TestHandlerDispatch).init(ga, .{ .port = 5994 }, &test_handler_dispatch);
865 var router = try dispatch_server.router(.{});
866 router.get("/", TestHandlerDispatch.root, .{});
867 test_server_threads[2] = try dispatch_server.listenInNewThread();
868 }
869
870 {
871 dispatch_action_context_server = try Server(*TestHandlerDispatchContext).init(ga, .{ .port = 5995 }, &test_handler_disaptch_context);
872 var router = try dispatch_action_context_server.router(.{});
873 router.get("/", TestHandlerDispatchContext.root, .{});
874 test_server_threads[3] = try dispatch_action_context_server.listenInNewThread();
875 }
876
877 {
878 // with only 1 worker, and a min/max conn of 1, each request should
879 // hit our reset path.
880 reuse_server = try Server(void).init(ga, .{ .port = 5996, .workers = .{ .count = 1, .min_conn = 1, .max_conn = 1 } }, {});
881 var router = try reuse_server.router(.{});
882 router.get("/test/writer", TestDummyHandler.reuseWriter, .{});
883 test_server_threads[4] = try reuse_server.listenInNewThread();
884 }
885
886 {
887 handle_server = try Server(TestHandlerHandle).init(ga, .{ .port = 5997 }, TestHandlerHandle{});
888 test_server_threads[5] = try handle_server.listenInNewThread();
889 }
890
891 {
892 websocket_server = try Server(TestWebsocketHandler).init(ga, .{ .port = 5998 }, TestWebsocketHandler{});
893 var router = try websocket_server.router(.{});
894 router.get("/ws", TestWebsocketHandler.upgrade, .{});
895 test_server_threads[6] = try websocket_server.listenInNewThread();
896 }
897
898 std.testing.refAllDecls(@This());
899}
900
901test "tests:afterAll" {
902 default_server.stop();
903 dispatch_default_server.stop();
904 dispatch_server.stop();
905 dispatch_action_context_server.stop();
906 reuse_server.stop();
907 handle_server.stop();
908 websocket_server.stop();
909
910 for (test_server_threads) |thread| {
911 thread.join();
912 }
913
914 default_server.deinit();
915 dispatch_default_server.deinit();
916 dispatch_server.deinit();
917 dispatch_action_context_server.deinit();
918 reuse_server.deinit();
919 handle_server.deinit();
920 websocket_server.deinit();
921
922 try t.expectEqual(false, global_test_allocator.detectLeaks());
923}
924
925test "httpz: quick shutdown" {
926 var server = try Server(void).init(t.allocator, .{ .port = 6992 }, {});
927 const thrd = try server.listenInNewThread();
928 server.stop();
929 thrd.join();
930 server.deinit();
931}
932
933test "httpz: invalid request" {
934 const stream = testStream(5992);
935 defer stream.close();
936 try stream.writeAll("TEA HTTP/1.1\r\n\r\n");
937
938 var buf: [100]u8 = undefined;
939 try t.expectString("HTTP/1.1 400 \r\nConnection: Close\r\nContent-Length: 15\r\n\r\nInvalid Request", testReadAll(stream, &buf));
940}
941
942test "httpz: invalid request path" {
943 const stream = testStream(5992);
944 defer stream.close();
945 try stream.writeAll("TEA /hello\rn\nWorld:test HTTP/1.1\r\n\r\n");
946
947 var buf: [100]u8 = undefined;
948 try t.expectString("HTTP/1.1 400 \r\nConnection: Close\r\nContent-Length: 15\r\n\r\nInvalid Request", testReadAll(stream, &buf));
949}
950
951test "httpz: invalid header name" {
952 const stream = testStream(5992);
953 defer stream.close();
954 try stream.writeAll("GET / HTTP/1.1\r\nOver: 9000\r\nHel\tlo:World\r\n\r\n");
955
956 var buf: [100]u8 = undefined;
957 try t.expectString("HTTP/1.1 400 \r\nConnection: Close\r\nContent-Length: 15\r\n\r\nInvalid Request", testReadAll(stream, &buf));
958}
959
960test "httpz: invalid content length value (1)" {
961 const stream = testStream(5992);
962 defer stream.close();
963 try stream.writeAll("GET / HTTP/1.1\r\nContent-Length: HaHA\r\n\r\n");
964
965 var buf: [100]u8 = undefined;
966 try t.expectString("HTTP/1.1 400 \r\nConnection: Close\r\nContent-Length: 15\r\n\r\nInvalid Request", testReadAll(stream, &buf));
967}
968
969test "httpz: invalid content length value (2)" {
970 const stream = testStream(5992);
971 defer stream.close();
972 try stream.writeAll("GET / HTTP/1.1\r\nContent-Length: 1.0\r\n\r\n");
973
974 var buf: [100]u8 = undefined;
975 try t.expectString("HTTP/1.1 400 \r\nConnection: Close\r\nContent-Length: 15\r\n\r\nInvalid Request", testReadAll(stream, &buf));
976}
977
978test "httpz: body too big" {
979 const stream = testStream(5993);
980 defer stream.close();
981 try stream.writeAll("POST / HTTP/1.1\r\nContent-Length: 999999999999999999\r\n\r\n");
982
983 var buf: [100]u8 = undefined;
984 try t.expectString("HTTP/1.1 413 \r\nConnection: Close\r\nContent-Length: 23\r\n\r\nRequest body is too big", testReadAll(stream, &buf));
985}
986
987test "httpz: overflow content length" {
988 const stream = testStream(5992);
989 defer stream.close();
990 try stream.writeAll("GET / HTTP/1.1\r\nContent-Length: 999999999999999999999999999\r\n\r\n");
991
992 var buf: [100]u8 = undefined;
993 try t.expectString("HTTP/1.1 400 \r\nConnection: Close\r\nContent-Length: 15\r\n\r\nInvalid Request", testReadAll(stream, &buf));
994}
995
996test "httpz: no route" {
997 const stream = testStream(5992);
998 defer stream.close();
999 try stream.writeAll("GET / HTTP/1.1\r\n\r\n");
1000
1001 var buf: [100]u8 = undefined;
1002 try t.expectString("HTTP/1.1 404 \r\nContent-Length: 9\r\n\r\nNot Found", testReadAll(stream, &buf));
1003}
1004
1005test "httpz: no route with custom notFound handler" {
1006 const stream = testStream(5993);
1007 defer stream.close();
1008 try stream.writeAll("GET /not_found HTTP/1.1\r\n\r\n");
1009
1010 var buf: [100]u8 = undefined;
1011 try t.expectString("HTTP/1.1 404 \r\nstate: 3\r\nContent-Length: 10\r\n\r\nwhere lah?", testReadAll(stream, &buf));
1012}
1013
1014test "httpz: unhandled exception" {
1015 std.testing.log_level = .err;
1016 defer std.testing.log_level = .warn;
1017
1018 const stream = testStream(5992);
1019 defer stream.close();
1020 try stream.writeAll("GET /fail HTTP/1.1\r\n\r\n");
1021
1022 var buf: [150]u8 = undefined;
1023 try t.expectString("HTTP/1.1 500 \r\nContent-Length: 21\r\n\r\nInternal Server Error", testReadAll(stream, &buf));
1024}
1025
1026test "httpz: unhandled exception with custom error handler" {
1027 std.testing.log_level = .err;
1028 defer std.testing.log_level = .warn;
1029
1030 const stream = testStream(5993);
1031 defer stream.close();
1032 try stream.writeAll("GET /fail HTTP/1.1\r\n\r\n");
1033
1034 var buf: [150]u8 = undefined;
1035 try t.expectString("HTTP/1.1 500 \r\nstate: 3\r\nerr: TestUnhandledError\r\nContent-Length: 29\r\n\r\n#/why/arent/tags/hierarchical", testReadAll(stream, &buf));
1036}
1037
1038test "httpz: custom methods" {
1039 const stream = testStream(5992);
1040 defer stream.close();
1041
1042 {
1043 try stream.writeAll("GET /test/method HTTP/1.1\r\n\r\n");
1044 var res = testReadParsed(stream);
1045 defer res.deinit();
1046 try res.expectJson(.{ .method = "GET", .string = "" });
1047 }
1048
1049 {
1050 try stream.writeAll("PUT /test/method HTTP/1.1\r\n\r\n");
1051 var res = testReadParsed(stream);
1052 defer res.deinit();
1053 try res.expectJson(.{ .method = "PUT", .string = "" });
1054 }
1055
1056 {
1057 try stream.writeAll("TEA /test/method HTTP/1.1\r\n\r\n");
1058 var res = testReadParsed(stream);
1059 defer res.deinit();
1060 try res.expectJson(.{ .method = "OTHER", .string = "TEA" });
1061 }
1062
1063 {
1064 try stream.writeAll("PING /test/method HTTP/1.1\r\n\r\n");
1065 var res = testReadParsed(stream);
1066 defer res.deinit();
1067 try res.expectJson(.{ .method = "OTHER", .string = "PING" });
1068 }
1069
1070 {
1071 try stream.writeAll("TEA /test/other HTTP/1.1\r\n\r\n");
1072 var buf: [100]u8 = undefined;
1073 try t.expectString("HTTP/1.1 404 \r\nContent-Length: 9\r\n\r\nNot Found", testReadAll(stream, &buf));
1074 }
1075}
1076
1077test "httpz: route params" {
1078 const stream = testStream(5993);
1079 defer stream.close();
1080 try stream.writeAll("GET /api/v2/users/9001 HTTP/1.1\r\n\r\n");
1081
1082 var buf: [100]u8 = undefined;
1083 try t.expectString("HTTP/1.1 200 \r\nContent-Length: 20\r\n\r\nversion=v2,user=9001", testReadAll(stream, &buf));
1084}
1085
1086test "httpz: request and response headers" {
1087 const stream = testStream(5993);
1088 defer stream.close();
1089 try stream.writeAll("GET /test/headers HTTP/1.1\r\nHeader-Name: Header-Value\r\n\r\n");
1090
1091 var buf: [100]u8 = undefined;
1092 try t.expectString("HTTP/1.1 200 \r\nstate: 3\r\nEcho: Header-Value\r\nother: test-value\r\nContent-Length: 0\r\n\r\n", testReadAll(stream, &buf));
1093}
1094
1095test "httpz: content-length body" {
1096 const stream = testStream(5993);
1097 defer stream.close();
1098 try stream.writeAll("GET /test/body/cl HTTP/1.1\r\nHeader-Name: Header-Value\r\nContent-Length: 4\r\n\r\nabcz");
1099
1100 var buf: [100]u8 = undefined;
1101 try t.expectString("HTTP/1.1 200 \r\nEcho-Body: abcz\r\nContent-Length: 0\r\n\r\n", testReadAll(stream, &buf));
1102}
1103
1104test "httpz: json response" {
1105 const stream = testStream(5992);
1106 defer stream.close();
1107 try stream.writeAll("GET /test/json HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1108
1109 var buf: [200]u8 = undefined;
1110 try t.expectString("HTTP/1.1 201 \r\nContent-Type: application/json; charset=UTF-8\r\nContent-Length: 26\r\n\r\n{\"over\":9000,\"teg\":\"soup\"}", testReadAll(stream, &buf));
1111}
1112
1113test "httpz: query" {
1114 const stream = testStream(5992);
1115 defer stream.close();
1116 try stream.writeAll("GET /test/query?fav=keemun%20te%61%21 HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1117
1118 var buf: [200]u8 = undefined;
1119 try t.expectString("HTTP/1.1 200 \r\nContent-Length: 11\r\n\r\nkeemun tea!", testReadAll(stream, &buf));
1120}
1121
1122test "httpz: chunked" {
1123 const stream = testStream(5992);
1124 defer stream.close();
1125 try stream.writeAll("GET /test/chunked HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1126
1127 var buf: [1000]u8 = undefined;
1128 try t.expectString("HTTP/1.1 200 \r\nOver: 9000!\r\nTransfer-Encoding: chunked\r\n\r\n7\r\nChunk 1\r\n11\r\nand another chunk\r\n0\r\n\r\n", testReadAll(stream, &buf));
1129}
1130
1131test "httpz: route-specific dispatcher" {
1132 const stream = testStream(5992);
1133 defer stream.close();
1134 try stream.writeAll("HEAD /test/dispatcher HTTP/1.1\r\n\r\n");
1135
1136 var buf: [200]u8 = undefined;
1137 try t.expectString("HTTP/1.1 200 \r\ndispatcher: test-dispatcher-1\r\nContent-Length: 6\r\n\r\naction", testReadAll(stream, &buf));
1138}
1139
1140test "httpz: middlewares" {
1141 const stream = testStream(5992);
1142 defer stream.close();
1143
1144 {
1145 try stream.writeAll("GET /test/middlewares HTTP/1.1\r\n\r\n");
1146 var res = testReadParsed(stream);
1147 defer res.deinit();
1148
1149 try res.expectJson(.{ .v1 = "tm1-100", .v2 = "tm2-100" });
1150 try t.expectString("httpz.local", res.headers.get("Access-Control-Allow-Origin").?);
1151 }
1152}
1153
1154test "httpz: CORS" {
1155 const stream = testStream(5992);
1156 defer stream.close();
1157
1158 {
1159 try stream.writeAll("GET /echo HTTP/1.1\r\n\r\n");
1160 var res = testReadParsed(stream);
1161 defer res.deinit();
1162 try t.expectEqual(null, res.headers.get("Access-Control-Max-Age"));
1163 try t.expectEqual(null, res.headers.get("Access-Control-Allow-Methods"));
1164 try t.expectEqual(null, res.headers.get("Access-Control-Allow-Headers"));
1165 try t.expectEqual(null, res.headers.get("Access-Control-Allow-Origin"));
1166 }
1167
1168 {
1169 // cors endpoint but not cors options
1170 try stream.writeAll("OPTIONS /test/cors HTTP/1.1\r\nSec-Fetch-Mode: navigate\r\n\r\n");
1171 var res = testReadParsed(stream);
1172 defer res.deinit();
1173
1174 try t.expectEqual(null, res.headers.get("Access-Control-Max-Age"));
1175 try t.expectEqual(null, res.headers.get("Access-Control-Allow-Methods"));
1176 try t.expectEqual(null, res.headers.get("Access-Control-Allow-Headers"));
1177 try t.expectString("httpz.local", res.headers.get("Access-Control-Allow-Origin").?);
1178 }
1179
1180 {
1181 // cors request
1182 try stream.writeAll("OPTIONS /test/cors HTTP/1.1\r\nSec-Fetch-Mode: cors\r\n\r\n");
1183 var res = testReadParsed(stream);
1184 defer res.deinit();
1185
1186 try t.expectString("300", res.headers.get("Access-Control-Max-Age").?);
1187 try t.expectString("GET,POST", res.headers.get("Access-Control-Allow-Methods").?);
1188 try t.expectString("content-type", res.headers.get("Access-Control-Allow-Headers").?);
1189 try t.expectString("httpz.local", res.headers.get("Access-Control-Allow-Origin").?);
1190 }
1191
1192 {
1193 // cors request, non-options
1194 try stream.writeAll("GET /test/cors HTTP/1.1\r\nSec-Fetch-Mode: cors\r\n\r\n");
1195 var res = testReadParsed(stream);
1196 defer res.deinit();
1197
1198 try t.expectEqual(null, res.headers.get("Access-Control-Max-Age"));
1199 try t.expectEqual(null, res.headers.get("Access-Control-Allow-Methods"));
1200 try t.expectEqual(null, res.headers.get("Access-Control-Allow-Headers"));
1201 try t.expectString("httpz.local", res.headers.get("Access-Control-Allow-Origin").?);
1202 }
1203}
1204
1205test "httpz: router groups" {
1206 const stream = testStream(5993);
1207 defer stream.close();
1208
1209 {
1210 try stream.writeAll("GET / HTTP/1.1\r\n\r\n");
1211 var res = testReadParsed(stream);
1212 defer res.deinit();
1213
1214 try res.expectJson(.{ .state = 3, .method = "GET", .path = "/" });
1215 try t.expectEqual(true, res.headers.get("dispatcher") == null);
1216 }
1217
1218 {
1219 try stream.writeAll("GET /admin/users HTTP/1.1\r\n\r\n");
1220 var res = testReadParsed(stream);
1221 defer res.deinit();
1222
1223 try res.expectJson(.{ .state = 99, .method = "GET", .path = "/admin/users" });
1224 try t.expectString("test-dispatcher-2", res.headers.get("dispatcher").?);
1225 }
1226
1227 {
1228 try stream.writeAll("PUT /admin/users/:id HTTP/1.1\r\n\r\n");
1229 var res = testReadParsed(stream);
1230 defer res.deinit();
1231
1232 try res.expectJson(.{ .state = 99, .method = "PUT", .path = "/admin/users/:id" });
1233 try t.expectString("test-dispatcher-2", res.headers.get("dispatcher").?);
1234 }
1235
1236 {
1237 try stream.writeAll("HEAD /debug/ping HTTP/1.1\r\n\r\n");
1238 var res = testReadParsed(stream);
1239 defer res.deinit();
1240
1241 try res.expectJson(.{ .state = 20, .method = "HEAD", .path = "/debug/ping" });
1242 try t.expectString("test-dispatcher-3", res.headers.get("dispatcher").?);
1243 }
1244
1245 {
1246 try stream.writeAll("OPTIONS /debug/stats HTTP/1.1\r\n\r\n");
1247 var res = testReadParsed(stream);
1248 defer res.deinit();
1249
1250 try res.expectJson(.{ .state = 20, .method = "OPTIONS", .path = "/debug/stats" });
1251 try t.expectString("test-dispatcher-3", res.headers.get("dispatcher").?);
1252 }
1253
1254 {
1255 try stream.writeAll("POST /login HTTP/1.1\r\n\r\n");
1256 var res = testReadParsed(stream);
1257 defer res.deinit();
1258
1259 try res.expectJson(.{ .state = 3, .method = "POST", .path = "/login" });
1260 try t.expectEqual(true, res.headers.get("dispatcher") == null);
1261 }
1262}
1263
1264test "httpz: event stream" {
1265 const stream = testStream(5992);
1266 defer stream.close();
1267 try stream.writeAll("GET /test/stream HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1268
1269 var res = testReadParsed(stream);
1270 defer res.deinit();
1271
1272 try t.expectEqual(818, res.status);
1273 try t.expectEqual(true, res.headers.get("Content-Length") == null);
1274 try t.expectString("text/event-stream; charset=UTF-8", res.headers.get("Content-Type").?);
1275 try t.expectString("no-cache", res.headers.get("Cache-Control").?);
1276 try t.expectString("keep-alive", res.headers.get("Connection").?);
1277 try t.expectString("helloa message", res.body);
1278}
1279
1280test "httpz: event stream sync" {
1281 const stream = testStream(5992);
1282 defer stream.close();
1283 try stream.writeAll("GET /test/streamsync HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1284
1285 var res = testReadParsed(stream);
1286 defer res.deinit();
1287
1288 try t.expectEqual(818, res.status);
1289 try t.expectEqual(true, res.headers.get("Content-Length") == null);
1290 try t.expectString("text/event-stream; charset=UTF-8", res.headers.get("Content-Type").?);
1291 try t.expectString("no-cache", res.headers.get("Cache-Control").?);
1292 try t.expectString("keep-alive", res.headers.get("Connection").?);
1293 try t.expectString("helloa sync message", res.body);
1294}
1295
1296test "httpz: keepalive" {
1297 const stream = testStream(5993);
1298 defer stream.close();
1299 try stream.writeAll("GET /api/v2/users/9001 HTTP/1.1\r\n\r\n");
1300
1301 var buf: [100]u8 = undefined;
1302 try t.expectString("HTTP/1.1 200 \r\nContent-Length: 20\r\n\r\nversion=v2,user=9001", testReadAll(stream, &buf));
1303
1304 try stream.writeAll("GET /api/v2/users/123 HTTP/1.1\r\n\r\n");
1305 try t.expectString("HTTP/1.1 200 \r\nContent-Length: 19\r\n\r\nversion=v2,user=123", testReadAll(stream, &buf));
1306}
1307
1308test "httpz: route data" {
1309 const stream = testStream(5992);
1310 defer stream.close();
1311 try stream.writeAll("GET /test/route_data HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1312
1313 var res = testReadParsed(stream);
1314 defer res.deinit();
1315 try res.expectJson(.{ .power = 12345 });
1316}
1317
1318test "httpz: keepalive with explicit write" {
1319 const stream = testStream(5993);
1320 defer stream.close();
1321 try stream.writeAll("GET /write/9001 HTTP/1.1\r\n\r\n");
1322
1323 var buf: [1000]u8 = undefined;
1324 try t.expectString("HTTP/1.1 200 \r\nContent-Length: 47\r\n\r\n{\"state\":3,\"method\":\"GET\",\"path\":\"/write/9001\"}", testReadAll(stream, &buf));
1325
1326 try stream.writeAll("GET /write/123 HTTP/1.1\r\n\r\n");
1327 try t.expectString("HTTP/1.1 200 \r\nContent-Length: 46\r\n\r\n{\"state\":3,\"method\":\"GET\",\"path\":\"/write/123\"}", testReadAll(stream, &buf));
1328}
1329
1330test "httpz: request in chunks" {
1331 const stream = testStream(5993);
1332 defer stream.close();
1333 try stream.writeAll("GET /api/v2/use");
1334 std.Thread.sleep(std.time.ns_per_ms * 10);
1335 try stream.writeAll("rs/11 HTTP/1.1\r\n\r\n");
1336
1337 var buf: [100]u8 = undefined;
1338 try t.expectString("HTTP/1.1 200 \r\nContent-Length: 18\r\n\r\nversion=v2,user=11", testReadAll(stream, &buf));
1339}
1340
1341test "httpz: writer re-use" {
1342 defer t.reset();
1343
1344 const stream = testStream(5996);
1345 defer stream.close();
1346
1347 var expected: [10]TestUser = undefined;
1348
1349 var buf: [100]u8 = undefined;
1350 for (0..10) |i| {
1351 expected[i] = .{
1352 .id = try std.fmt.allocPrint(t.arena.allocator(), "id-{d}", .{i}),
1353 .power = i,
1354 };
1355 try stream.writeAll(try std.fmt.bufPrint(&buf, "GET /test/writer?count={d} HTTP/1.1\r\nContent-Length: 0\r\n\r\n", .{i + 1}));
1356
1357 var res = testReadParsed(stream);
1358 defer res.deinit();
1359
1360 try res.expectJson(.{ .data = expected[0 .. i + 1] });
1361 }
1362}
1363
1364test "httpz: custom dispatch without action context" {
1365 const stream = testStream(5994);
1366 defer stream.close();
1367 try stream.writeAll("GET / HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1368
1369 var buf: [200]u8 = undefined;
1370 try t.expectString("HTTP/1.1 200 \r\nContent-Type: application/json; charset=UTF-8\r\ndstate: 10\r\ndispatch: TestHandlerDispatch\r\nContent-Length: 12\r\n\r\n{\"state\":10}", testReadAll(stream, &buf));
1371}
1372
1373test "httpz: custom dispatch with action context" {
1374 const stream = testStream(5995);
1375 defer stream.close();
1376 try stream.writeAll("GET /?name=teg HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1377
1378 var buf: [200]u8 = undefined;
1379 try t.expectString("HTTP/1.1 200 \r\nContent-Type: application/json; charset=UTF-8\r\ndstate: 20\r\ndispatch: TestHandlerDispatchContext\r\nContent-Length: 12\r\n\r\n{\"other\":30}", testReadAll(stream, &buf));
1380}
1381
1382test "httpz: custom handle" {
1383 const stream = testStream(5997);
1384 defer stream.close();
1385 try stream.writeAll("GET /whatever?name=teg HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1386
1387 var buf: [100]u8 = undefined;
1388 try t.expectString("HTTP/1.1 200 \r\nContent-Length: 9\r\n\r\nhello teg", testReadAll(stream, &buf));
1389}
1390
1391test "httpz: request body reader" {
1392 {
1393 // no body
1394 const stream = testStream(5992);
1395 defer stream.close();
1396 try stream.writeAll("GET /test/req_reader HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1397
1398 var res = testReadParsed(stream);
1399 defer res.deinit();
1400 try res.expectJson(.{ .length = 0 });
1401 }
1402
1403 {
1404 // small body
1405 const stream = testStream(5992);
1406 defer stream.close();
1407 try stream.writeAll("GET /test/req_reader HTTP/1.1\r\nContent-Length: 4\r\n\r\n123z");
1408
1409 var res = testReadParsed(stream);
1410 defer res.deinit();
1411 try res.expectJson(.{ .length = 4 });
1412 }
1413
1414 var r = t.getRandom();
1415 const random = r.random();
1416
1417 // a bit of fuzzing
1418 for (0..10) |_| {
1419 const stream = testStream(5992);
1420 defer stream.close();
1421 var req: []const u8 = "GET /test/req_reader HTTP/1.1\r\nContent-Length: 20000\r\n\r\n" ++ ("a" ** 20_000);
1422 while (req.len > 0) {
1423 const len = random.uintAtMost(usize, req.len - 1) + 1;
1424 const n = stream.write(req[0..len]) catch |err| switch (err) {
1425 error.WouldBlock => 0,
1426 else => return err,
1427 };
1428 std.Thread.sleep(std.time.ns_per_ms * 2);
1429 req = req[n..];
1430 }
1431
1432 var res = testReadParsed(stream);
1433 defer res.deinit();
1434 try res.expectJson(.{ .length = 20_000 });
1435 }
1436}
1437
1438test "websocket: invalid request" {
1439 const stream = testStream(5998);
1440 defer stream.close();
1441 try stream.writeAll("GET /ws HTTP/1.1\r\nContent-Length: 0\r\n\r\n");
1442
1443 var res = testReadParsed(stream);
1444 defer res.deinit();
1445 try t.expectString("invalid websocket", res.body);
1446}
1447
1448test "websocket: upgrade" {
1449 const stream = testStream(5998);
1450 defer stream.close();
1451 try stream.writeAll("GET /ws HTTP/1.1\r\nContent-Length: 0\r\n");
1452 try stream.writeAll("upgrade: WEBsocket\r\n");
1453 try stream.writeAll("Sec-Websocket-verSIon: 13\r\n");
1454 try stream.writeAll("ConnectioN: abc,upgrade,123\r\n");
1455 try stream.writeAll("SEC-WEBSOCKET-KeY: a-secret-key\r\n\r\n");
1456
1457 var res = testReadHeader(stream);
1458 defer res.deinit();
1459 try t.expectEqual(101, res.status);
1460 try t.expectString("websocket", res.headers.get("Upgrade").?);
1461 try t.expectString("upgrade", res.headers.get("Connection").?);
1462 try t.expectString("55eM2SNGu+68v5XXrr982mhPFkU=", res.headers.get("Sec-Websocket-Accept").?);
1463
1464 try stream.writeAll(&websocket.frameText("over 9000!"));
1465 try stream.writeAll(&websocket.frameText("close"));
1466
1467 var pos: usize = 0;
1468 var buf: [100]u8 = undefined;
1469 var wait_count: usize = 0;
1470 while (pos < 16) {
1471 const n = stream.read(buf[pos..]) catch |err| switch (err) {
1472 error.WouldBlock => {
1473 if (wait_count == 100) {
1474 break;
1475 }
1476 wait_count += 1;
1477 std.Thread.sleep(std.time.ns_per_ms);
1478 continue;
1479 },
1480 else => return err,
1481 };
1482 if (n == 0) {
1483 break;
1484 }
1485 pos += n;
1486 }
1487 try t.expectEqual(16, pos);
1488 try t.expectEqual(129, buf[0]);
1489 try t.expectEqual(10, buf[1]);
1490 try t.expectString("over 9000!", buf[2..12]);
1491 try t.expectString(&.{ 136, 2, 3, 232 }, buf[12..16]);
1492}
1493
1494test "ContentType: forX" {
1495 inline for (@typeInfo(ContentType).@"enum".fields) |field| {
1496 if (comptime std.mem.eql(u8, "BINARY", field.name)) continue;
1497 if (comptime std.mem.eql(u8, "EVENTS", field.name)) continue;
1498 try t.expectEqual(@field(ContentType, field.name), ContentType.forExtension(field.name));
1499 try t.expectEqual(@field(ContentType, field.name), ContentType.forExtension("." ++ field.name));
1500 try t.expectEqual(@field(ContentType, field.name), ContentType.forFile("some_file." ++ field.name));
1501 }
1502 // variations
1503 try t.expectEqual(ContentType.HTML, ContentType.forExtension(".htm"));
1504 try t.expectEqual(ContentType.JPG, ContentType.forExtension(".jpeg"));
1505
1506 try t.expectEqual(ContentType.UNKNOWN, ContentType.forExtension(".spice"));
1507 try t.expectEqual(ContentType.UNKNOWN, ContentType.forExtension(""));
1508 try t.expectEqual(ContentType.UNKNOWN, ContentType.forExtension(".x"));
1509 try t.expectEqual(ContentType.UNKNOWN, ContentType.forFile(""));
1510 try t.expectEqual(ContentType.UNKNOWN, ContentType.forFile("css"));
1511 try t.expectEqual(ContentType.UNKNOWN, ContentType.forFile("css"));
1512 try t.expectEqual(ContentType.UNKNOWN, ContentType.forFile("must.spice"));
1513}
1514
1515fn testStream(port: u16) std.net.Stream {
1516 const timeout = std.mem.toBytes(posix.timeval{
1517 .sec = 0,
1518 .usec = 20_000,
1519 });
1520
1521 const address = std.net.Address.parseIp("127.0.0.1", port) catch unreachable;
1522 const stream = std.net.tcpConnectToAddress(address) catch unreachable;
1523 posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout) catch unreachable;
1524 posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout) catch unreachable;
1525 return stream;
1526}
1527
1528fn testReadAll(stream: std.net.Stream, buf: []u8) []u8 {
1529 var pos: usize = 0;
1530 var blocked = false;
1531 while (true) {
1532 std.debug.assert(pos < buf.len);
1533 const n = stream.read(buf[pos..]) catch |err| switch (err) {
1534 error.WouldBlock => {
1535 if (blocked) return buf[0..pos];
1536 blocked = true;
1537 std.Thread.sleep(std.time.ns_per_ms);
1538 continue;
1539 },
1540 error.ConnectionResetByPeer => return buf[0..pos],
1541 else => @panic(@errorName(err)),
1542 };
1543 if (n == 0) {
1544 return buf[0..pos];
1545 }
1546 pos += n;
1547 blocked = false;
1548 }
1549 unreachable;
1550}
1551
1552fn testReadParsed(stream: std.net.Stream) testing.Testing.Response {
1553 var buf: [4096]u8 = undefined;
1554 const data = testReadAll(stream, &buf);
1555 return testing.parse(data) catch unreachable;
1556}
1557
1558fn testReadHeader(stream: std.net.Stream) testing.Testing.Response {
1559 var pos: usize = 0;
1560 var blocked = false;
1561 var buf: [1024]u8 = undefined;
1562 while (true) {
1563 std.debug.assert(pos < buf.len);
1564 const n = stream.read(buf[pos..]) catch |err| switch (err) {
1565 error.WouldBlock => {
1566 if (blocked) unreachable;
1567 blocked = true;
1568 std.Thread.sleep(std.time.ns_per_ms);
1569 continue;
1570 },
1571 else => @panic(@errorName(err)),
1572 };
1573
1574 if (n == 0) unreachable;
1575
1576 pos += n;
1577 if (std.mem.endsWith(u8, buf[0..pos], "\r\n\r\n")) {
1578 return testing.parse(buf[0..pos]) catch unreachable;
1579 }
1580 blocked = false;
1581 }
1582 unreachable;
1583}
1584
1585const TestUser = struct {
1586 id: []const u8,
1587 power: usize,
1588};
1589
1590// simulates having a void handler, but keeps the test actions organized within
1591// this namespace.
1592const TestDummyHandler = struct {
1593 const RouteData = struct {
1594 power: usize,
1595 };
1596
1597 fn fail(_: *Request, _: *Response) !void {
1598 return error.Failure;
1599 }
1600
1601 fn reqQuery(req: *Request, res: *Response) !void {
1602 res.status = 200;
1603 const query = try req.query();
1604 res.body = query.get("fav").?;
1605 }
1606
1607 fn method(req: *Request, res: *Response) !void {
1608 try res.json(.{ .method = req.method, .string = req.method_string }, .{});
1609 }
1610
1611 fn chunked(_: *Request, res: *Response) !void {
1612 res.header("Over", "9000!");
1613 res.status = 200;
1614 try res.chunk("Chunk 1");
1615 try res.chunk("and another chunk");
1616 }
1617
1618 fn jsonRes(_: *Request, res: *Response) !void {
1619 res.setStatus(.created);
1620 try res.json(.{ .over = 9000, .teg = "soup" }, .{});
1621 }
1622
1623 fn routeData(req: *Request, res: *Response) !void {
1624 const rd: *const RouteData = @ptrCast(@alignCast(req.route_data.?));
1625 try res.json(.{ .power = rd.power }, .{});
1626 }
1627
1628 fn eventStream(_: *Request, res: *Response) !void {
1629 res.status = 818;
1630 try res.startEventStream(StreamContext{ .data = "hello" }, StreamContext.handle);
1631 }
1632
1633 fn eventStreamSync(_: *Request, res: *Response) !void {
1634 res.status = 818;
1635 const stream = try res.startEventStreamSync();
1636 var w = stream.writer(&.{});
1637 w.interface.writeAll("hello") catch unreachable;
1638 w.interface.writeAll("a sync message") catch unreachable;
1639 }
1640
1641 fn reqReader(req: *Request, res: *Response) !void {
1642 var reader = try req.reader(2000);
1643
1644 var l: usize = 0;
1645 var buf: [1024]u8 = undefined;
1646 while (true) {
1647 const n = try reader.read(&buf);
1648 if (n == 0) {
1649 break;
1650 }
1651 if (req.body_len > 10 and std.mem.indexOfNonePos(u8, buf[0..n], 0, "a") != null) {
1652 return error.InvalidData;
1653 }
1654 l += n;
1655 }
1656 return res.json(.{ .length = l }, .{});
1657 }
1658
1659 const StreamContext = struct {
1660 data: []const u8,
1661
1662 fn handle(self: StreamContext, stream: std.net.Stream) void {
1663 stream.writeAll(self.data) catch unreachable;
1664 stream.writeAll("a message") catch unreachable;
1665 }
1666 };
1667
1668 fn routeSpecificDispacthcer(action: Action(void), req: *Request, res: *Response) !void {
1669 res.header("dispatcher", "test-dispatcher-1");
1670 return action(req, res);
1671 }
1672
1673 fn dispatchedAction(_: *Request, res: *Response) !void {
1674 var writer = res.writer();
1675 return writer.interface.writeAll("action");
1676 }
1677
1678 fn middlewares(req: *Request, res: *Response) !void {
1679 return res.json(.{
1680 .v1 = TestMiddleware.value1(req),
1681 .v2 = TestMiddleware.value2(req),
1682 }, .{});
1683 }
1684
1685 // called by the re-use server, but put here because, like the default server
1686 // this is a handler-less server
1687 fn reuseWriter(req: *Request, res: *Response) !void {
1688 res.status = 200;
1689 const query = try req.query();
1690 const count = try std.fmt.parseInt(u16, query.get("count").?, 10);
1691
1692 var data = try res.arena.alloc(TestUser, count);
1693 for (0..count) |i| {
1694 data[i] = .{
1695 .id = try std.fmt.allocPrint(res.arena, "id-{d}", .{i}),
1696 .power = i,
1697 };
1698 }
1699 return res.json(.{ .data = data }, .{});
1700 }
1701};
1702
1703const TestHandlerDefaultDispatch = struct {
1704 state: usize,
1705
1706 fn dispatch2(h: *TestHandlerDefaultDispatch, action: Action(*TestHandlerDefaultDispatch), req: *Request, res: *Response) !void {
1707 res.header("dispatcher", "test-dispatcher-2");
1708 return action(h, req, res);
1709 }
1710
1711 fn dispatch3(h: *TestHandlerDefaultDispatch, action: Action(*TestHandlerDefaultDispatch), req: *Request, res: *Response) !void {
1712 res.header("dispatcher", "test-dispatcher-3");
1713 return action(h, req, res);
1714 }
1715
1716 fn echo(h: *TestHandlerDefaultDispatch, req: *Request, res: *Response) !void {
1717 return res.json(.{
1718 .state = h.state,
1719 .method = @tagName(req.method),
1720 .path = req.url.path,
1721 }, .{});
1722 }
1723
1724 fn echoWrite(h: *TestHandlerDefaultDispatch, req: *Request, res: *Response) !void {
1725 const json_writer = std.json.fmt(.{
1726 .state = h.state,
1727 .method = @tagName(req.method),
1728 .path = req.url.path,
1729 }, .{});
1730
1731 var aw: std.io.Writer.Allocating = .init(res.arena);
1732 try json_writer.format(&aw.writer);
1733
1734 res.body = aw.written();
1735 return res.write();
1736 }
1737
1738 fn params(_: *TestHandlerDefaultDispatch, req: *Request, res: *Response) !void {
1739 const args = .{ req.param("version").?, req.param("UserId").? };
1740 res.body = try std.fmt.allocPrint(req.arena, "version={s},user={s}", args);
1741 }
1742
1743 fn headers(h: *TestHandlerDefaultDispatch, req: *Request, res: *Response) !void {
1744 res.header("state", try std.fmt.allocPrint(res.arena, "{d}", .{h.state}));
1745 res.header("Echo", req.header("header-name").?);
1746 res.header("other", "test-value");
1747 }
1748
1749 fn clBody(_: *TestHandlerDefaultDispatch, req: *Request, res: *Response) !void {
1750 res.header("Echo-Body", req.body().?);
1751 }
1752
1753 fn fail(_: *TestHandlerDefaultDispatch, _: *Request, _: *Response) !void {
1754 return error.TestUnhandledError;
1755 }
1756
1757 pub fn notFound(h: *TestHandlerDefaultDispatch, _: *Request, res: *Response) !void {
1758 res.status = 404;
1759 res.header("state", try std.fmt.allocPrint(res.arena, "{d}", .{h.state}));
1760 res.body = "where lah?";
1761 }
1762
1763 pub fn uncaughtError(h: *TestHandlerDefaultDispatch, _: *Request, res: *Response, err: anyerror) void {
1764 res.status = 500;
1765 res.header("state", std.fmt.allocPrint(res.arena, "{d}", .{h.state}) catch unreachable);
1766 res.header("err", @errorName(err));
1767 res.body = "#/why/arent/tags/hierarchical";
1768 }
1769};
1770
1771const TestHandlerDispatch = struct {
1772 state: usize,
1773
1774 pub fn dispatch(self: *TestHandlerDispatch, action: Action(*TestHandlerDispatch), req: *Request, res: *Response) !void {
1775 res.header("dstate", try std.fmt.allocPrint(res.arena, "{d}", .{self.state}));
1776 res.header("dispatch", "TestHandlerDispatch");
1777 return action(self, req, res);
1778 }
1779
1780 fn root(h: *TestHandlerDispatch, _: *Request, res: *Response) !void {
1781 return res.json(.{ .state = h.state }, .{});
1782 }
1783};
1784
1785const TestHandlerDispatchContext = struct {
1786 state: usize,
1787
1788 const ActionContext = struct {
1789 other: usize,
1790 };
1791
1792 pub fn dispatch(self: *TestHandlerDispatchContext, action: Action(*ActionContext), req: *Request, res: *Response) !void {
1793 res.header("dstate", try std.fmt.allocPrint(res.arena, "{d}", .{self.state}));
1794 res.header("dispatch", "TestHandlerDispatchContext");
1795 var action_context = ActionContext{ .other = self.state + 10 };
1796 return action(&action_context, req, res);
1797 }
1798
1799 pub fn root(a: *const ActionContext, _: *Request, res: *Response) !void {
1800 return res.json(.{ .other = a.other }, .{});
1801 }
1802};
1803
1804const TestHandlerHandle = struct {
1805 pub fn handle(_: TestHandlerHandle, req: *Request, res: *Response) void {
1806 const query = req.query() catch unreachable;
1807 var writer = res.writer();
1808 writer.interface.print("hello {s}", .{query.get("name") orelse "world"}) catch unreachable;
1809 }
1810};
1811
1812const TestWebsocketHandler = struct {
1813 pub const WebsocketHandler = struct {
1814 ctx: u32,
1815 conn: *websocket.Conn,
1816
1817 pub fn init(conn: *websocket.Conn, ctx: u32) !WebsocketHandler {
1818 return .{
1819 .ctx = ctx,
1820 .conn = conn,
1821 };
1822 }
1823
1824 pub fn afterInit(self: *WebsocketHandler, ctx: u32) !void {
1825 try t.expectEqual(self.ctx, ctx);
1826 }
1827
1828 pub fn clientMessage(self: *WebsocketHandler, data: []const u8) !void {
1829 if (std.mem.eql(u8, data, "close")) {
1830 self.conn.close(.{}) catch {};
1831 return;
1832 }
1833 try self.conn.write(data);
1834 }
1835 };
1836
1837 pub fn upgrade(_: TestWebsocketHandler, req: *Request, res: *Response) !void {
1838 if (try upgradeWebsocket(WebsocketHandler, req, res, 9001) == false) {
1839 res.status = 500;
1840 res.body = "invalid websocket";
1841 }
1842 }
1843};
1844
1845const TestMiddleware = struct {
1846 const Config = struct {
1847 id: i32,
1848 };
1849
1850 allocator: Allocator,
1851 v1: []const u8,
1852 v2: []const u8,
1853
1854 fn init(config: TestMiddleware.Config, mc: MiddlewareConfig) !TestMiddleware {
1855 return .{
1856 .allocator = mc.allocator,
1857 .v1 = try std.fmt.allocPrint(mc.arena, "tm1-{d}", .{config.id}),
1858 .v2 = try std.fmt.allocPrint(mc.allocator, "tm2-{d}", .{config.id}),
1859 };
1860 }
1861
1862 pub fn deinit(self: *const TestMiddleware) void {
1863 self.allocator.free(self.v2);
1864 }
1865
1866 fn value1(req: *const Request) []const u8 {
1867 const v: [*]u8 = @ptrCast(req.middlewares.get("text_middleware_1").?);
1868 return v[0..7];
1869 }
1870
1871 fn value2(req: *const Request) []const u8 {
1872 const v: [*]u8 = @ptrCast(req.middlewares.get("text_middleware_2").?);
1873 return v[0..7];
1874 }
1875
1876 fn execute(self: *const TestMiddleware, req: *Request, _: *Response, executor: anytype) !void {
1877 try req.middlewares.put("text_middleware_1", (try req.arena.dupe(u8, self.v1)).ptr);
1878 try req.middlewares.put("text_middleware_2", (try req.arena.dupe(u8, self.v2)).ptr);
1879 return executor.next();
1880 }
1881};