Skip to content

Commit d8a593a

Browse files
committed
Allow to set a custom error handler in router configuration.
+ Add an option for a custom error handler and set a complete default one. * Change handler context to allow its initialization without a matched route. * Alphabetical order of HTTPError errors.
1 parent eadd35c commit d8a593a

File tree

6 files changed

+169
-121
lines changed

6 files changed

+169
-121
lines changed

docs/getting_started.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ pub fn main() !void {
9797
});
9898
}
9999
}.handler_fn,
100+
.error_handler = struct {
101+
fn handler_fn(ctx: *Context, _: anyerror) !void {
102+
try ctx.respond(.{
103+
.status = .@"Internal Server Error",
104+
.mime = http.Mime.HTML,
105+
.body = "Oh no, Internal Server Error!",
106+
});
107+
}
108+
}.handler_fn,
100109
});
101110
102111
// This provides the entry function into the Tardy runtime. This will run

examples/basic/main.zig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ pub fn main() !void {
7878
});
7979
}
8080
}.handler_fn,
81+
.error_handler = struct {
82+
fn handler_fn(ctx: *Context, _: anyerror) !void {
83+
try ctx.respond(.{
84+
.status = .@"Internal Server Error",
85+
.mime = http.Mime.HTML,
86+
.body = "Oh no, Internal Server Error!",
87+
});
88+
}
89+
}.handler_fn,
8190
});
8291

8392
// This provides the entry function into the Tardy runtime. This will run

src/http/context.zig

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ const _SSE = @import("sse.zig").SSE;
1818
const Runtime = @import("tardy").Runtime;
1919
const TaskFn = @import("tardy").TaskFn;
2020

