diff --git a/CHANGELOG.md b/CHANGELOG.md index afdff93..99c1f80 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ + +## Version 0.8.0 - 2025-12-29 + +Enables keep-alive connections for internal routes, allowing multiple requests over the same connection. Adds a test to verify handling of the second request on an internal route. + +**Changes:** 2 files, 99 lines +**Languages:** C (99 lines) + ## Version 0.7.0 - 2025-12-29 The dashboard now formats numbers and times in metrics for improved readability. diff --git a/src/connection.c b/src/connection.c index be51977..d495201 100755 --- a/src/connection.c +++ b/src/connection.c @@ -1236,7 +1236,19 @@ static void handle_write_event(connection_t *conn) { memset(&conn->request, 0, sizeof(http_request_t)); conn->request.keep_alive = 1; conn->state = CLIENT_STATE_READING_HEADERS; + + conn->content_type_checked = 0; + conn->is_textual_content = 0; + conn->response_headers_parsed = 0; + conn->original_content_length = 0; + conn->content_length_delta = 0; + conn->patch_blocked = 0; + connection_modify_epoll(conn->fd, EPOLLIN); + + if (buffer_available_read(&conn->read_buf) > 0) { + handle_client_read(conn); + } } else { connection_modify_epoll(conn->fd, EPOLLIN); } diff --git a/tests/test_connection.c b/tests/test_connection.c index da4d257..b4b6f1f 100755 --- a/tests/test_connection.c +++ b/tests/test_connection.c @@ -4,6 +4,7 @@ #include "../src/buffer.h" #include "../src/monitor.h" #include "../src/config.h" +#include "../src/http.h" #include #include #include @@ -2136,6 +2137,91 @@ void test_connection_cleanup_active_conn(void) { TEST_SUITE_END(); } +void test_connection_keep_alive_internal_route_second_request(void) { + TEST_SUITE_BEGIN("Keep-Alive Internal Route Second Request"); + + connection_init_all(); + connection_update_cached_time(); + int old_epoll = epoll_fd; + epoll_fd = epoll_create1(0); + TEST_ASSERT(epoll_fd >= 0, "Epoll created"); + + int sockfd[2]; + socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd); + + connection_set_non_blocking(sockfd[0]); + connection_set_non_blocking(sockfd[1]); + + connection_t *conn = &connections[sockfd[0]]; + memset(conn, 0, sizeof(connection_t)); + conn->fd = sockfd[0]; + conn->type = CONN_TYPE_CLIENT; + conn->state = CLIENT_STATE_READING_HEADERS; + buffer_init(&conn->read_buf, 4096); + buffer_init(&conn->write_buf, 4096); + conn->splice_pipe[0] = -1; + conn->splice_pipe[1] = -1; + + struct epoll_event ev = { .events = EPOLLIN | EPOLLOUT, .data.fd = sockfd[0] }; + epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd[0], &ev); + conn->epoll_events = EPOLLIN | EPOLLOUT; + + const char *first_request = "GET /rproxy/dashboard HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n"; + IGNORE_RESULT(write(sockfd[1], first_request, strlen(first_request))); + + struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] }; + connection_handle_event(&event); + TEST_ASSERT(conn->state == CLIENT_STATE_SERVING_INTERNAL || conn->write_buf.tail > 0, "First dashboard request processed"); + + if (conn->state == CLIENT_STATE_SERVING_INTERNAL && conn->request.keep_alive) { + memset(&conn->request, 0, sizeof(http_request_t)); + conn->request.keep_alive = 1; + conn->state = CLIENT_STATE_READING_HEADERS; + conn->content_type_checked = 0; + conn->is_textual_content = 0; + conn->response_headers_parsed = 0; + conn->original_content_length = 0; + conn->content_length_delta = 0; + conn->patch_blocked = 0; + conn->write_buf.head = 0; + conn->write_buf.tail = 0; + } + + const char *second_request = "GET /rproxy/api/stats HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n"; + memcpy(conn->read_buf.data + conn->read_buf.tail, second_request, strlen(second_request)); + conn->read_buf.tail += strlen(second_request); + + TEST_ASSERT(buffer_available_read(&conn->read_buf) > 0, "Second request is in buffer"); + + if (buffer_available_read(&conn->read_buf) > 0 && conn->state == CLIENT_STATE_READING_HEADERS) { + char *data_start = conn->read_buf.data + conn->read_buf.head; + size_t data_len = buffer_available_read(&conn->read_buf); + char *headers_end = memmem(data_start, data_len, "\r\n\r\n", 4); + + if (headers_end) { + size_t headers_len = (headers_end - data_start) + 4; + int parse_result = http_parse_request(data_start, headers_len, &conn->request); + TEST_ASSERT_EQ(1, parse_result, "Second request parsed successfully"); + + int is_internal = (strncmp(conn->request.uri, "/rproxy/dashboard", 17) == 0 || + strncmp(conn->request.uri, "/rproxy/api/stats", 17) == 0); + TEST_ASSERT_EQ(1, is_internal, "Second request detected as internal route"); + TEST_ASSERT_STR_EQ("/rproxy/api/stats", conn->request.uri, "Second request URI correct"); + } + } + + if (conn->type != CONN_TYPE_UNUSED) { + buffer_free(&conn->read_buf); + buffer_free(&conn->write_buf); + } + close(sockfd[0]); + close(sockfd[1]); + close(epoll_fd); + epoll_fd = old_epoll; + + TEST_SUITE_END(); +} + void run_connection_tests(void) { test_connection_init_all(); test_connection_set_non_blocking(); @@ -2198,4 +2284,5 @@ void run_connection_tests(void) { test_connection_handle_write_complete_close(); test_connection_handle_error_state(); test_connection_cleanup_active_conn(); + test_connection_keep_alive_internal_route_second_request(); }