diff --git a/fjord_server b/fjord_server new file mode 100755 index 0000000..7aa80a3 Binary files /dev/null and b/fjord_server differ diff --git a/fjord_test.odin b/fjord_test.odin index 597b5e7..20dfa94 100644 --- a/fjord_test.odin +++ b/fjord_test.odin @@ -4,6 +4,7 @@ import "core:fmt" import "core:log" import "core:net" import "core:os/os2" +import "core:path/slashpath" import "core:strings" import "core:thread" import "core:time" @@ -12,10 +13,26 @@ import "core:testing" import http "http" -Error :: enum { +StandardError :: enum { 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) { entity := request.path_variables["entity"] @@ -74,8 +91,8 @@ test_router_ok :: proc(t: ^testing.T) { defer log.destroy_console_logger(context.logger) endpoint := net.Endpoint { - address = net.IP4_Address{127, 0, 0, 1}, - port = 8080, + address = LOCAL_ADDRESS, + port = get_test_port(), } THREAD_COUNT :: 8 @@ -161,12 +178,21 @@ start_concurrent_server :: proc( shutdown_concurrent_server :: proc( server: ^Server($Error_Type), t: ^thread.Thread, + endpoint: net.Endpoint, ) { server_shutdown(server) // 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( - {command = []string{"curl", "-s", "http://127.0.0.1:8080/dummy"}}, + {command = []string{"curl", "-s", url}}, context.temp_allocator, ) assert(err == nil) @@ -175,14 +201,30 @@ shutdown_concurrent_server :: proc( 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( {command = []string{"curl", "-s", url}}, context.temp_allocator, ) 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") return } @@ -207,9 +249,10 @@ test_server_general_ok :: 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 = net.IP4_Address{127, 0, 0, 1}, - port = 8080, + address = LOCAL_ADDRESS, + port = port, } THREAD_COUNT :: 8 @@ -237,20 +280,138 @@ test_server_general_ok :: proc(t: ^testing.T) { ITERATIONS :: 100 for i in 0 ..< ITERATIONS { assert_endpoint_response( - "http://127.0.0.1:8080/hello/world", + endpoint, + {"hello", "world"}, "
Hello first world
", ) assert_endpoint_response( - "http://127.0.0.1:8080/hello/lonely%20world/only", + endpoint, + {"hello", "lonely%20world", "only"}, "
Hello second lonely%20world
", ) assert_endpoint_response( - "http://127.0.0.1:8080/hello/world/and/worlds%20friend", + endpoint, + {"hello", "world", "and", "worlds%20friend"}, "
Hello third world and worlds%20friend
", ) } - 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 := "
Custom 404
" + 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 := "
This route exists
" + 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"}, + "
Custom 404
", + ) + + assert_endpoint_response( + endpoint, + {"exists"}, + "
This route exists
", + ) + + 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 := "
Custom Error Handler
" + 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"}, + "
Custom Error Handler
", + ) + + shutdown_concurrent_server(&server, t, endpoint) free_all(context.temp_allocator) } diff --git a/server.odin b/server.odin index 374c726..aa964ac 100644 --- a/server.odin +++ b/server.odin @@ -1,12 +1,15 @@ package fjord +import "base:intrinsics" import "base:runtime" import "core:bufio" import "core:bytes" +import "core:fmt" import "core:io" import "core:log" import "core:net" import "core:path/slashpath" +import "core:reflect" import "core:strconv" import "core:strings" import "core:sync" @@ -15,7 +18,10 @@ import "core:unicode" import http "http" -Server :: struct($Error_Type: typeid) { +Server :: struct( + $Error_Type: typeid +) where intrinsics.type_is_union(Error_Type) +{ endpoint: net.Endpoint, not_found_handler: #type proc( request: ^http.Request, @@ -23,6 +29,7 @@ Server :: struct($Error_Type: typeid) { http.Response, Error_Type, ), + error_handler: #type proc(error: Error_Type) -> http.Response, running: bool, router: Router(Error_Type), thread_pool: thread.Pool, @@ -66,7 +73,7 @@ server_init :: proc( endpoint: net.Endpoint, thread_count: uint, allocator := context.allocator, -) { +) where intrinsics.type_is_union(Error_Type) { server^ = Server(Error_Type) { endpoint = endpoint, not_found_handler = proc( @@ -83,6 +90,19 @@ server_init :: proc( ) return http_response, nil }, + error_handler = proc(error: Error_Type) -> http.Response { + error_string := fmt.tprint(error) + body := strings.concatenate( + {"
Error: ", error_string, "
"}, + context.temp_allocator, + ) + http_response := http.make_response( + .BadRequest, + transmute([]byte)body, + .Html, + ) + return http_response + }, thread_count = thread_count, running = false, allocator = allocator, @@ -95,7 +115,7 @@ server_init :: proc( server_destroy :: proc(server: ^Server($Error_Type)) { 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) @@ -115,6 +135,14 @@ server_set_not_found_handler :: proc( 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: ^Server($Error_Type), 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) + log.info(handler_err) if handler_err != nil { - log.warnf("Handler failed with error: %s", handler_err) - return + http_response = server.error_handler(handler_err) } 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) } -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) spawn_request_thread :: proc( server: ^Server($Error_Type), 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^ = RequestThreadData(Error_Type) { server = server, @@ -237,5 +264,4 @@ server_listen_and_serve :: proc(server: ^Server($Error_Type)) { } thread.pool_finish(&server.thread_pool) - thread.pool_join(&server.thread_pool) }