21-
const raw_respond = @import("server.zig").raw_respond;
22-
2321
// Context is dependent on the server that gets created.
2422
pub fn Context(comptime Server: type, comptime AppState: type) type {
2523
return struct {
@@ -30,7 +28,7 @@ pub fn Context(comptime Server: type, comptime AppState: type) type {
3028
/// Custom user-data state.
3129
state: AppState,
3230
/// The matched route instance.
33-
route: *const Route(Server, AppState),
31+
route: ?*const Route(Server, AppState),
3432
/// The Request that triggered this handler.
3533
request: *const Request,
3634
/// The Response that will be returned.

src/http/lib.zig

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ pub const Headers = @import("../core/case_string_map.zig").CaseStringMap([]const
99
pub const Server = @import("server.zig").Server;
1010

1111
pub const HTTPError = error{
12-
TooManyHeaders,
1312
ContentTooLarge,
14-
MalformedRequest,
13+
HTTPVersionNotSupported,
1514
InvalidMethod,
15+
LengthRequired,
16+
MalformedRequest,
17+
MethodNotAllowed,
18+
TooManyHeaders,
1619
URITooLong,
17-
HTTPVersionNotSupported,
1820
};

src/http/router.zig

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const std = @import("std");
22
const log = std.log.scoped(.@"zzz/http/router");
33
const assert = std.debug.assert;
44

5+
const HTTPError = @import("lib.zig").HTTPError;
6+
57
const _Route = @import("router/route.zig").Route;
68

79
const Capture = @import("router/routing_trie.zig").Capture;
@@ -13,7 +15,13 @@ const _Context = @import("context.zig").Context;
1315
const _RoutingTrie = @import("router/routing_trie.zig").RoutingTrie;
1416
const QueryMap = @import("router/routing_trie.zig").QueryMap;
1517

16-
/// Default not found handler: send a plain text response.
18+
/// Error handler type.
19+
pub fn ErrorHandlerFn(comptime Server: type, comptime AppState: type) type {
20+
const Context = _Context(Server, AppState);
21+
return *const fn (context: *Context, err: anyerror) anyerror!void;
22+
}
23+
24+
/// Create a default not found handler: send a plain text response.
1725
pub fn default_not_found_handler(comptime Server: type, comptime AppState: type) _Route(Server, AppState).HandlerFn {
1826
const Context = _Context(Server, AppState);
1927

@@ -28,6 +36,87 @@ pub fn default_not_found_handler(comptime Server: type, comptime AppState: type)
2836
}.not_found_handler;
2937
}
3038

39+
/// Create a default error handler: send a plain text response with the error, if known, internal server error otherwise.
40+
pub fn default_error_handler(comptime Server: type, comptime AppState: type) ErrorHandlerFn(Server, AppState) {
41+
const Context = _Context(Server, AppState);
42+
return struct { fn f(ctx: *Context, err: anyerror) !void {
43+
// Handle all default HTTP errors.
44+
switch (err) {
45+
HTTPError.ContentTooLarge => {
46+
try ctx.respond(.{
47+
.status = .@"Content Too Large",
48+
.mime = Mime.TEXT,
49+
.body = "Request was too large.",
50+
});
51+
},
52+
HTTPError.HTTPVersionNotSupported => {
53+
try ctx.respond(.{
54+
.status = .@"HTTP Version Not Supported",
55+
.mime = Mime.HTML,
56+
.body = "HTTP version not supported.",
57+
});
58+
},
59+
HTTPError.InvalidMethod => {
60+
try ctx.respond(.{
61+
.status = .@"Not Implemented",
62+
.mime = Mime.TEXT,
63+
.body = "Not implemented.",
64+
});
65+
},
66+
HTTPError.LengthRequired => {
67+
try ctx.respond(.{
68+
.status = .@"Length Required",
69+
.mime = Mime.TEXT,
70+
.body = "Length required.",
71+
});
72+
},
73+
HTTPError.MalformedRequest => {
74+
try ctx.respond(.{
75+
.status = .@"Bad Request",
76+
.mime = Mime.TEXT,
77+
.body = "Malformed request.",
78+
});
79+
},
80+
HTTPError.MethodNotAllowed => {
81+
if (ctx.route) |route| {
82+
add_allow_header: {
83+
// We also need to add to Allow header.
84+
// This uses the connection's arena to allocate 64 bytes.
85+
const allowed = route.get_allowed(ctx.provision.arena.allocator()) catch break :add_allow_header;
86+
ctx.provision.response.headers.put_assume_capacity("Allow", allowed);
87+
}
88+
}
89+
try ctx.respond(.{
90+
.status = .@"Method Not Allowed",
91+
.mime = Mime.TEXT,
92+
.body = "Method not allowed.",
93+
});
94+
},
95+
HTTPError.TooManyHeaders => {
96+
try ctx.respond(.{
97+
.status = .@"Request Header Fields Too Large",
98+
.mime = Mime.TEXT,
99+
.body = "Too many headers.",
100+
});
101+
},
102+
HTTPError.URITooLong => {
103+
try ctx.respond(.{
104+
.status = .@"URI Too Long",
105+
.mime = Mime.TEXT,
106+
.body = "URI too long.",
107+
});
108+
},
109+
else => {
110+
try ctx.respond(.{
111+
.status = .@"Internal Server Error",
112+
.mime = Mime.TEXT,
113+
.body = "Internal server error.",
114+
});
115+
},
116+
}
117+
} }.f;
118+
}
119+
31120
/// Initialize a router with the given routes.
32121
pub fn Router(comptime Server: type, comptime AppState: type) type {
33122
return struct {
@@ -40,17 +129,20 @@ pub fn Router(comptime Server: type, comptime AppState: type) type {
40129
/// Router configuration structure.
41130
pub const Configuration = struct {
42131
not_found_handler: Route.HandlerFn = default_not_found_handler(Server, AppState),
132+
error_handler: ErrorHandlerFn(Server, AppState) = default_error_handler(Server, AppState),
43133
};
44134

45135
routes: RoutingTrie,
46136
not_found_route: Route,
137+
error_handler: ErrorHandlerFn(Server, AppState),
47138
state: AppState,
48139

49140
pub fn init(state: AppState, comptime _routes: []const Route, comptime configuration: Configuration) Self {
50141
const self = Self{
51142
// Initialize the routing tree from the given routes.
52143
.routes = comptime RoutingTrie.init(_routes),
53144
.not_found_route = comptime Route.init("").all(configuration.not_found_handler),
145+
.error_handler = configuration.error_handler,
54146
.state = state,
55147
};
56148

0 commit comments

Comments
 (0)