Add custom error handler callback

This commit is contained in:
Hugo Mårdbrink 2025-11-14 12:02:14 +01:00
parent 595872f81c
commit e739557f8e
3 changed files with 221 additions and 34 deletions

BIN
fjord_server Executable file

Binary file not shown.

View file

@ -4,6 +4,7 @@ import "core:fmt"
import "core:log" import "core:log"
import "core:net" import "core:net"
import "core:os/os2" import "core:os/os2"
import "core:path/slashpath"
import "core:strings" import "core:strings"
import "core:thread" import "core:thread"
import "core:time" import "core:time"
@ -12,10 +13,26 @@ import "core:testing"
import http "http" import http "http"
Error :: enum { StandardError :: enum {
TestError, TestError,
} }
Error :: union {
StandardError,
}
LOCAL_ADDRESS :: net.IP4_Address{127, 0, 0, 1}
@(private = "file")
port_counter := 50000
@(private = "file")
get_test_port :: proc() -> int {
port := port_counter
port_counter += 10
return port
}
first_handler :: proc(request: ^http.Request) -> (http.Response, Error) { first_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
entity := request.path_variables["entity"] entity := request.path_variables["entity"]
@ -74,8 +91,8 @@ test_router_ok :: proc(t: ^testing.T) {
defer log.destroy_console_logger(context.logger) defer log.destroy_console_logger(context.logger)
endpoint := net.Endpoint { endpoint := net.Endpoint {
address = net.IP4_Address{127, 0, 0, 1}, address = LOCAL_ADDRESS,
port = 8080, port = get_test_port(),
} }
THREAD_COUNT :: 8 THREAD_COUNT :: 8
@ -161,12 +178,21 @@ start_concurrent_server :: proc(
shutdown_concurrent_server :: proc( shutdown_concurrent_server :: proc(
server: ^Server($Error_Type), server: ^Server($Error_Type),
t: ^thread.Thread, t: ^thread.Thread,
endpoint: net.Endpoint,
) { ) {
server_shutdown(server) server_shutdown(server)
// Send a dummy request to unblock accept_tcp, todo: fix this immidiate server shutdown // Send a dummy request to unblock accept_tcp, todo: fix this immidiate server shutdown
url := fmt.tprintf(
"http://%d.%d.%d.%d:%d/dummy",
endpoint.address.(net.IP4_Address)[0],
endpoint.address.(net.IP4_Address)[1],
endpoint.address.(net.IP4_Address)[2],
endpoint.address.(net.IP4_Address)[3],
endpoint.port,
)
_, _, _, err := os2.process_exec( _, _, _, err := os2.process_exec(
{command = []string{"curl", "-s", "http://127.0.0.1:8080/dummy"}}, {command = []string{"curl", "-s", url}},
context.temp_allocator, context.temp_allocator,
) )
assert(err == nil) assert(err == nil)
@ -175,14 +201,30 @@ shutdown_concurrent_server :: proc(
thread.destroy(t) thread.destroy(t)
} }
assert_endpoint_response :: proc(url: string, expected_response: string) { assert_endpoint_response :: proc(
endpoint: net.Endpoint,
path: []string,
expected_response: string,
) {
// Build URL from endpoint and path
path_str := slashpath.join(path, context.temp_allocator)
url := fmt.tprintf(
"http://%d.%d.%d.%d:%d/%s",
endpoint.address.(net.IP4_Address)[0],
endpoint.address.(net.IP4_Address)[1],
endpoint.address.(net.IP4_Address)[2],
endpoint.address.(net.IP4_Address)[3],
endpoint.port,
path_str,
)
state, stdout, stderr, err := os2.process_exec( state, stdout, stderr, err := os2.process_exec(
{command = []string{"curl", "-s", url}}, {command = []string{"curl", "-s", url}},
context.temp_allocator, context.temp_allocator,
) )
if err != nil { if err != nil {
log.errorf("Failed to execute curl: %v", err) log.errorf("Failed to execute curl: %d %s %s", err, stdout, stderr)
assert(false, "curl execution failed") assert(false, "curl execution failed")
return return
} }
@ -207,9 +249,10 @@ test_server_general_ok :: proc(t: ^testing.T) {
context.logger = log.create_console_logger(.Info) context.logger = log.create_console_logger(.Info)
defer log.destroy_console_logger(context.logger) defer log.destroy_console_logger(context.logger)
port := get_test_port()
endpoint := net.Endpoint { endpoint := net.Endpoint {
address = net.IP4_Address{127, 0, 0, 1}, address = LOCAL_ADDRESS,
port = 8080, port = port,
} }
THREAD_COUNT :: 8 THREAD_COUNT :: 8
@ -237,20 +280,138 @@ test_server_general_ok :: proc(t: ^testing.T) {
ITERATIONS :: 100 ITERATIONS :: 100
for i in 0 ..< ITERATIONS { for i in 0 ..< ITERATIONS {
assert_endpoint_response( assert_endpoint_response(
"http://127.0.0.1:8080/hello/world", endpoint,
{"hello", "world"},
"<div>Hello first world</div>", "<div>Hello first world</div>",
) )
assert_endpoint_response( assert_endpoint_response(
"http://127.0.0.1:8080/hello/lonely%20world/only", endpoint,
{"hello", "lonely%20world", "only"},
"<div>Hello second lonely%20world</div>", "<div>Hello second lonely%20world</div>",
) )
assert_endpoint_response( assert_endpoint_response(
"http://127.0.0.1:8080/hello/world/and/worlds%20friend", endpoint,
{"hello", "world", "and", "worlds%20friend"},
"<div>Hello third world and worlds%20friend</div>", "<div>Hello third world and worlds%20friend</div>",
) )
} }
shutdown_concurrent_server(&server, t) shutdown_concurrent_server(&server, t, endpoint)
free_all(context.temp_allocator)
}
@(test)
test_custom_not_found_handler :: proc(t: ^testing.T) {
context.logger = log.create_console_logger(.Info)
defer log.destroy_console_logger(context.logger)
port := get_test_port()
endpoint := net.Endpoint {
address = LOCAL_ADDRESS,
port = port,
}
THREAD_COUNT :: 8
server: Server(Error)
server_init(&server, endpoint, THREAD_COUNT, context.allocator)
defer server_destroy(&server)
custom_not_found_handler :: proc(
request: ^http.Request,
) -> (
http.Response,
Error,
) {
body := "<div>Custom 404</div>"
http_response := http.make_response(
.NotFound,
transmute([]byte)body,
.Html,
)
return http_response, nil
}
server_set_not_found_handler(&server, custom_not_found_handler)
server_add_route(
&server,
.GET,
{"exists"},
proc(request: ^http.Request) -> (http.Response, Error) {
body := "<div>This route exists</div>"
response := http.make_response(
.Ok,
transmute([]byte)body,
.Html,
context.temp_allocator,
)
return response, nil
},
)
t := start_concurrent_server(&server)
assert_endpoint_response(
endpoint,
{"does-not-exist"},
"<div>Custom 404</div>",
)
assert_endpoint_response(
endpoint,
{"exists"},
"<div>This route exists</div>",
)
shutdown_concurrent_server(&server, t, endpoint)
free_all(context.temp_allocator)
}
@(test)
test_custom_error_handler :: proc(t: ^testing.T) {
context.logger = log.create_console_logger(.Info)
defer log.destroy_console_logger(context.logger)
port := get_test_port()
endpoint := net.Endpoint {
address = LOCAL_ADDRESS,
port = port,
}
THREAD_COUNT :: 8
server: Server(Error)
server_init(&server, endpoint, THREAD_COUNT, context.allocator)
defer server_destroy(&server)
custom_error_handler :: proc(error: Error) -> http.Response {
body := "<div>Custom Error Handler</div>"
http_response := http.make_response(
.BadRequest,
transmute([]byte)body,
.Html,
)
return http_response
}
server_set_error_handler(&server, custom_error_handler)
error_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
return http.Response{}, .TestError
}
server_add_route(&server, .GET, {"error"}, error_handler)
t := start_concurrent_server(&server)
assert_endpoint_response(
endpoint,
{"error"},
"<div>Custom Error Handler</div>",
)
shutdown_concurrent_server(&server, t, endpoint)
free_all(context.temp_allocator) free_all(context.temp_allocator)
} }

View file

@ -1,12 +1,15 @@
package fjord package fjord
import "base:intrinsics"
import "base:runtime" import "base:runtime"
import "core:bufio" import "core:bufio"
import "core:bytes" import "core:bytes"
import "core:fmt"
import "core:io" import "core:io"
import "core:log" import "core:log"
import "core:net" import "core:net"
import "core:path/slashpath" import "core:path/slashpath"
import "core:reflect"
import "core:strconv" import "core:strconv"
import "core:strings" import "core:strings"
import "core:sync" import "core:sync"
@ -15,7 +18,10 @@ import "core:unicode"
import http "http" import http "http"
Server :: struct($Error_Type: typeid) { Server :: struct(
$Error_Type: typeid
) where intrinsics.type_is_union(Error_Type)
{
endpoint: net.Endpoint, endpoint: net.Endpoint,
not_found_handler: #type proc( not_found_handler: #type proc(
request: ^http.Request, request: ^http.Request,
@ -23,6 +29,7 @@ Server :: struct($Error_Type: typeid) {
http.Response, http.Response,
Error_Type, Error_Type,
), ),
error_handler: #type proc(error: Error_Type) -> http.Response,
running: bool, running: bool,
router: Router(Error_Type), router: Router(Error_Type),
thread_pool: thread.Pool, thread_pool: thread.Pool,
@ -66,7 +73,7 @@ server_init :: proc(
endpoint: net.Endpoint, endpoint: net.Endpoint,
thread_count: uint, thread_count: uint,
allocator := context.allocator, allocator := context.allocator,
) { ) where intrinsics.type_is_union(Error_Type) {
server^ = Server(Error_Type) { server^ = Server(Error_Type) {
endpoint = endpoint, endpoint = endpoint,
not_found_handler = proc( not_found_handler = proc(
@ -83,6 +90,19 @@ server_init :: proc(
) )
return http_response, nil return http_response, nil
}, },
error_handler = proc(error: Error_Type) -> http.Response {
error_string := fmt.tprint(error)
body := strings.concatenate(
{"<div>Error: ", error_string, "</div>"},
context.temp_allocator,
)
http_response := http.make_response(
.BadRequest,
transmute([]byte)body,
.Html,
)
return http_response
},
thread_count = thread_count, thread_count = thread_count,
running = false, running = false,
allocator = allocator, allocator = allocator,
@ -95,7 +115,7 @@ server_init :: proc(
server_destroy :: proc(server: ^Server($Error_Type)) { server_destroy :: proc(server: ^Server($Error_Type)) {
if sync.atomic_load(&server.thread_pool.is_running) { if sync.atomic_load(&server.thread_pool.is_running) {
thread.pool_join(&server.thread_pool) thread.pool_finish(&server.thread_pool)
} }
thread.pool_destroy(&server.thread_pool) thread.pool_destroy(&server.thread_pool)
@ -115,6 +135,14 @@ server_set_not_found_handler :: proc(
server.not_found_handler = not_found_handler server.not_found_handler = not_found_handler
} }
// This is used instead of default server_make param until compiler bug is fixed: https://github.com/odin-lang/Odin/issues/5792
server_set_error_handler :: proc(
server: ^Server($Error_Type),
error_handler: #type proc(error: Error_Type) -> http.Response,
) {
server.error_handler = error_handler
}
server_add_route :: proc( server_add_route :: proc(
server: ^Server($Error_Type), server: ^Server($Error_Type),
method: http.Method, method: http.Method,
@ -171,9 +199,9 @@ serve :: proc(server: ^Server($Error_Type), client_socket: net.TCP_Socket) {
http_response, handler_err := handler(&http_request) http_response, handler_err := handler(&http_request)
log.info(handler_err)
if handler_err != nil { if handler_err != nil {
log.warnf("Handler failed with error: %s", handler_err) http_response = server.error_handler(handler_err)
return
} }
marshalled_response := http.marshall_response( marshalled_response := http.marshall_response(
@ -183,27 +211,26 @@ serve :: proc(server: ^Server($Error_Type), client_socket: net.TCP_Socket) {
net.send_tcp(client_socket, marshalled_response) net.send_tcp(client_socket, marshalled_response)
} }
RequestThreadData :: struct($Error_Type: typeid) {
server: ^Server(Error_Type),
client_socket: net.TCP_Socket,
}
@(private)
request_thread_task :: proc(t: thread.Task) {
task_data := cast(^RequestThreadData(any))t.data
defer free(task_data, t.allocator)
serve(task_data.server, task_data.client_socket)
net.close(task_data.client_socket)
// context.temp_allocator is freed automatically on thread death.
}
@(private) @(private)
spawn_request_thread :: proc( spawn_request_thread :: proc(
server: ^Server($Error_Type), server: ^Server($Error_Type),
client_socket: net.TCP_Socket, client_socket: net.TCP_Socket,
) { ) {
RequestThreadData :: struct($Error_Type: typeid) {
server: ^Server(Error_Type),
client_socket: net.TCP_Socket,
}
request_thread_task :: proc(t: thread.Task) {
task_data := cast(^RequestThreadData(Error_Type))t.data
defer free(task_data, t.allocator)
serve(task_data.server, task_data.client_socket)
net.close(task_data.client_socket)
// context.temp_allocator is freed automatically on thread death.
}
task_data := new(RequestThreadData(Error_Type)) task_data := new(RequestThreadData(Error_Type))
task_data^ = RequestThreadData(Error_Type) { task_data^ = RequestThreadData(Error_Type) {
server = server, server = server,
@ -237,5 +264,4 @@ server_listen_and_serve :: proc(server: ^Server($Error_Type)) {
} }
thread.pool_finish(&server.thread_pool) thread.pool_finish(&server.thread_pool)
thread.pool_join(&server.thread_pool)
} }