Add custom error handler callback
This commit is contained in:
parent
595872f81c
commit
e739557f8e
3 changed files with 221 additions and 34 deletions
BIN
fjord_server
Executable file
BIN
fjord_server
Executable file
Binary file not shown.
185
fjord_test.odin
185
fjord_test.odin
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
server.odin
60
server.odin
|
|
@ -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) {
|
@(private)
|
||||||
|
spawn_request_thread :: proc(
|
||||||
|
server: ^Server($Error_Type),
|
||||||
|
client_socket: net.TCP_Socket,
|
||||||
|
) {
|
||||||
|
RequestThreadData :: struct($Error_Type: typeid) {
|
||||||
server: ^Server(Error_Type),
|
server: ^Server(Error_Type),
|
||||||
client_socket: net.TCP_Socket,
|
client_socket: net.TCP_Socket,
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
request_thread_task :: proc(t: thread.Task) {
|
||||||
request_thread_task :: proc(t: thread.Task) {
|
task_data := cast(^RequestThreadData(Error_Type))t.data
|
||||||
task_data := cast(^RequestThreadData(any))t.data
|
|
||||||
defer free(task_data, t.allocator)
|
defer free(task_data, t.allocator)
|
||||||
|
|
||||||
serve(task_data.server, task_data.client_socket)
|
serve(task_data.server, task_data.client_socket)
|
||||||
|
|
||||||
net.close(task_data.client_socket)
|
net.close(task_data.client_socket)
|
||||||
// context.temp_allocator is freed automatically on thread death.
|
// context.temp_allocator is freed automatically on thread death.
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
|
||||||
spawn_request_thread :: proc(
|
|
||||||
server: ^Server($Error_Type),
|
|
||||||
client_socket: net.TCP_Socket,
|
|
||||||
) {
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue