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)
}