From e4562bc3959d807be735817bf54313f5252ab8b5 Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 9 Oct 2026 09:25:14 +0800 Subject: [PATCH 1/4] =?UTF-8?q?refactor(api):=20=E5=8E=9F=E5=AD=90?= =?UTF-8?q?=E6=9E=84=E9=80=A0=20+=20=E6=89=80=E6=9C=89=E6=9D=83=E8=BF=9B?= =?UTF-8?q?=E5=90=8D=E5=AD=97=20=E2=80=94=E2=80=94=20=E6=B6=88=E9=99=A4=20?= =?UTF-8?q?5=20=E6=AD=A5=E6=9E=84=E9=80=A0=E4=B8=8E=E5=AE=98=E6=96=B9?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E7=9A=84=20double-free?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按"原子性 = API 不可再分割,正交性 = API 没有重合"改造公共 API(v2.10 破坏性变更,不留兼容层)。本 PR 处理 N1(构造不可分割)、O1(读取 API 重合)、O3(构造入口三重叠)。 ## 新增:uvhttp_server_listen_routes —— 原子的构造入口 此前"起一个带路由的监听服务器"需要 5 步,且没有原子版本: server_new -> router_new -> add_route xN -> set_router -> listen 调用方被迫为每一步手写回滚。现在一次调用完成: const uvhttp_route_t routes[] = { {"/", UVHTTP_ANY, hello}, {"/health", UVHTTP_GET, health}, }; uvhttp_server_listen_routes(loop, routes, 2, "0.0.0.0", 8080, &server); 要么返回一个完全就绪、路由齐全的监听中的 server,要么什么都没创建 (*server 在所有失败路径上为 NULL)。无中间态,调用方无回滚可写。 ## 修复:官方嵌入示例的 double-free(ASan 可复现) examples/embedding/main.c 的 listen 失败分支: uvhttp_router_free(router); // router 已被 take_router 交给 server uvhttp_server_free(server); // 内部再释放一次 -> 二次释放 根因是命名与语义不符:`set_router` 听起来是借用,实际是所有权转移, 而这个事实只写在 @note 里。调用方照着最直觉的写法就踩中。 ## 重命名:所有权进函数名 - uvhttp_server_set_router -> uvhttp_server_take_router - uvhttp_server_set_context -> uvhttp_server_take_context "take" 明确表达所有权转移。同时这两个函数原先会静默覆盖已有的指针 (旧 router 泄漏),现在返回 INVALID_PARAM 拒绝;重复 take 同一个对象 仍为幂等,便于 teardown 路径复用。 ## 删除:请求读取 API 的纯别名(O1) 三个函数体各只有一行转发,且声明在 server.h 里却不涉及 server: uvhttp_get_header(req, name) -> uvhttp_request_get_header(req, name) uvhttp_get_param(req, name) -> uvhttp_request_get_query_param(...) // 名字有歧义 uvhttp_get_body(req) -> uvhttp_request_get_body(req) 同一能力两个入口,无依据可选。其中 uvhttp_get_param 实为 query param, 与路由参数 :id 混淆。保留 request.h 的正规名,测试改调之。 ## 顺带:uvhttp_method_t 移到 uvhttp_common.h server.h 需要它但不能 include request.h(循环)。原先靠前向声明不可行 (需要完整 enum 定义才能作为 struct 字段),故移到两个头文件共同依赖的 common.h。类型本身未变。 ## 修复:另外 3 处同类 double-free 同一模式在 examples 与文档示例中重复出现,均已修正(router 交给 server 后不再单独释放): - examples/custom_compression_strategy.c - examples/06_advanced/sse_server.c(正常退出路径) - examples/06_advanced/context_injection.c - examples/08_e2e_tests/test_static_simple_e2e.c(2 处) - docs/guide/EMBEDDING_GUIDE.md + docs/zh/(英中双语) ## 测试 新增 test/unit/test_server_atomic_construct.cpp(14 个): - 构造成功路径(3 路由 / 0 路由) - 6 条失败路径逐一断言 *server 保持 NULL(含路由表内 NULL path / NULL handler —— 旧 5 步版本要调用方自己处理的情况) - 端口 1 绑定失败 = 官方示例崩溃的那条路径,ASan 下无二次释放 - 所有权:二次 take 被拒、同一对象重复 take 幂等、take_context 同理 验证: - ctest 120/120 通过(新增 14 个) - 新测试在 ASan 下 14/14 通过,无泄漏报告 - 文档示例(first-server.md / EMBEDDING_GUIDE.md)提取出来实编实跑: -Wall 无警告,实测返回 HTTP/1.1 200 ## 未在本 PR 处理的 按 brain 审计的优先级,以下留待后续: - P0 配置静默失效:uvhttp_config_t 26 字段中 14 个从不被执行路径读取 - P0 router 迁移致路由全丢:13+ 顶层路由 + 参数路由 -> 已注册路由全丢 - O2 两个 cache API 逐字同构,应合并为一个抽象 - O4 free/cleanup/simple_free 三组同义命名 - N2 发送响应 4~5 步且无原子版本 --- benchmark/benchmark_unified.c | 2 +- docs/api/API_REFERENCE.md | 2 +- docs/api/PROTOCOL_UPGRADE_API.md | 2 +- docs/api/introduction.md | 2 +- docs/guide/EMBEDDING_GUIDE.md | 35 +-- docs/guide/INSTALL_CMAKE.md | 2 +- docs/guide/LIBUV_DATA_POINTER.md | 8 +- docs/guide/TUTORIAL.md | 34 +-- docs/guide/first-server.md | 84 ++---- docs/guide/getting-started.md | 2 +- docs/guide/introduction.md | 2 +- docs/guide/websocket.md | 2 +- docs/spec/server-api.md | 4 +- docs/zh/api/introduction.md | 2 +- docs/zh/guide/EMBEDDING_GUIDE.md | 33 +-- docs/zh/guide/INSTALL_CMAKE.md | 2 +- docs/zh/guide/LIBUV_DATA_POINTER.md | 8 +- docs/zh/guide/STATIC_FILE_SERVER.md | 2 +- docs/zh/guide/TUTORIAL.md | 34 +-- docs/zh/guide/first-server.md | 75 ++---- docs/zh/guide/getting-started.md | 2 +- docs/zh/guide/introduction.md | 2 +- docs/zh/guide/websocket.md | 2 +- examples/01_basics/01_hello_world.c | 2 +- examples/01_basics/helloworld.c | 6 +- examples/01_basics/helloworld_complete.c | 6 +- examples/01_basics/quick_api_demo.c | 2 +- examples/01_basics/signal_handling_demo.c | 2 +- examples/01_basics/simple_api_demo.c | 2 +- examples/02_routing/01_simple_routing.c | 2 +- examples/02_routing/02_method_routing.c | 2 +- examples/03_middleware/logging_middleware.c | 2 +- .../03_middleware/middleware_chain_demo.c | 2 +- .../middleware_compile_time_demo.c | 2 +- examples/03_middleware/rate_limit_demo.c | 2 +- examples/03_middleware/test_middleware.c | 2 +- examples/06_advanced/api_demo.c | 2 +- examples/06_advanced/config_demo.c | 4 +- examples/06_advanced/context_injection.c | 8 +- .../06_advanced/protocol_upgrade_example.c | 2 +- examples/06_advanced/sse_server.c | 4 +- examples/07_performance/compression_server.c | 2 +- .../07_performance/performance_https_server.c | 2 +- .../08_e2e_tests/test_static_simple_e2e.c | 6 +- examples/custom_compression_strategy.c | 5 +- examples/embedding/main.c | 42 ++- examples/simple_server.c | 2 +- include/uvhttp_common.h | 16 ++ include/uvhttp_request.h | 14 +- include/uvhttp_server.h | 100 +++++-- manual/test_tls_e2e.c | 2 +- src/uvhttp_server.c | 125 +++++++-- test/unit/test_boundary_null_safety.cpp | 2 +- .../test_connection_integration_coverage.cpp | 2 +- test/unit/test_death.cpp | 2 +- test/unit/test_memory.cpp | 4 +- .../test_protocol_upgrade_boost_coverage.cpp | 4 +- test/unit/test_request_parser_callbacks.cpp | 8 +- test/unit/test_request_response_coverage.cpp | 4 +- test/unit/test_server_api_coverage.cpp | 10 +- test/unit/test_server_atomic_construct.cpp | 249 ++++++++++++++++++ test/unit/test_server_boost_coverage.cpp | 20 +- test/unit/test_server_boost_coverage2.cpp | 6 +- test/unit/test_server_error_coverage.cpp | 10 +- .../test_server_simple_handlers_coverage.cpp | 16 +- test/unit/test_stress.cpp | 4 +- test/unit/test_zerocopy_threshold_wire.cpp | 2 +- 67 files changed, 686 insertions(+), 370 deletions(-) create mode 100644 test/unit/test_server_atomic_construct.cpp diff --git a/benchmark/benchmark_unified.c b/benchmark/benchmark_unified.c index 8bea585e..f58ab6ac 100644 --- a/benchmark/benchmark_unified.c +++ b/benchmark/benchmark_unified.c @@ -862,7 +862,7 @@ int main(int argc, char* argv[]) { /* Set server context */ g_signal_server = server; - uvhttp_error_t result = uvhttp_server_set_context(server, NULL); + uvhttp_error_t result = uvhttp_server_take_context(server, NULL); if (result != UVHTTP_OK) { fprintf(stderr, "Warning: Failed to set server context: %s\n", uvhttp_error_string(result)); } diff --git a/docs/api/API_REFERENCE.md b/docs/api/API_REFERENCE.md index b1050072..52ffdf14 100644 --- a/docs/api/API_REFERENCE.md +++ b/docs/api/API_REFERENCE.md @@ -840,7 +840,7 @@ int main(void) { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); uvhttp_router_add_route(router, "/", home_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_error_t result = uvhttp_server_listen(server, "0.0.0.0", 8080); if (result != UVHTTP_OK) { diff --git a/docs/api/PROTOCOL_UPGRADE_API.md b/docs/api/PROTOCOL_UPGRADE_API.md index bb1005ae..c510a3a4 100644 --- a/docs/api/PROTOCOL_UPGRADE_API.md +++ b/docs/api/PROTOCOL_UPGRADE_API.md @@ -490,7 +490,7 @@ int main() { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); uvhttp_router_add_route(router, "/", http_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // 5. 注册 IPPS 协议升级 uvhttp_server_register_protocol_upgrade( diff --git a/docs/api/introduction.md b/docs/api/introduction.md index 3223fa3b..c4bf666d 100644 --- a/docs/api/introduction.md +++ b/docs/api/introduction.md @@ -170,7 +170,7 @@ int main() { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_router_add_route(router, "/", index_handler); uvhttp_router_add_route(router, "/api", api_handler); diff --git a/docs/guide/EMBEDDING_GUIDE.md b/docs/guide/EMBEDDING_GUIDE.md index e2d287e6..cfb0496e 100644 --- a/docs/guide/EMBEDDING_GUIDE.md +++ b/docs/guide/EMBEDDING_GUIDE.md @@ -162,35 +162,28 @@ int main(int argc, char** argv) { return 1; } - if (uvhttp_server_new(loop, &g_app.server) != UVHTTP_OK) { - fprintf(stderr, "error: cannot create uvhttp server\n"); + /* Atomic construction: server + routes + listen in one call. + * + * Before v2.10 this was five calls with a hand-written rollback per + * failure. The listen-failure branch called uvhttp_router_free on a + * router the server already owned (take_router transfers ownership) and + * then uvhttp_server_free freed it again - a double free. + * uvhttp_server_listen_routes has no partial state, so there is nothing + * to roll back. */ + const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, hello_handler}, + }; + if (uvhttp_server_listen_routes(loop, routes, 1, "0.0.0.0", port, + &g_app.server) != UVHTTP_OK) { + fprintf(stderr, "error: cannot start server on 0.0.0.0:%d\n", port); return 1; } - if (uvhttp_router_new(&g_app.router) != UVHTTP_OK) { - fprintf(stderr, "error: cannot create router\n"); - uvhttp_server_free(g_app.server); - return 1; - } - if (uvhttp_router_add_route(g_app.router, "/", hello_handler) != UVHTTP_OK) { - fprintf(stderr, "error: cannot add route /\n"); - uvhttp_router_free(g_app.router); - uvhttp_server_free(g_app.server); - return 1; - } - uvhttp_server_set_router(g_app.server, g_app.router); uv_signal_init(loop, &g_sigint); uv_signal_start(&g_sigint, on_signal, SIGINT); uv_signal_init(loop, &g_sigterm); uv_signal_start(&g_sigterm, on_signal, SIGTERM); - if (uvhttp_server_listen(g_app.server, "0.0.0.0", port) != UVHTTP_OK) { - fprintf(stderr, "error: cannot listen on 0.0.0.0:%d\n", port); - uvhttp_router_free(g_app.router); - uvhttp_server_free(g_app.server); - return 1; - } - printf("Embedded uvhttp listening on http://0.0.0.0:%d\n", port); printf("Test: curl http://localhost:%d/\n", port); diff --git a/docs/guide/INSTALL_CMAKE.md b/docs/guide/INSTALL_CMAKE.md index 58089adc..c44166a5 100644 --- a/docs/guide/INSTALL_CMAKE.md +++ b/docs/guide/INSTALL_CMAKE.md @@ -173,7 +173,7 @@ int main() { // Add routes uvhttp_router_add_route(router, "/", home_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running on http://0.0.0.0:8080\n"); diff --git a/docs/guide/LIBUV_DATA_POINTER.md b/docs/guide/LIBUV_DATA_POINTER.md index 880a255b..6ccfb189 100644 --- a/docs/guide/LIBUV_DATA_POINTER.md +++ b/docs/guide/LIBUV_DATA_POINTER.md @@ -64,7 +64,7 @@ int main() { // Create the server uvhttp_server_new(loop, &ctx->server); uvhttp_router_new(&ctx->router); - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); // Set the context into the event loop's data pointer loop->data = ctx; @@ -193,7 +193,7 @@ int main() { // Create the server uvhttp_server_new(loop, &ctx->server); uvhttp_router_new(&ctx->router); - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); // Add routes uvhttp_router_add_route(ctx->router, "/stats", stats_handler); @@ -271,7 +271,7 @@ void* worker_thread(void* arg) { // Create the server uvhttp_server_new(ctx->loop, &ctx->server); uvhttp_router_new(&ctx->router); - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); // Add routes uvhttp_router_add_route(ctx->router, "/", worker_handler); @@ -359,7 +359,7 @@ app_context_t* app_context_create(uv_loop_t* loop) { ctx->start_time = time(NULL); if (ctx->server && ctx->router) { - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); loop->data = ctx; return ctx; } diff --git a/docs/guide/TUTORIAL.md b/docs/guide/TUTORIAL.md index 9c407961..2e59d124 100644 --- a/docs/guide/TUTORIAL.md +++ b/docs/guide/TUTORIAL.md @@ -144,7 +144,7 @@ int main() { // Create a router uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // Add a route uvhttp_router_add_route(router, "/", hello_handler); @@ -367,7 +367,7 @@ int main() { uvhttp_router_add_route(router, "/about", about_handler); uvhttp_router_add_route(router, "/api", api_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -442,7 +442,7 @@ int main() { // Add a route with a parameter uvhttp_router_add_route(router, "/user/*", user_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -541,7 +541,7 @@ int main() { uvhttp_router_add_route(router, "/resource", put_handler); uvhttp_router_add_route(router, "/resource", delete_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -630,7 +630,7 @@ int main() { uvhttp_router_add_route(router, "/public", public_handler); uvhttp_router_add_route(router, "/protected", protected_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -712,7 +712,7 @@ int main() { uvhttp_router_add_route(router, "/api/v2/users", api_v2_users_handler); uvhttp_router_add_route(router, "/api/v2/posts", api_v2_posts_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -802,7 +802,7 @@ int main() { uvhttp_router_add_route(router, "/headers", headers_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -894,7 +894,7 @@ int main() { uvhttp_router_add_route(router, "/api/json", json_post_handler); uvhttp_router_add_route(router, "/api/upload", upload_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -1019,7 +1019,7 @@ int main() { uvhttp_router_add_route(router, "/", home_handler); uvhttp_router_add_route(router, "/static/*", static_file_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("\n========================================\n"); @@ -1181,7 +1181,7 @@ int main() { uvhttp_router_add_route(router, "/api", api_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -1263,7 +1263,7 @@ int main() { uvhttp_router_add_route(router, "/stream", stream_handler); uvhttp_router_add_route(router, "/sse", sse_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -1364,7 +1364,7 @@ app_context_t* app_context_create(uv_loop_t* loop, const char* name) { } // Set the router - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); // Set the context into the event loop loop->data = ctx; @@ -1604,7 +1604,7 @@ void* worker_thread_func(void* arg) { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); uvhttp_router_add_route(router, "/", request_handler); - uvhttp_server_set_router(worker->server, router); + uvhttp_server_take_router(worker->server, router); // Start listening (each thread listens on a different port) int port = PORT_BASE + worker->thread_id; @@ -1891,7 +1891,7 @@ int main() { uvhttp_router_add_route(router, "/api/users", get_users_handler); uvhttp_router_add_route(router, "/api/users", create_user_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -2042,7 +2042,7 @@ int main() { uvhttp_router_add_route(router, "/api", api_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running at http://localhost:8080\n"); @@ -2244,7 +2244,7 @@ int main() { uvhttp_router_add_route(router, "/", load_balance_handler); uvhttp_router_add_route(router, "/health", health_check_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Load balancer running at http://localhost:8080\n"); @@ -2473,7 +2473,7 @@ uvhttp_server_t* server = NULL; uvhttp_server_new(loop, &server); uvhttp_router_t* router = NULL; uvhttp_router_new(&router); -uvhttp_server_set_router(server, router); +uvhttp_server_take_router(server, router); uvhttp_router_add_route(router, "/", handler); uvhttp_server_listen(server, "0.0.0.0", 8080); diff --git a/docs/guide/first-server.md b/docs/guide/first-server.md index 41603eb2..dcd0bc10 100644 --- a/docs/guide/first-server.md +++ b/docs/guide/first-server.md @@ -46,28 +46,22 @@ int main() { uv_loop_t* loop = uv_default_loop(); - uvhttp_error_t r = uvhttp_server_new(loop, &g_server); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create server: %s\n", uvhttp_error_string(r)); - return 1; - } - - uvhttp_router_t* router = NULL; - r = uvhttp_router_new(&router); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create router: %s\n", uvhttp_error_string(r)); - uvhttp_server_free(g_server); - return 1; - } - - uvhttp_server_set_router(g_server, router); - uvhttp_router_add_route(router, "/", hello_handler); - - uvhttp_error_t result = uvhttp_server_listen(g_server, "0.0.0.0", 8080); + /* Atomic construction: build the server, register the routes and start + * listening in one call. Either we get a fully listening server or + * nothing at all — no rollback branches to get wrong. + * + * (Before v2.10 this took five calls and each failure branch needed its + * own cleanup. The obvious cleanup was wrong: after take_router the + * server owns the router, so calling uvhttp_router_free on it too is a + * double free.) */ + const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, hello_handler}, + }; + + uvhttp_error_t result = + uvhttp_server_listen_routes(loop, routes, 1, "0.0.0.0", 8080, &g_server); if (result != UVHTTP_OK) { fprintf(stderr, "Failed to start server: %s\n", uvhttp_error_string(result)); - uvhttp_router_free(router); - uvhttp_server_free(g_server); return 1; } @@ -159,29 +153,15 @@ int main() { uv_loop_t* loop = uv_default_loop(); - uvhttp_error_t r = uvhttp_server_new(loop, &g_server); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create server: %s\n", uvhttp_error_string(r)); - return 1; - } + const uvhttp_route_t routes[] = { + {"/api", UVHTTP_ANY, api_handler}, + {"/api/status", UVHTTP_ANY, api_handler}, + }; - uvhttp_router_t* router = NULL; - r = uvhttp_router_new(&router); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create router: %s\n", uvhttp_error_string(r)); - uvhttp_server_free(g_server); - return 1; - } - - uvhttp_server_set_router(g_server, router); - uvhttp_router_add_route(router, "/api", api_handler); - uvhttp_router_add_route(router, "/api/status", api_handler); - - uvhttp_error_t result = uvhttp_server_listen(g_server, "0.0.0.0", 8080); + uvhttp_error_t result = + uvhttp_server_listen_routes(loop, routes, 2, "0.0.0.0", 8080, &g_server); if (result != UVHTTP_OK) { fprintf(stderr, "Failed to start server: %s\n", uvhttp_error_string(result)); - uvhttp_router_free(router); - uvhttp_server_free(g_server); return 1; } @@ -289,28 +269,14 @@ int main() { uv_loop_t* loop = uv_default_loop(); - uvhttp_error_t r = uvhttp_server_new(loop, &g_server); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create server: %s\n", uvhttp_error_string(r)); - return 1; - } - - uvhttp_router_t* router = NULL; - r = uvhttp_router_new(&router); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create router: %s\n", uvhttp_error_string(r)); - uvhttp_server_free(g_server); - return 1; - } - - uvhttp_server_set_router(g_server, router); - uvhttp_router_add_route(router, "/users/*", user_handler); + const uvhttp_route_t routes[] = { + {"/users/:id", UVHTTP_ANY, user_handler}, + }; - uvhttp_error_t result = uvhttp_server_listen(g_server, "0.0.0.0", 8080); + uvhttp_error_t result = + uvhttp_server_listen_routes(loop, routes, 1, "0.0.0.0", 8080, &g_server); if (result != UVHTTP_OK) { fprintf(stderr, "Failed to start server: %s\n", uvhttp_error_string(result)); - uvhttp_router_free(router); - uvhttp_server_free(g_server); return 1; } diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index df8ffe28..3598895f 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -67,7 +67,7 @@ int main() { fprintf(stderr, "Failed to create router: %s\n", uvhttp_error_string(result)); return 1; } - result = uvhttp_server_set_router(server, router); + result = uvhttp_server_take_router(server, router); if (result != UVHTTP_OK) { fprintf(stderr, "Failed to set router: %s\n", uvhttp_error_string(result)); return 1; diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md index 5bc14876..7cf1613b 100644 --- a/docs/guide/introduction.md +++ b/docs/guide/introduction.md @@ -223,7 +223,7 @@ int main() { // Create router uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // Add route uvhttp_router_add_route(router, "/hello", hello_handler); diff --git a/docs/guide/websocket.md b/docs/guide/websocket.md index 3c08aba7..50e665aa 100644 --- a/docs/guide/websocket.md +++ b/docs/guide/websocket.md @@ -82,7 +82,7 @@ int main() { uvhttp_server_new(loop, &server); uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // Register the WebSocket handler uvhttp_ws_handler_t ws_handler = { diff --git a/docs/spec/server-api.md b/docs/spec/server-api.md index ceabbb5b..e46de6ad 100644 --- a/docs/spec/server-api.md +++ b/docs/spec/server-api.md @@ -58,8 +58,8 @@ connection management. - `UVHTTP_ERROR_INVALID_PARAM`: `server` or `handler` is NULL - **Thread safety**: Not thread-safe. -### uvhttp_server_set_router -- **Signature**: `uvhttp_error_t uvhttp_server_set_router(uvhttp_server_t* server, uvhttp_router_t* router)` +### uvhttp_server_take_router +- **Signature**: `uvhttp_error_t uvhttp_server_take_router(uvhttp_server_t* server, uvhttp_router_t* router)` - **Purpose**: Attach a router for path-based request dispatching - **Preconditions**: `server` must be valid. `router` must be a valid router. - **Postconditions**: `server->router` is set. The server does not own the router; the caller must free it after the server. diff --git a/docs/zh/api/introduction.md b/docs/zh/api/introduction.md index 6ef3107e..fe7821db 100644 --- a/docs/zh/api/introduction.md +++ b/docs/zh/api/introduction.md @@ -165,7 +165,7 @@ int main() { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_router_add_route(router, "/", index_handler); uvhttp_router_add_route(router, "/api", api_handler); diff --git a/docs/zh/guide/EMBEDDING_GUIDE.md b/docs/zh/guide/EMBEDDING_GUIDE.md index 05de4cfd..a7997797 100644 --- a/docs/zh/guide/EMBEDDING_GUIDE.md +++ b/docs/zh/guide/EMBEDDING_GUIDE.md @@ -155,35 +155,26 @@ int main(int argc, char** argv) { return 1; } - if (uvhttp_server_new(loop, &g_app.server) != UVHTTP_OK) { - fprintf(stderr, "error: cannot create uvhttp server\n"); + /* 原子构造:一次调用完成 server + 路由 + 监听 + * + * v2.10 之前这里需要 5 步,每个失败分支都要手写回滚。listen 失败 + * 分支里的 uvhttp_router_free 释放的是 server 已经接管的 router + * (take_router 转移所有权),随后 uvhttp_server_free 再释放一次 —— + * 二次释放。uvhttp_server_listen_routes 不存在中间态,无需回滚。 */ + const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, hello_handler}, + }; + if (uvhttp_server_listen_routes(loop, routes, 1, "0.0.0.0", port, + &g_app.server) != UVHTTP_OK) { + fprintf(stderr, "error: cannot start server on 0.0.0.0:%d\n", port); return 1; } - if (uvhttp_router_new(&g_app.router) != UVHTTP_OK) { - fprintf(stderr, "error: cannot create router\n"); - uvhttp_server_free(g_app.server); - return 1; - } - if (uvhttp_router_add_route(g_app.router, "/", hello_handler) != UVHTTP_OK) { - fprintf(stderr, "error: cannot add route /\n"); - uvhttp_router_free(g_app.router); - uvhttp_server_free(g_app.server); - return 1; - } - uvhttp_server_set_router(g_app.server, g_app.router); uv_signal_init(loop, &g_sigint); uv_signal_start(&g_sigint, on_signal, SIGINT); uv_signal_init(loop, &g_sigterm); uv_signal_start(&g_sigterm, on_signal, SIGTERM); - if (uvhttp_server_listen(g_app.server, "0.0.0.0", port) != UVHTTP_OK) { - fprintf(stderr, "error: cannot listen on 0.0.0.0:%d\n", port); - uvhttp_router_free(g_app.router); - uvhttp_server_free(g_app.server); - return 1; - } - printf("Embedded uvhttp listening on http://0.0.0.0:%d\n", port); printf("Test: curl http://localhost:%d/\n", port); diff --git a/docs/zh/guide/INSTALL_CMAKE.md b/docs/zh/guide/INSTALL_CMAKE.md index dc96aec3..203ac1e8 100644 --- a/docs/zh/guide/INSTALL_CMAKE.md +++ b/docs/zh/guide/INSTALL_CMAKE.md @@ -173,7 +173,7 @@ int main() { // 添加路由 uvhttp_router_add_route(router, "/", home_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("Server running on http://0.0.0.0:8080\n"); diff --git a/docs/zh/guide/LIBUV_DATA_POINTER.md b/docs/zh/guide/LIBUV_DATA_POINTER.md index 0672607a..06b56cc1 100644 --- a/docs/zh/guide/LIBUV_DATA_POINTER.md +++ b/docs/zh/guide/LIBUV_DATA_POINTER.md @@ -64,7 +64,7 @@ int main() { // 创建服务器 uvhttp_server_new(loop, &ctx->server); uvhttp_router_new(&ctx->router); - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); // 将上下文设置到事件循环的 data 指针 loop->data = ctx; @@ -193,7 +193,7 @@ int main() { // 创建服务器 uvhttp_server_new(loop, &ctx->server); uvhttp_router_new(&ctx->router); - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); // 添加路由 uvhttp_router_add_route(ctx->router, "/stats", stats_handler); @@ -271,7 +271,7 @@ void* worker_thread(void* arg) { // 创建服务器 uvhttp_server_new(ctx->loop, &ctx->server); uvhttp_router_new(&ctx->router); - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); // 添加路由 uvhttp_router_add_route(ctx->router, "/", worker_handler); @@ -359,7 +359,7 @@ app_context_t* app_context_create(uv_loop_t* loop) { ctx->start_time = time(NULL); if (ctx->server && ctx->router) { - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); loop->data = ctx; return ctx; } diff --git a/docs/zh/guide/STATIC_FILE_SERVER.md b/docs/zh/guide/STATIC_FILE_SERVER.md index 59fc3ab1..3c13be4a 100644 --- a/docs/zh/guide/STATIC_FILE_SERVER.md +++ b/docs/zh/guide/STATIC_FILE_SERVER.md @@ -101,7 +101,7 @@ int main() { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); uvhttp_router_add_route(router, "/*", static_file_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); uv_run(loop, UV_RUN_DEFAULT); diff --git a/docs/zh/guide/TUTORIAL.md b/docs/zh/guide/TUTORIAL.md index cef76c29..8d31a5d7 100644 --- a/docs/zh/guide/TUTORIAL.md +++ b/docs/zh/guide/TUTORIAL.md @@ -148,7 +148,7 @@ int main() { // 创建路由器 uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // 添加路由 uvhttp_router_add_route(router, "/", hello_handler); @@ -367,7 +367,7 @@ int main() { uvhttp_router_add_route(router, "/about", about_handler); uvhttp_router_add_route(router, "/api", api_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -442,7 +442,7 @@ int main() { // 添加带参数的路由 uvhttp_router_add_route(router, "/user/*", user_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -541,7 +541,7 @@ int main() { uvhttp_router_add_route(router, "/resource", put_handler); uvhttp_router_add_route(router, "/resource", delete_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -630,7 +630,7 @@ int main() { uvhttp_router_add_route(router, "/public", public_handler); uvhttp_router_add_route(router, "/protected", protected_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -712,7 +712,7 @@ int main() { uvhttp_router_add_route(router, "/api/v2/users", api_v2_users_handler); uvhttp_router_add_route(router, "/api/v2/posts", api_v2_posts_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -802,7 +802,7 @@ int main() { uvhttp_router_add_route(router, "/headers", headers_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -894,7 +894,7 @@ int main() { uvhttp_router_add_route(router, "/api/json", json_post_handler); uvhttp_router_add_route(router, "/api/upload", upload_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -1019,7 +1019,7 @@ int main() { uvhttp_router_add_route(router, "/", home_handler); uvhttp_router_add_route(router, "/static/*", static_file_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("\n========================================\n"); @@ -1180,7 +1180,7 @@ int main() { uvhttp_router_add_route(router, "/api", api_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -1260,7 +1260,7 @@ int main() { uvhttp_router_add_route(router, "/stream", stream_handler); uvhttp_router_add_route(router, "/sse", sse_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -1361,7 +1361,7 @@ app_context_t* app_context_create(uv_loop_t* loop, const char* name) { } // 设置路由器 - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); // 将上下文设置到事件循环 loop->data = ctx; @@ -1599,7 +1599,7 @@ void* worker_thread_func(void* arg) { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); uvhttp_router_add_route(router, "/", request_handler); - uvhttp_server_set_router(worker->server, router); + uvhttp_server_take_router(worker->server, router); // 启动服务器监听(每个线程监听不同端口) int port = PORT_BASE + worker->thread_id; @@ -1886,7 +1886,7 @@ int main() { uvhttp_router_add_route(router, "/api/users", get_users_handler); uvhttp_router_add_route(router, "/api/users", create_user_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -2037,7 +2037,7 @@ int main() { uvhttp_router_add_route(router, "/api", api_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("服务器运行在 http://localhost:8080\n"); @@ -2239,7 +2239,7 @@ int main() { uvhttp_router_add_route(router, "/", load_balance_handler); uvhttp_router_add_route(router, "/health", health_check_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8080); printf("负载均衡器运行在 http://localhost:8080\n"); @@ -2470,7 +2470,7 @@ uvhttp_server_t* server = NULL; uvhttp_server_new(loop, &server); uvhttp_router_t* router = NULL; uvhttp_router_new(&router); -uvhttp_server_set_router(server, router); +uvhttp_server_take_router(server, router); uvhttp_router_add_route(router, "/", handler); uvhttp_server_listen(server, "0.0.0.0", 8080); diff --git a/docs/zh/guide/first-server.md b/docs/zh/guide/first-server.md index 797b56a3..1a7ad683 100644 --- a/docs/zh/guide/first-server.md +++ b/docs/zh/guide/first-server.md @@ -46,28 +46,15 @@ int main() { uv_loop_t* loop = uv_default_loop(); - uvhttp_error_t r = uvhttp_server_new(loop, &g_server); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create server: %s\n", uvhttp_error_string(r)); - return 1; - } - - uvhttp_router_t* router = NULL; - r = uvhttp_router_new(&router); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create router: %s\n", uvhttp_error_string(r)); - uvhttp_server_free(g_server); - return 1; - } + /* 原子构造:一次调用完成 server + 路由 + 监听,无中间态、无需回滚 */ + const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, hello_handler}, + }; - uvhttp_server_set_router(g_server, router); - uvhttp_router_add_route(router, "/", hello_handler); - - uvhttp_error_t result = uvhttp_server_listen(g_server, "0.0.0.0", 8080); + uvhttp_error_t result = + uvhttp_server_listen_routes(loop, routes, 1, "0.0.0.0", 8080, &g_server); if (result != UVHTTP_OK) { fprintf(stderr, "Failed to start server: %s\n", uvhttp_error_string(result)); - uvhttp_router_free(router); - uvhttp_server_free(g_server); return 1; } @@ -159,29 +146,15 @@ int main() { uv_loop_t* loop = uv_default_loop(); - uvhttp_error_t r = uvhttp_server_new(loop, &g_server); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create server: %s\n", uvhttp_error_string(r)); - return 1; - } - - uvhttp_router_t* router = NULL; - r = uvhttp_router_new(&router); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create router: %s\n", uvhttp_error_string(r)); - uvhttp_server_free(g_server); - return 1; - } - - uvhttp_server_set_router(g_server, router); - uvhttp_router_add_route(router, "/api", api_handler); - uvhttp_router_add_route(router, "/api/status", api_handler); + const uvhttp_route_t routes[] = { + {"/api", UVHTTP_ANY, api_handler}, + {"/api/status", UVHTTP_ANY, api_handler}, + }; - uvhttp_error_t result = uvhttp_server_listen(g_server, "0.0.0.0", 8080); + uvhttp_error_t result = + uvhttp_server_listen_routes(loop, routes, 2, "0.0.0.0", 8080, &g_server); if (result != UVHTTP_OK) { fprintf(stderr, "Failed to start server: %s\n", uvhttp_error_string(result)); - uvhttp_router_free(router); - uvhttp_server_free(g_server); return 1; } @@ -289,28 +262,14 @@ int main() { uv_loop_t* loop = uv_default_loop(); - uvhttp_error_t r = uvhttp_server_new(loop, &g_server); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create server: %s\n", uvhttp_error_string(r)); - return 1; - } - - uvhttp_router_t* router = NULL; - r = uvhttp_router_new(&router); - if (r != UVHTTP_OK) { - fprintf(stderr, "Failed to create router: %s\n", uvhttp_error_string(r)); - uvhttp_server_free(g_server); - return 1; - } - - uvhttp_server_set_router(g_server, router); - uvhttp_router_add_route(router, "/users/*", user_handler); + const uvhttp_route_t routes[] = { + {"/users/:id", UVHTTP_ANY, user_handler}, + }; - uvhttp_error_t result = uvhttp_server_listen(g_server, "0.0.0.0", 8080); + uvhttp_error_t result = + uvhttp_server_listen_routes(loop, routes, 1, "0.0.0.0", 8080, &g_server); if (result != UVHTTP_OK) { fprintf(stderr, "Failed to start server: %s\n", uvhttp_error_string(result)); - uvhttp_router_free(router); - uvhttp_server_free(g_server); return 1; } diff --git a/docs/zh/guide/getting-started.md b/docs/zh/guide/getting-started.md index ac32d8d3..8e8a69c8 100644 --- a/docs/zh/guide/getting-started.md +++ b/docs/zh/guide/getting-started.md @@ -81,7 +81,7 @@ int main() { uvhttp_server_new(loop, &server); uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // 添加路由 uvhttp_router_add_route(router, "/", hello_handler); diff --git a/docs/zh/guide/introduction.md b/docs/zh/guide/introduction.md index c6a234de..0f0421b7 100644 --- a/docs/zh/guide/introduction.md +++ b/docs/zh/guide/introduction.md @@ -208,7 +208,7 @@ int main() { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_router_add_route(router, "/hello", hello_handler); diff --git a/docs/zh/guide/websocket.md b/docs/zh/guide/websocket.md index 81a0f4f3..d99b90d3 100644 --- a/docs/zh/guide/websocket.md +++ b/docs/zh/guide/websocket.md @@ -76,7 +76,7 @@ int main() { uvhttp_server_new(loop, &server); uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // 注册 WebSocket 处理器 uvhttp_ws_handler_t ws_handler = { diff --git a/examples/01_basics/01_hello_world.c b/examples/01_basics/01_hello_world.c index 0ccb2da6..ade10205 100644 --- a/examples/01_basics/01_hello_world.c +++ b/examples/01_basics/01_hello_world.c @@ -127,7 +127,7 @@ int main() { // 步骤 5: 设置路由器到服务器 printf("步骤 5: 设置路由器...\n"); - uvhttp_server_set_router(ctx.server, router); + uvhttp_server_take_router(ctx.server, router); printf("✓ 路由器设置成功\n\n"); // 步骤 6: 启动服务器监听 diff --git a/examples/01_basics/helloworld.c b/examples/01_basics/helloworld.c index e2384f43..46d2b273 100644 --- a/examples/01_basics/helloworld.c +++ b/examples/01_basics/helloworld.c @@ -43,7 +43,7 @@ static app_context_t* app_context_new(uv_loop_t* loop) { } /* Release an application context. The router and config are owned by the - * server (set via uvhttp_server_set_router / server->config), so they are + * server (set via uvhttp_server_take_router / server->config), so they are * freed together with the server. */ static void app_context_free(app_context_t* ctx) { if (ctx) { @@ -218,7 +218,7 @@ int main() { * "Global configuration not initialized" warning) and attach the * context to the server. */ uvhttp_config_set_current(ctx->uvhttp_ctx, config); - uvhttp_server_set_context(ctx->server, ctx->uvhttp_ctx); + uvhttp_server_take_context(ctx->server, ctx->uvhttp_ctx); printf("UVHTTP context set to server\n"); /* Create the router (output-parameter style). */ @@ -245,7 +245,7 @@ int main() { printf("Route added successfully\n"); /* Wire the router into the server via the setter. */ - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); printf("Router set to server\n"); /* Start listening. */ diff --git a/examples/01_basics/helloworld_complete.c b/examples/01_basics/helloworld_complete.c index 914b81a4..a132a09d 100644 --- a/examples/01_basics/helloworld_complete.c +++ b/examples/01_basics/helloworld_complete.c @@ -42,7 +42,7 @@ static app_context_t* app_context_new(uv_loop_t* loop) { } /* Release an application context. The router and config are owned by the - * server (set via uvhttp_server_set_router / server->config), so they are + * server (set via uvhttp_server_take_router / server->config), so they are * freed together with the server. */ static void app_context_free(app_context_t* ctx) { if (ctx) { @@ -228,7 +228,7 @@ int main() { uvhttp_config_set_current(ctx->uvhttp_ctx, config); printf("Global configuration set\n"); - uvhttp_server_set_context(ctx->server, ctx->uvhttp_ctx); + uvhttp_server_take_context(ctx->server, ctx->uvhttp_ctx); printf("uvhttp context set to server\n"); /* Create the router (output-parameter style). */ @@ -255,7 +255,7 @@ int main() { printf("Route added successfully\n"); /* Wire the router into the server via the setter. */ - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); printf("Router set to server\n"); /* Start listening. */ diff --git a/examples/01_basics/quick_api_demo.c b/examples/01_basics/quick_api_demo.c index 043581ec..171a0dff 100644 --- a/examples/01_basics/quick_api_demo.c +++ b/examples/01_basics/quick_api_demo.c @@ -139,7 +139,7 @@ int main() { free(ctx); return 1; } - uvhttp_server_set_router(ctx->server, router); + uvhttp_server_take_router(ctx->server, router); // 添加路由 uvhttp_router_add_route(router, "/", hello_handler); diff --git a/examples/01_basics/signal_handling_demo.c b/examples/01_basics/signal_handling_demo.c index fd95ff26..109ca52a 100644 --- a/examples/01_basics/signal_handling_demo.c +++ b/examples/01_basics/signal_handling_demo.c @@ -131,7 +131,7 @@ int main() { free(ctx); return 1; } - uvhttp_server_set_router(ctx->server, router); + uvhttp_server_take_router(ctx->server, router); // 添加路由 uvhttp_router_add_route(router, "/", hello_handler); diff --git a/examples/01_basics/simple_api_demo.c b/examples/01_basics/simple_api_demo.c index 0321a3d9..68dfaf52 100644 --- a/examples/01_basics/simple_api_demo.c +++ b/examples/01_basics/simple_api_demo.c @@ -75,7 +75,7 @@ int main() { uvhttp_server_free(server); return 1; } - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); /* Add a catch-all route. */ uvhttp_router_add_route(router, "/*", simple_handler); diff --git a/examples/02_routing/01_simple_routing.c b/examples/02_routing/01_simple_routing.c index bb360823..6cb92688 100644 --- a/examples/02_routing/01_simple_routing.c +++ b/examples/02_routing/01_simple_routing.c @@ -281,7 +281,7 @@ int main() { printf("\n"); // 设置路由器 - uvhttp_server_set_router(ctx.server, router); + uvhttp_server_take_router(ctx.server, router); // 启动服务器 int listen_result = uvhttp_server_listen(ctx.server, "0.0.0.0", 8080); diff --git a/examples/02_routing/02_method_routing.c b/examples/02_routing/02_method_routing.c index f4766cc7..b2dcf97c 100644 --- a/examples/02_routing/02_method_routing.c +++ b/examples/02_routing/02_method_routing.c @@ -369,7 +369,7 @@ int main() { printf("\n"); // 设置路由器 - uvhttp_server_set_router(ctx->server, router); + uvhttp_server_take_router(ctx->server, router); // 启动服务器 int listen_result = uvhttp_server_listen(ctx->server, "0.0.0.0", 8080); diff --git a/examples/03_middleware/logging_middleware.c b/examples/03_middleware/logging_middleware.c index 5ab1f8f9..41b0661c 100644 --- a/examples/03_middleware/logging_middleware.c +++ b/examples/03_middleware/logging_middleware.c @@ -135,7 +135,7 @@ int main(void) { /* Wrap handlers with the logging middleware */ uvhttp_router_add_route(router, "/", hello_handler); uvhttp_router_add_route(router, "/delay", delay_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); err = uvhttp_server_listen(server, "127.0.0.1", 8080); if (err != UVHTTP_OK) { diff --git a/examples/03_middleware/middleware_chain_demo.c b/examples/03_middleware/middleware_chain_demo.c index 92c22555..0fd0ebaa 100644 --- a/examples/03_middleware/middleware_chain_demo.c +++ b/examples/03_middleware/middleware_chain_demo.c @@ -246,7 +246,7 @@ int main(void) { uvhttp_router_add_route(router, "/api/admin", admin_handler); uvhttp_router_add_route(router, "/health", health_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8083); printf("服务器运行在 http://localhost:8083\n"); diff --git a/examples/03_middleware/middleware_compile_time_demo.c b/examples/03_middleware/middleware_compile_time_demo.c index 2952b908..483ff2ea 100644 --- a/examples/03_middleware/middleware_compile_time_demo.c +++ b/examples/03_middleware/middleware_compile_time_demo.c @@ -202,7 +202,7 @@ int main(void) { uvhttp_router_add_route(router, "/public", public_handler); uvhttp_router_add_route(router, "/protected", protected_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8082); printf("服务器运行在 http://localhost:8082\n"); diff --git a/examples/03_middleware/rate_limit_demo.c b/examples/03_middleware/rate_limit_demo.c index 6ea9287d..b97a6064 100644 --- a/examples/03_middleware/rate_limit_demo.c +++ b/examples/03_middleware/rate_limit_demo.c @@ -191,7 +191,7 @@ int main(void) { /* 添加路由 */ uvhttp_router_add_route(router, "/api", api_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8085); printf("服务器运行在 http://localhost:8085\n"); diff --git a/examples/03_middleware/test_middleware.c b/examples/03_middleware/test_middleware.c index 4e64c43d..db1a8aa8 100644 --- a/examples/03_middleware/test_middleware.c +++ b/examples/03_middleware/test_middleware.c @@ -67,7 +67,7 @@ int main(void) { /* 添加路由 */ uvhttp_router_add_route(router, "/test", test_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_listen(server, "0.0.0.0", 8086); printf("服务器运行在 http://localhost:8086/test\n"); diff --git a/examples/06_advanced/api_demo.c b/examples/06_advanced/api_demo.c index d00b4044..3bb9dc97 100644 --- a/examples/06_advanced/api_demo.c +++ b/examples/06_advanced/api_demo.c @@ -189,7 +189,7 @@ int main() { } // 设置路由器到服务器 - uvhttp_server_set_router(ctx->server, router); + uvhttp_server_take_router(ctx->server, router); // 添加路由 uvhttp_router_add_route(router, "/", home_handler); diff --git a/examples/06_advanced/config_demo.c b/examples/06_advanced/config_demo.c index fbca7bef..447373d0 100644 --- a/examples/06_advanced/config_demo.c +++ b/examples/06_advanced/config_demo.c @@ -392,8 +392,8 @@ int main(int argc, char* argv[]) { // 添加路由 uvhttp_router_add_route(g_app->router, "/", demo_handler); uvhttp_router_add_route(g_app->router, "/config", config_api_handler); - // 使用 uvhttp_server_set_router 而非直接赋值 - uvhttp_server_set_router(g_app->server, g_app->router); + // 使用 uvhttp_server_take_router 而非直接赋值 + uvhttp_server_take_router(g_app->server, g_app->router); printf(" 路由设置完成\n"); // 启动配置动态调整定时器 diff --git a/examples/06_advanced/context_injection.c b/examples/06_advanced/context_injection.c index 32ab6fe4..fda1f03e 100644 --- a/examples/06_advanced/context_injection.c +++ b/examples/06_advanced/context_injection.c @@ -89,7 +89,7 @@ app_context_t* app_context_create(uv_loop_t* loop, const char* name) { } // 设置路由器 - uvhttp_server_set_router(ctx->server, ctx->router); + uvhttp_server_take_router(ctx->server, ctx->router); // 创建 uvhttp_context 并设置到服务器 // 这是避免独占 loop->data 的关键! @@ -97,7 +97,7 @@ app_context_t* app_context_create(uv_loop_t* loop, const char* name) { uvhttp_error_t result_uvhttp_ctx = uvhttp_context_create(loop, &uvhttp_ctx); if (result_uvhttp_ctx != UVHTTP_OK || !uvhttp_ctx) { fprintf(stderr, "错误: 无法创建 uvhttp_context\n"); - uvhttp_router_free(ctx->router); + /* router 已通过 take_router 交给 server,不要再单独释放 */ uvhttp_server_free(ctx->server); uvhttp_free(ctx); return NULL; @@ -107,7 +107,7 @@ app_context_t* app_context_create(uv_loop_t* loop, const char* name) { ctx->uvhttp_ctx = uvhttp_ctx; // 将 uvhttp_context 设置到服务器 - uvhttp_server_set_context(ctx->server, uvhttp_ctx); + uvhttp_server_take_context(ctx->server, uvhttp_ctx); printf("✓ 应用上下文创建成功\n"); printf(" 服务器名称: %s\n", ctx->server_name); @@ -371,7 +371,7 @@ int info_handler(uvhttp_request_t* req, uvhttp_response_t* res) { cJSON_AddStringToObject(json_obj, "example", "shared_loop_data"); cJSON_AddStringToObject(json_obj, "description", "演示如何避免独占 loop->data"); - cJSON_AddStringToObject(json_obj, "solution", "使用 uvhttp_server_set_context() 设置服务器上下文"); + cJSON_AddStringToObject(json_obj, "solution", "使用 uvhttp_server_take_context() 设置服务器上下文"); cJSON_AddStringToObject(json_obj, "scenario", "当其他应用也在使用 loop->data 时"); char* json_string = cJSON_PrintUnformatted(json_obj); diff --git a/examples/06_advanced/protocol_upgrade_example.c b/examples/06_advanced/protocol_upgrade_example.c index aff3aca6..1225a707 100644 --- a/examples/06_advanced/protocol_upgrade_example.c +++ b/examples/06_advanced/protocol_upgrade_example.c @@ -291,7 +291,7 @@ int main(int argc, char* argv[]) { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); uvhttp_router_add_route(router, "/", http_request_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); /* 6. Register IPPS protocol upgrade */ result = uvhttp_server_register_protocol_upgrade( diff --git a/examples/06_advanced/sse_server.c b/examples/06_advanced/sse_server.c index 951c1a1d..7cc804d8 100644 --- a/examples/06_advanced/sse_server.c +++ b/examples/06_advanced/sse_server.c @@ -201,7 +201,7 @@ int main(int argc, char** argv) { uvhttp_router_add_route(router, "/", index_handler); uvhttp_router_add_route(router, "/events", events_handler); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); err = uvhttp_server_listen(server, "127.0.0.1", port); if (err != UVHTTP_OK) { @@ -215,7 +215,7 @@ int main(int argc, char** argv) { uv_run(loop, UV_RUN_DEFAULT); - uvhttp_router_free(router); + /* router 已交给 server(take_router),由 uvhttp_server_free 一并释放 */ uvhttp_server_free(server); return 0; } \ No newline at end of file diff --git a/examples/07_performance/compression_server.c b/examples/07_performance/compression_server.c index cfb5121b..d94f557d 100644 --- a/examples/07_performance/compression_server.c +++ b/examples/07_performance/compression_server.c @@ -239,7 +239,7 @@ int main(int argc, char** argv) { } // Set router to server - uvhttp_server_set_router(g_server, router); + uvhttp_server_take_router(g_server, router); // Add routes int route_result; diff --git a/examples/07_performance/performance_https_server.c b/examples/07_performance/performance_https_server.c index 3f26f09c..10870500 100644 --- a/examples/07_performance/performance_https_server.c +++ b/examples/07_performance/performance_https_server.c @@ -301,7 +301,7 @@ int main(int argc, char* argv[]) { uvhttp_router_add_route(router, "/api", on_request_json); /* Set router (owned by the server from now on) */ - uvhttp_server_set_router(ctx.server, router); + uvhttp_server_take_router(ctx.server, router); /* Listen */ err = uvhttp_server_listen(ctx.server, host, port); diff --git a/examples/08_e2e_tests/test_static_simple_e2e.c b/examples/08_e2e_tests/test_static_simple_e2e.c index 43eb92ba..5e0bdce2 100644 --- a/examples/08_e2e_tests/test_static_simple_e2e.c +++ b/examples/08_e2e_tests/test_static_simple_e2e.c @@ -119,7 +119,7 @@ int main(int argc, char** argv) { return 1; } - uvhttp_server_set_router(app.server, app.router); + uvhttp_server_take_router(app.server, app.router); g_app = &app; /* Create static files context */ @@ -136,7 +136,7 @@ int main(int argc, char** argv) { result = uvhttp_static_create(&static_config, &app.static_ctx); if (result != UVHTTP_OK) { fprintf(stderr, "Failed to create static context: %d\n", result); - uvhttp_router_free(app.router); + /* router 已通过 take_router 交给 server,不要再单独释放 */ uvhttp_server_free(app.server); return 1; } @@ -155,7 +155,7 @@ int main(int argc, char** argv) { if (result != UVHTTP_OK) { fprintf(stderr, "Failed to start server: %d\n", result); uvhttp_static_free(app.static_ctx); - uvhttp_router_free(app.router); + /* router 已通过 take_router 交给 server,不要再单独释放 */ uvhttp_server_free(app.server); return 1; } diff --git a/examples/custom_compression_strategy.c b/examples/custom_compression_strategy.c index f502b67d..387707af 100644 --- a/examples/custom_compression_strategy.c +++ b/examples/custom_compression_strategy.c @@ -306,12 +306,13 @@ int main(int argc, char* argv[]) { uvhttp_router_add_route(router, "/strategy3", handler_strategy_3); uvhttp_router_add_route(router, "/convenient", handler_convenient); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); /* 启动服务器 */ if (uvhttp_server_listen(server, "0.0.0.0", port) != UVHTTP_OK) { fprintf(stderr, "Failed to start server\n"); - uvhttp_router_free(router); + /* router 已通过 take_router 交给 server,只需释放 server。 + * 再调 uvhttp_router_free(router) 是二次释放。 */ uvhttp_server_free(server); return 1; } diff --git a/examples/embedding/main.c b/examples/embedding/main.c index 3058a720..8790977a 100644 --- a/examples/embedding/main.c +++ b/examples/embedding/main.c @@ -64,25 +64,25 @@ int main(int argc, char** argv) { return 1; } - /* 创建服务器 */ - if (uvhttp_server_new(loop, &g_app.server) != UVHTTP_OK) { - fprintf(stderr, "error: cannot create uvhttp server\n"); + /* 创建服务器 + 注册路由 + 监听:一次调用完成(原子构造) + * + * v2.10 之前这里需要 5 步(server_new / router_new / add_route / + * take_router / listen),并且每个失败分支都要手写回滚。那段回滚代码 + * 在 listen 失败时会先 router_free 再 server_free —— 而 server 已经 + * 通过 take_router 接管了 router,于是二次释放(ASan 可复现的 + * double-free)。 + * + * 现在只有一次调用:要么返回一个完全就绪的监听中的 server, + * 要么什么都没创建,不需要任何清理。 */ + const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, hello_handler}, + }; + if (uvhttp_server_listen_routes(loop, routes, 1, "0.0.0.0", port, + &g_app.server) != UVHTTP_OK) { + fprintf(stderr, "error: cannot start server on 0.0.0.0:%d\n", port); return 1; } - - /* 创建路由器并注册路由 */ - if (uvhttp_router_new(&g_app.router) != UVHTTP_OK) { - fprintf(stderr, "error: cannot create router\n"); - uvhttp_server_free(g_app.server); - return 1; - } - if (uvhttp_router_add_route(g_app.router, "/", hello_handler) != UVHTTP_OK) { - fprintf(stderr, "error: cannot add route /\n"); - uvhttp_router_free(g_app.router); - uvhttp_server_free(g_app.server); - return 1; - } - uvhttp_server_set_router(g_app.server, g_app.router); + /* router 由 server 内部创建并持有,不要(也不能)自己释放 */ /* 注册优雅退出信号 */ uv_signal_init(loop, &g_sigint); @@ -90,14 +90,6 @@ int main(int argc, char** argv) { uv_signal_init(loop, &g_sigterm); uv_signal_start(&g_sigterm, on_signal, SIGTERM); - /* 监听并启动 */ - if (uvhttp_server_listen(g_app.server, "0.0.0.0", port) != UVHTTP_OK) { - fprintf(stderr, "error: cannot listen on 0.0.0.0:%d\n", port); - uvhttp_router_free(g_app.router); - uvhttp_server_free(g_app.server); - return 1; - } - printf("Embedded uvhttp listening on http://0.0.0.0:%d\n", port); printf("Test: curl http://localhost:%d/\n", port); diff --git a/examples/simple_server.c b/examples/simple_server.c index a13bb518..ba815e30 100644 --- a/examples/simple_server.c +++ b/examples/simple_server.c @@ -101,7 +101,7 @@ int main(void) { } // Attach router to server - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // Add routes uvhttp_router_add_route(router, "/", root_handler); diff --git a/include/uvhttp_common.h b/include/uvhttp_common.h index 62b1656a..b94ad907 100644 --- a/include/uvhttp_common.h +++ b/include/uvhttp_common.h @@ -7,6 +7,22 @@ /* Include constant definitions */ #include "uvhttp_constants.h" +/* ========== HTTP Method Enumeration ========== + * Lives here rather than in uvhttp_request.h because both uvhttp_request.h + * and uvhttp_server.h need it and the two are mutually reachable; common.h + * is the shared dependency that breaks the cycle. + * Moved in v2.10 for that reason - the type itself is unchanged. */ +typedef enum uvhttp_method { + UVHTTP_ANY = 0, + UVHTTP_GET, + UVHTTP_POST, + UVHTTP_PUT, + UVHTTP_DELETE, + UVHTTP_HEAD, + UVHTTP_OPTIONS, + UVHTTP_PATCH +} uvhttp_method_t; + /* ========== Static Assertion Macro Definitions ========== */ #ifdef __cplusplus # define UVHTTP_STATIC_ASSERT(cond, msg) static_assert(cond, msg) diff --git a/include/uvhttp_request.h b/include/uvhttp_request.h index 520de886..b34831f2 100644 --- a/include/uvhttp_request.h +++ b/include/uvhttp_request.h @@ -20,17 +20,9 @@ extern "C" { #define MAX_URL_LEN 2048 #define MAX_BODY_LEN (1024 * 1024) // 1MB -// HTTP method enumeration -typedef enum { - UVHTTP_ANY = 0, - UVHTTP_GET, - UVHTTP_POST, - UVHTTP_PUT, - UVHTTP_DELETE, - UVHTTP_HEAD, - UVHTTP_OPTIONS, - UVHTTP_PATCH -} uvhttp_method_t; +/* uvhttp_method_t now lives in uvhttp_common.h — it is needed by both + * uvhttp_request.h and uvhttp_server.h, and those two include each other + * transitively, so the enum belongs in the shared header. */ typedef struct uvhttp_request uvhttp_request_t; diff --git a/include/uvhttp_server.h b/include/uvhttp_server.h index 38f5280d..038a48d4 100644 --- a/include/uvhttp_server.h +++ b/include/uvhttp_server.h @@ -25,6 +25,10 @@ typedef struct uvhttp_response uvhttp_response_t; typedef struct uvhttp_router uvhttp_router_t; typedef struct uvhttp_connection uvhttp_connection_t; +/* uvhttp_method_t comes from uvhttp_common.h (moved there in v2.10 to break + * the request.h <-> server.h cycle). uvhttp_request_handler_t is declared in + * uvhttp_request.h, which this header must not include. */ + #if UVHTTP_FEATURE_TLS typedef struct uvhttp_tls_context uvhttp_tls_context_t; #endif @@ -64,6 +68,14 @@ extern "C" { typedef struct uvhttp_server uvhttp_server_t; +/* One route table entry, for uvhttp_server_listen_routes. UVHTTP_ANY means + * "match every method". */ +typedef struct { + const char* path; + uvhttp_method_t method; + uvhttp_request_handler_t handler; +} uvhttp_route_t; + /* Server builder structure (unified API) */ typedef struct { uvhttp_server_t* server; @@ -224,6 +236,47 @@ uvhttp_error_t uvhttp_server_listen(uvhttp_server_t* server, const char* host, * afterwards to release it. */ uvhttp_error_t uvhttp_server_stop(uvhttp_server_t* server); + +/** + * @brief Create a server, register routes, and start listening - atomically + * + * @param loop Event loop (owned by the caller; the server never closes it) + * @param routes Route table (may be NULL only when route_count is 0) + * @param route_count Number of entries in routes + * @param host Bind address (e.g. "0.0.0.0" or "127.0.0.1") + * @param port TCP port to bind + * @param server Output parameter; set to NULL on every failure path + * @return UVHTTP_OK on success, error code otherwise + * + * @note This is the ATOMIC form of what used to be five separate calls: + * uvhttp_server_new -> uvhttp_router_new -> uvhttp_router_add_route xN + * -> uvhttp_server_take_router -> uvhttp_server_listen. + * Either the returned server is fully listening with every route + * registered, or nothing was created. There is no intermediate state + * and no cleanup for the caller to write. + * + * @note On success the server owns the routes (it builds and holds the + * router internally) and releases them in uvhttp_server_free. + * + * @note Added in v2.10. The five-step sequence remains available for callers + * that need to keep the router handle - prefer this for everything else. + * + * @example + * uvhttp_server_t* server = NULL; + * uvhttp_route_t routes[] = { + * {"/", UVHTTP_ANY, hello}, + * {"/health", UVHTTP_GET, health}, + * }; + * if (uvhttp_server_listen_routes(loop, routes, 2, "0.0.0.0", 8080, + * &server) != UVHTTP_OK) { + * // nothing was allocated; nothing to clean up + * } + */ +uvhttp_error_t uvhttp_server_listen_routes(uv_loop_t* loop, + const uvhttp_route_t* routes, + size_t route_count, const char* host, + int port, + uvhttp_server_t** server); #if UVHTTP_FEATURE_TLS uvhttp_error_t uvhttp_server_enable_tls(uvhttp_server_t* server, uvhttp_tls_context_t* tls_ctx); @@ -235,8 +288,8 @@ uvhttp_error_t uvhttp_server_disable_tls(uvhttp_server_t* server); * @param server Server to release (may be NULL, then this is a no-op) * @return UVHTTP_OK on success, error code otherwise * - * @note Ownership contract: once set with uvhttp_server_set_router / - * uvhttp_server_set_context, the router and context are owned by the + * @note Ownership contract: once set with uvhttp_server_take_router / + * uvhttp_server_take_context, the router and context are owned by the * server and are released by this function. The caller MUST NOT free * them separately, otherwise the double free occurs. The same applies * to any config reached through the context. @@ -254,30 +307,40 @@ uvhttp_error_t uvhttp_server_free(uvhttp_server_t* server); uvhttp_error_t uvhttp_server_set_handler(uvhttp_server_t* server, uvhttp_request_handler_t handler); /** - * @brief Attach a router to the server + * @brief Hand a router over to the server (ownership transfer) * * @param server Server to configure * @param router Router created with uvhttp_router_new * @return UVHTTP_OK on success, error code otherwise * - * @note Ownership contract: after this call the server owns the router and - * releases it in uvhttp_server_free. Do NOT call uvhttp_router_free on - * it yourself. + * @note OWNERSHIP TRANSFERS HERE. After this call the server owns the router + * and releases it in uvhttp_server_free. Do NOT call uvhttp_router_free + * on it yourself - doing so is a double free. + * + * @note Renamed from uvhttp_server_set_router in v2.10. The old name implied + * a borrow while the behavior was a transfer; that mismatch is what made + * examples/embedding/main.c double-free on its listen-failure path. + * + * @note Returns UVHTTP_ERROR_INVALID_PARAM if the server already owns a + * different router, rather than silently leaking the previous one. */ -uvhttp_error_t uvhttp_server_set_router(uvhttp_server_t* server, - uvhttp_router_t* router); +uvhttp_error_t uvhttp_server_take_router(uvhttp_server_t* server, + uvhttp_router_t* router); /** - * @brief Attach a context (shared configuration/state) to the server + * @brief Hand a context over to the server (ownership transfer) * * @param server Server to configure * @param context Context created with uvhttp_context_create * @return UVHTTP_OK on success, error code otherwise * - * @note Ownership contract: after this call the server owns the context and - * releases it in uvhttp_server_free. Do NOT free it yourself. + * @note OWNERSHIP TRANSFERS HERE. After this call the server owns the context + * and releases it in uvhttp_server_free. Do NOT free it yourself. + * + * @note Renamed from uvhttp_server_set_context in v2.10, for the same reason + * as uvhttp_server_take_router. */ -uvhttp_error_t uvhttp_server_set_context(uvhttp_server_t* server, - struct uvhttp_context* context); +uvhttp_error_t uvhttp_server_take_context(uvhttp_server_t* server, + struct uvhttp_context* context); #if UVHTTP_FEATURE_RATE_LIMIT /* ========== Rate Limiting API (Core Functionality) ========== */ @@ -409,10 +472,13 @@ uvhttp_server_builder_t* uvhttp_set_timeout(uvhttp_server_builder_t* server, uvhttp_server_builder_t* uvhttp_set_max_body_size( uvhttp_server_builder_t* server, size_t size); -/* Convenient request parameter access */ -const char* uvhttp_get_param(uvhttp_request_t* request, const char* name); -const char* uvhttp_get_header(uvhttp_request_t* request, const char* name); -const char* uvhttp_get_body(uvhttp_request_t* request); +/* Request accessors live in uvhttp_request.h: + * uvhttp_request_get_header() (was also uvhttp_get_header) + * uvhttp_request_get_query_param() (was also uvhttp_get_param) + * uvhttp_request_get_body() (was also uvhttp_get_body) + * The short aliases were removed in v2.10 - they were pure forwarders that + * gave the request API two entry points with no basis for choosing between + * them, and they were declared here despite operating on the request. */ /* Health check endpoint. * Registers a handler at the given path that returns HTTP 200 with diff --git a/manual/test_tls_e2e.c b/manual/test_tls_e2e.c index 0e9077d9..6ac2ed6d 100644 --- a/manual/test_tls_e2e.c +++ b/manual/test_tls_e2e.c @@ -117,7 +117,7 @@ int main(int argc, char** argv) { return 1; } memset(server_ctx, 0, sizeof(struct uvhttp_context)); - result = uvhttp_server_set_context(ctx->server, server_ctx); + result = uvhttp_server_take_context(ctx->server, server_ctx); if (result != UVHTTP_OK) { fprintf(stderr, "Failed to set context: %s\n", uvhttp_error_string(result)); uvhttp_free(server_ctx); diff --git a/src/uvhttp_server.c b/src/uvhttp_server.c index 333ea485..6c18919e 100644 --- a/src/uvhttp_server.c +++ b/src/uvhttp_server.c @@ -642,11 +642,109 @@ uvhttp_error_t uvhttp_server_set_handler(uvhttp_server_t* server, return UVHTTP_OK; } -uvhttp_error_t uvhttp_server_set_router(uvhttp_server_t* server, - uvhttp_router_t* router) { +/* ========== Atomic construction ========== */ + +uvhttp_error_t uvhttp_server_listen_routes(uv_loop_t* loop, + const uvhttp_route_t* routes, + size_t route_count, const char* host, + int port, uvhttp_server_t** server) { if (!server) { return UVHTTP_ERROR_INVALID_PARAM; } + *server = NULL; + + if (!loop || !host) { + return UVHTTP_ERROR_INVALID_PARAM; + } + if (route_count > 0 && !routes) { + return UVHTTP_ERROR_INVALID_PARAM; + } + + /* This is the atomic form of the five-step sequence the API used to + * require (server_new -> router_new -> add_route xN -> set_router -> + * listen). Every partial state below is cleaned up before returning, so + * the caller either gets a fully listening server or nothing at all - + * no hand-written rollback branches, no ownership questions. */ + + uvhttp_server_t* s = NULL; + uvhttp_error_t err = uvhttp_server_new(loop, &s); + if (err != UVHTTP_OK) { + return err; + } + + /* Routes may be empty (a server that only serves via a later + * uvhttp_server_take_router or a raw handler). */ + if (route_count > 0) { + uvhttp_router_t* router = NULL; + err = uvhttp_router_new(&router); + if (err != UVHTTP_OK) { + uvhttp_server_free(s); + return err; + } + + for (size_t i = 0; i < route_count; i++) { + if (!routes[i].path || !routes[i].handler) { + uvhttp_router_free(router); + uvhttp_server_free(s); + return UVHTTP_ERROR_INVALID_PARAM; + } + err = routes[i].method == UVHTTP_ANY + ? uvhttp_router_add_route(router, routes[i].path, + routes[i].handler) + : uvhttp_router_add_route_method(router, routes[i].path, + routes[i].method, + routes[i].handler); + if (err != UVHTTP_OK) { + /* Router is still owned locally here - freeing it before the + * server is the correct order, and the server has not taken + * ownership yet, so there is nothing to un-claim. */ + uvhttp_router_free(router); + uvhttp_server_free(s); + return err; + } + } + + err = uvhttp_server_take_router(s, router); + if (err != UVHTTP_OK) { + uvhttp_router_free(router); + uvhttp_server_free(s); + return err; + } + } + + err = uvhttp_server_listen(s, host, port); + if (err != UVHTTP_OK) { + /* The server owns the router by now, so uvhttp_server_free releases + * it. Freeing the router here as well is exactly the double free the + * old five-step sequence invited. */ + uvhttp_server_free(s); + return err; + } + + *server = s; + return UVHTTP_OK; +} + +uvhttp_error_t uvhttp_server_take_router(uvhttp_server_t* server, + uvhttp_router_t* router) { + if (!server) { + return UVHTTP_ERROR_INVALID_PARAM; + } + + /* Ownership transfer is deliberate and is now named as such + * (uvhttp_server_take_router, v2.10; was uvhttp_server_take_router). + * "set" implied a borrow, but the server frees the router in + * uvhttp_server_free - a mismatch that made the official embedding + * example double-free on its listen-failure path. + * + * Handing over a router that is already owned leaves the previous one + * leaked; reject it rather than silently drop the reference. */ + if (server->router && server->router != router) { + UVHTTP_LOG_ERROR( + "server already owns a router - free it first or use a server " + "that has none"); + return UVHTTP_ERROR_INVALID_PARAM; + } server->router = router; return UVHTTP_OK; @@ -688,12 +786,18 @@ uvhttp_error_t uvhttp_server_enable_health_check(uvhttp_server_t* server, return UVHTTP_OK; } -uvhttp_error_t uvhttp_server_set_context(uvhttp_server_t* server, - struct uvhttp_context* context) { +uvhttp_error_t uvhttp_server_take_context(uvhttp_server_t* server, + struct uvhttp_context* context) { if (!server) { return UVHTTP_ERROR_INVALID_PARAM; } + /* Ownership transfer - see uvhttp_server_take_router. */ + if (server->context && server->context != context) { + UVHTTP_LOG_ERROR("server already owns a context - free it first"); + return UVHTTP_ERROR_INVALID_PARAM; + } + server->context = context; return UVHTTP_OK; } @@ -927,19 +1031,6 @@ uvhttp_server_builder_t* uvhttp_set_max_body_size( return server; } -// Convenient request parameter get -const char* uvhttp_get_param(uvhttp_request_t* request, const char* name) { - return uvhttp_request_get_query_param(request, name); -} - -const char* uvhttp_get_header(uvhttp_request_t* request, const char* name) { - return uvhttp_request_get_header(request, name); -} - -const char* uvhttp_get_body(uvhttp_request_t* request) { - return uvhttp_request_get_body(request); -} - // Server run and cleanup int uvhttp_server_run(uvhttp_server_builder_t* server) { if (!server || !server->loop) diff --git a/test/unit/test_boundary_null_safety.cpp b/test/unit/test_boundary_null_safety.cpp index 6a1318c9..b9c15d7f 100644 --- a/test/unit/test_boundary_null_safety.cpp +++ b/test/unit/test_boundary_null_safety.cpp @@ -282,7 +282,7 @@ TEST(UvhttpHealthCheckTest, EnableOnExistingRouter) { uvhttp_router_t* router = NULL; uvhttp_router_new(&router); - uvhttp_server_set_router(srv, router); + uvhttp_server_take_router(srv, router); uvhttp_error_t err = uvhttp_server_enable_health_check(srv, "/healthz"); EXPECT_EQ(err, UVHTTP_OK); diff --git a/test/unit/test_connection_integration_coverage.cpp b/test/unit/test_connection_integration_coverage.cpp index 2272db90..4bf962ef 100644 --- a/test/unit/test_connection_integration_coverage.cpp +++ b/test/unit/test_connection_integration_coverage.cpp @@ -65,7 +65,7 @@ static int setup_server(uv_loop_t* loop, uvhttp_server_t** server) { uvhttp_router_new(&router); if (router) { uvhttp_router_add_route(router, "/test", test_handler); - uvhttp_server_set_router(*server, router); + uvhttp_server_take_router(*server, router); } err = uvhttp_server_listen(*server, "127.0.0.1", 0); diff --git a/test/unit/test_death.cpp b/test/unit/test_death.cpp index 0ee8f82b..3b7854f7 100644 --- a/test/unit/test_death.cpp +++ b/test/unit/test_death.cpp @@ -67,7 +67,7 @@ TEST(DeathTest, NullServerSetRouter) { uvhttp_error_t result = uvhttp_router_new(&router); ASSERT_EQ(result, UVHTTP_OK); ASSERT_NE(router, nullptr); - result = uvhttp_server_set_router(nullptr, router); + result = uvhttp_server_take_router(nullptr, router); EXPECT_NE(result, UVHTTP_OK); uvhttp_router_free(router); } diff --git a/test/unit/test_memory.cpp b/test/unit/test_memory.cpp index 4abed194..181253cf 100644 --- a/test/unit/test_memory.cpp +++ b/test/unit/test_memory.cpp @@ -68,7 +68,7 @@ TEST(MemoryTest, ServerMemoryUsage) { ASSERT_EQ(result, UVHTTP_OK); ASSERT_NE(router, nullptr); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); uvhttp_server_free(server); /* router 已被 server 释放,不需要再释放 */ @@ -98,7 +98,7 @@ TEST(MemoryTest, MultipleServerCreation) { ASSERT_NE(router, nullptr); ASSERT_NE(router, nullptr); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); result = uvhttp_server_listen(server, "127.0.0.1", 0); ASSERT_EQ(result, UVHTTP_OK); diff --git a/test/unit/test_protocol_upgrade_boost_coverage.cpp b/test/unit/test_protocol_upgrade_boost_coverage.cpp index 236eae8f..c8d5c562 100644 --- a/test/unit/test_protocol_upgrade_boost_coverage.cpp +++ b/test/unit/test_protocol_upgrade_boost_coverage.cpp @@ -532,7 +532,7 @@ TEST_F(ConnectionHelperTest, GetFd_WithAcceptedConnection_ReturnsValidFd) { rerr = uvhttp_router_add_route_method(router, "/test", UVHTTP_GET, fd_peeraddr_handler); ASSERT_EQ(rerr, UVHTTP_OK); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // Listen on port 0 uvhttp_error_t serr = uvhttp_server_listen(server, "127.0.0.1", 0); @@ -614,7 +614,7 @@ TEST_F(ConnectionHelperTest, GetPeerAddress_WithAcceptedConnection_ReturnsAddr) rerr = uvhttp_router_add_route_method(router, "/test", UVHTTP_GET, fd_peeraddr_handler); ASSERT_EQ(rerr, UVHTTP_OK); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); // Listen on port 0 uvhttp_error_t serr = uvhttp_server_listen(server, "127.0.0.1", 0); diff --git a/test/unit/test_request_parser_callbacks.cpp b/test/unit/test_request_parser_callbacks.cpp index 33238c6d..96e48448 100644 --- a/test/unit/test_request_parser_callbacks.cpp +++ b/test/unit/test_request_parser_callbacks.cpp @@ -645,7 +645,7 @@ TEST_F(ParserCallbackTest, RouterDispatch_HandlerFound) { UVHTTP_OK); // Attach router to server - ASSERT_EQ(uvhttp_server_set_router(server, router), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); s_router_handler_called = 0; @@ -674,7 +674,7 @@ TEST_F(ParserCallbackTest, RouterDispatch_NoHandler_404) { test_route_handler), UVHTTP_OK); - ASSERT_EQ(uvhttp_server_set_router(server, router), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); s_router_handler_called = 0; @@ -708,7 +708,7 @@ TEST_F(ParserCallbackTest, RouterDispatch_PostMethodMatch) { test_route_handler), UVHTTP_OK); - ASSERT_EQ(uvhttp_server_set_router(server, router), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); s_router_handler_called = 0; @@ -736,7 +736,7 @@ TEST_F(ParserCallbackTest, RouterDispatch_WrongMethod_404) { test_route_handler), UVHTTP_OK); - ASSERT_EQ(uvhttp_server_set_router(server, router), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); s_router_handler_called = 0; diff --git a/test/unit/test_request_response_coverage.cpp b/test/unit/test_request_response_coverage.cpp index 3c8121ec..64d293c7 100644 --- a/test/unit/test_request_response_coverage.cpp +++ b/test/unit/test_request_response_coverage.cpp @@ -1042,7 +1042,7 @@ TEST_F(RequestCoverageTest, Router_NoMatch_404Response) { ASSERT_EQ(uvhttp_router_add_route_method(router, "/other", UVHTTP_GET, cov_route_handler), UVHTTP_OK); - ASSERT_EQ(uvhttp_server_set_router(server, router), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); const char* raw = "GET /nonexistent HTTP/1.1\r\nHost: x\r\n\r\n"; int rc = Execute(raw); @@ -1059,7 +1059,7 @@ TEST_F(RequestCoverageTest, Router_Match_HandlerCalled) { ASSERT_EQ(uvhttp_router_add_route_method(router, "/api/test", UVHTTP_GET, cov_route_handler), UVHTTP_OK); - ASSERT_EQ(uvhttp_server_set_router(server, router), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); const char* raw = "GET /api/test HTTP/1.1\r\nHost: x\r\n\r\n"; int rc = Execute(raw); diff --git a/test/unit/test_server_api_coverage.cpp b/test/unit/test_server_api_coverage.cpp index d499bdf8..d6e1ed15 100644 --- a/test/unit/test_server_api_coverage.cpp +++ b/test/unit/test_server_api_coverage.cpp @@ -97,7 +97,7 @@ TEST_F(UvhttpServerApiTest, ServerSetRouter) { ASSERT_NE(router, nullptr); /* 设置路由 */ - uvhttp_error_t result = uvhttp_server_set_router(server, router); + uvhttp_error_t result = uvhttp_server_take_router(server, router); EXPECT_EQ(result, UVHTTP_OK); /* 验证路由已设置 */ @@ -111,7 +111,7 @@ TEST_F(UvhttpServerApiTest, ServerSetRouterNullServer) { ASSERT_EQ(result, UVHTTP_OK); ASSERT_NE(router, nullptr); - result = uvhttp_server_set_router(NULL, router); + result = uvhttp_server_take_router(NULL, router); EXPECT_NE(result, UVHTTP_OK); uvhttp_router_free(router); @@ -122,7 +122,7 @@ TEST_F(UvhttpServerApiTest, ServerSetRouterNullRouter) { ASSERT_EQ(create_server(), UVHTTP_OK); /* 设置NULL router是允许的,用于清除路由 */ - uvhttp_error_t result = uvhttp_server_set_router(server, NULL); + uvhttp_error_t result = uvhttp_server_take_router(server, NULL); EXPECT_EQ(result, UVHTTP_OK); EXPECT_EQ(server->router, nullptr); } @@ -139,7 +139,7 @@ TEST_F(UvhttpServerApiTest, ServerSetContext) { ASSERT_NE(context, nullptr); /* 设置上下文 */ - result = uvhttp_server_set_context(server, context); + result = uvhttp_server_take_context(server, context); EXPECT_EQ(result, UVHTTP_OK); /* 验证上下文已设置 */ @@ -154,7 +154,7 @@ TEST_F(UvhttpServerApiTest, ServerSetContextNullServer) { ASSERT_EQ(result, UVHTTP_OK); - result = uvhttp_server_set_context(NULL, context); + result = uvhttp_server_take_context(NULL, context); EXPECT_NE(result, UVHTTP_OK); diff --git a/test/unit/test_server_atomic_construct.cpp b/test/unit/test_server_atomic_construct.cpp new file mode 100644 index 00000000..70884843 --- /dev/null +++ b/test/unit/test_server_atomic_construct.cpp @@ -0,0 +1,249 @@ +/* Atomicity tests for uvhttp_server_listen_routes and the ownership + * contract of uvhttp_server_take_router / uvhttp_server_take_context. + * + * Background: before v2.10, starting a server took five separate calls + * (server_new -> router_new -> add_route xN -> set_router -> listen) and the + * caller had to hand-write the rollback for each failure. The official + * embedding example got that rollback wrong: on listen failure it called + * uvhttp_router_free(router) and then uvhttp_server_free(server), which frees + * the router again - a double free (reproduced under ASan). + * + * These tests pin down the properties that fix is supposed to guarantee: + * - all-or-nothing construction (no partial state on any failure path) + * - *server is NULL on every failure + * - ownership transfer is total and cannot be double-applied + */ + +#include + +extern "C" { +#include "uvhttp.h" +#include "uvhttp_context.h" +} + +namespace { + +int ok_handler(uvhttp_request_t* req, uvhttp_response_t* resp) { + (void)req; + (void)resp; + return 0; +} + +} // namespace + +/* ---------- success path ---------- */ + +TEST(UvhttpServerAtomicConstruct, BuildsListeningServerWithRoutes) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, ok_handler}, + {"/health", UVHTTP_GET, ok_handler}, + {"/submit", UVHTTP_POST, ok_handler}, + }; + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_listen_routes(loop, routes, 3, "127.0.0.1", 0, + &server), + UVHTTP_OK) + << "atomic construction should succeed"; + ASSERT_NE(server, nullptr); + + uvhttp_server_free(server); +} + +/* A route table of zero entries is legal: a server that serves via a handler + * or a router handed over later. */ +TEST(UvhttpServerAtomicConstruct, ZeroRoutesIsAllowed) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_listen_routes(loop, nullptr, 0, "127.0.0.1", 0, + &server), + UVHTTP_OK); + ASSERT_NE(server, nullptr); + + uvhttp_server_free(server); +} + +/* ---------- failure paths: *server must be NULL, nothing allocated ---------- */ + +TEST(UvhttpServerAtomicConstruct, NullOutParamIsRejected) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + EXPECT_EQ(uvhttp_server_listen_routes(loop, nullptr, 0, "127.0.0.1", 0, + nullptr), + UVHTTP_ERROR_INVALID_PARAM); +} + +TEST(UvhttpServerAtomicConstruct, NullLoopIsRejected) { + uvhttp_server_t* server = nullptr; + EXPECT_EQ(uvhttp_server_listen_routes(nullptr, nullptr, 0, "127.0.0.1", 0, + &server), + UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ(server, nullptr) << "out param must stay NULL on failure"; +} + +TEST(UvhttpServerAtomicConstruct, NullHostIsRejected) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + EXPECT_EQ(uvhttp_server_listen_routes(loop, nullptr, 0, nullptr, 0, + &server), + UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ(server, nullptr); +} + +/* count > 0 with a NULL table is a caller bug; must not dereference. */ +TEST(UvhttpServerAtomicConstruct, NullRoutesWithNonZeroCountIsRejected) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + EXPECT_EQ(uvhttp_server_listen_routes(loop, nullptr, 3, "127.0.0.1", 0, + &server), + UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ(server, nullptr); +} + +/* A malformed entry (NULL path or NULL handler) must abort construction + * cleanly - this is the case the five-step version made the caller handle. */ +TEST(UvhttpServerAtomicConstruct, NullPathInRouteTableIsRejected) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, ok_handler}, + {nullptr, UVHTTP_ANY, ok_handler}, + }; + + uvhttp_server_t* server = nullptr; + EXPECT_EQ(uvhttp_server_listen_routes(loop, routes, 2, "127.0.0.1", 0, + &server), + UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ(server, nullptr) + << "a bad entry must abort the whole construction, not half-apply it"; +} + +TEST(UvhttpServerAtomicConstruct, NullHandlerInRouteTableIsRejected) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, nullptr}, + }; + + uvhttp_server_t* server = nullptr; + EXPECT_EQ(uvhttp_server_listen_routes(loop, routes, 1, "127.0.0.1", 0, + &server), + UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ(server, nullptr); +} + +/* Port 1 is privileged, so bind fails. This is the exact path the embedding + * example got wrong: the server already owned the router at this point, so + * the cleanup must be server_free ALONE. Under ASan/valgrind a double free + * here is the regression signal. */ +TEST(UvhttpServerAtomicConstruct, ListenFailureLeavesNothingBehind) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, ok_handler}, + }; + + uvhttp_server_t* server = nullptr; + const uvhttp_error_t err = + uvhttp_server_listen_routes(loop, routes, 1, "127.0.0.1", 1, &server); + + /* Binding a privileged port must fail. If it somehow succeeds, the test + * would leak the server, so free it and skip rather than assert. */ + if (err == UVHTTP_OK) { + uvhttp_server_free(server); + GTEST_SKIP() << "running as root: privileged port bind succeeded"; + } + + EXPECT_EQ(server, nullptr) + << "out param must be NULL after a failed bind; the router the " + "server had already taken ownership of must not be freed twice"; +} + +/* ---------- ownership: take_router ---------- */ + +TEST(UvhttpServerOwnership, TakeRouterRejectsNullServer) { + EXPECT_EQ(uvhttp_server_take_router(nullptr, nullptr), + UVHTTP_ERROR_INVALID_PARAM); +} + +TEST(UvhttpServerOwnership, TakeContextRejectsNullServer) { + EXPECT_EQ(uvhttp_server_take_context(nullptr, nullptr), + UVHTTP_ERROR_INVALID_PARAM); +} + +/* Handing over a second, different router would leak the first one - the old + * implementation silently overwrote the pointer. Now it is rejected. */ +TEST(UvhttpServerOwnership, SecondRouterIsRejectedNotSilentlyDropped) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_new(loop, &server), UVHTTP_OK); + + uvhttp_router_t* first = nullptr; + ASSERT_EQ(uvhttp_router_new(&first), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_router(server, first), UVHTTP_OK); + + uvhttp_router_t* second = nullptr; + ASSERT_EQ(uvhttp_router_new(&second), UVHTTP_OK); + EXPECT_EQ(uvhttp_server_take_router(server, second), + UVHTTP_ERROR_INVALID_PARAM) + << "replacing an owned router must be refused, not silently leak it"; + + /* The rejected router is still ours to free; the server frees its own. */ + uvhttp_router_free(second); + uvhttp_server_free(server); +} + +/* Re-taking the SAME router is a no-op, not an error: it keeps idempotent + * teardown paths (e.g. a helper that hands over a router it already gave + * away) working. */ +TEST(UvhttpServerOwnership, RetakingSameRouterIsIdempotent) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_new(loop, &server), UVHTTP_OK); + + uvhttp_router_t* router = nullptr; + ASSERT_EQ(uvhttp_router_new(&router), UVHTTP_OK); + ASSERT_EQ(uvhttp_router_add_route(router, "/", ok_handler), UVHTTP_OK); + + ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); + EXPECT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); + + uvhttp_server_free(server); +} + +TEST(UvhttpServerOwnership, SecondContextIsRejected) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_new(loop, &server), UVHTTP_OK); + + uvhttp_context_t* first = nullptr; + ASSERT_EQ(uvhttp_context_create(loop, &first), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_context(server, first), UVHTTP_OK); + + uvhttp_context_t* second = nullptr; + ASSERT_EQ(uvhttp_context_create(loop, &second), UVHTTP_OK); + EXPECT_EQ(uvhttp_server_take_context(server, second), + UVHTTP_ERROR_INVALID_PARAM); + + uvhttp_context_destroy(second); + uvhttp_server_free(server); +} diff --git a/test/unit/test_server_boost_coverage.cpp b/test/unit/test_server_boost_coverage.cpp index c28ba03b..32768329 100644 --- a/test/unit/test_server_boost_coverage.cpp +++ b/test/unit/test_server_boost_coverage.cpp @@ -93,7 +93,7 @@ TEST_F(ServerSetFunctionsTest, SetRouter_ValidServer) { ASSERT_EQ(rerr, UVHTTP_OK); ASSERT_NE(router, nullptr); - uvhttp_error_t err = uvhttp_server_set_router(server, router); + uvhttp_error_t err = uvhttp_server_take_router(server, router); EXPECT_EQ(err, UVHTTP_OK); EXPECT_EQ(server->router, router); @@ -102,18 +102,18 @@ TEST_F(ServerSetFunctionsTest, SetRouter_ValidServer) { } TEST_F(ServerSetFunctionsTest, SetRouter_NullServer) { - uvhttp_error_t err = uvhttp_server_set_router(server, nullptr); + uvhttp_error_t err = uvhttp_server_take_router(server, nullptr); // NULL router is a valid operation (clears router) EXPECT_EQ(err, UVHTTP_OK); } TEST_F(ServerSetFunctionsTest, SetContext_ValidServer) { - uvhttp_error_t err = uvhttp_server_set_context(server, nullptr); + uvhttp_error_t err = uvhttp_server_take_context(server, nullptr); EXPECT_EQ(err, UVHTTP_OK); } TEST_F(ServerSetFunctionsTest, SetContext_NullServer) { - uvhttp_error_t err = uvhttp_server_set_context(nullptr, nullptr); + uvhttp_error_t err = uvhttp_server_take_context(nullptr, nullptr); EXPECT_EQ(err, UVHTTP_ERROR_INVALID_PARAM); } @@ -313,17 +313,17 @@ TEST_F(BuilderAPITest, SetMaxBodySize_Valid) { // --- Convenient request parameter access --- TEST_F(BuilderAPITest, GetParam_NullRequest) { - const char* result = uvhttp_get_param(nullptr, "key"); + const char* result = uvhttp_request_get_query_param(nullptr, "key"); EXPECT_EQ(result, nullptr); } TEST_F(BuilderAPITest, GetHeader_NullRequest) { - const char* result = uvhttp_get_header(nullptr, "Content-Type"); + const char* result = uvhttp_request_get_header(nullptr, "Content-Type"); EXPECT_EQ(result, nullptr); } TEST_F(BuilderAPITest, GetBody_NullRequest) { - const char* result = uvhttp_get_body(nullptr); + const char* result = uvhttp_request_get_body(nullptr); EXPECT_EQ(result, nullptr); } @@ -1887,7 +1887,7 @@ TEST_F(TcpHttpCycleTest, FullHttpRequestResponseCycle) { ASSERT_EQ(rerr, UVHTTP_OK); // Attach router to server - uvhttp_error_t serr = uvhttp_server_set_router(server, router); + uvhttp_error_t serr = uvhttp_server_take_router(server, router); ASSERT_EQ(serr, UVHTTP_OK); // Listen on port 0 (OS assigns random port) @@ -2206,7 +2206,7 @@ TEST_F(TcpHttpCycleTest, SendResponseData_DirectCall_WithRealConnection) { send_data_direct_handler); ASSERT_EQ(rerr, UVHTTP_OK); - uvhttp_error_t serr = uvhttp_server_set_router(server, router); + uvhttp_error_t serr = uvhttp_server_take_router(server, router); ASSERT_EQ(serr, UVHTTP_OK); serr = uvhttp_server_listen(server, "127.0.0.1", 0); @@ -2322,7 +2322,7 @@ TEST_F(TcpHttpCycleTest, ResponseSendRaw_KeepAlive0_SetsConnKeepAlive) { keepalive_close_handler); ASSERT_EQ(rerr, UVHTTP_OK); - uvhttp_error_t serr = uvhttp_server_set_router(server, router); + uvhttp_error_t serr = uvhttp_server_take_router(server, router); ASSERT_EQ(serr, UVHTTP_OK); serr = uvhttp_server_listen(server, "127.0.0.1", 0); diff --git a/test/unit/test_server_boost_coverage2.cpp b/test/unit/test_server_boost_coverage2.cpp index ce0859cb..a6e1ccc7 100644 --- a/test/unit/test_server_boost_coverage2.cpp +++ b/test/unit/test_server_boost_coverage2.cpp @@ -129,7 +129,7 @@ TEST_F(ListenWithContextConfigTest, Listen_WithContextConfigBacklog) { uvhttp_config_set_current(context, config); // Set the context on the server - err = uvhttp_server_set_context(server, context); + err = uvhttp_server_take_context(server, context); ASSERT_EQ(err, UVHTTP_OK); // Set a handler @@ -161,7 +161,7 @@ TEST_F(ListenWithContextConfigTest, Listen_WithContextNullConfig) { ASSERT_EQ(err, UVHTTP_OK); // Set the context on the server - err = uvhttp_server_set_context(server, context); + err = uvhttp_server_take_context(server, context); ASSERT_EQ(err, UVHTTP_OK); // Listen - context exists but config is NULL, uses default backlog @@ -187,7 +187,7 @@ TEST_F(ListenWithContextConfigTest, Listen_WithContextConfigBacklogZero) { uvhttp_config_set_current(context, config); // Set the context on the server - err = uvhttp_server_set_context(server, context); + err = uvhttp_server_take_context(server, context); ASSERT_EQ(err, UVHTTP_OK); // Listen - config->backlog is 0, so default backlog is used diff --git a/test/unit/test_server_error_coverage.cpp b/test/unit/test_server_error_coverage.cpp index 8ae52449..bd898b10 100644 --- a/test/unit/test_server_error_coverage.cpp +++ b/test/unit/test_server_error_coverage.cpp @@ -69,7 +69,7 @@ TEST(UvhttpServerErrorCoverageTest, SetHandlerNullParams) { /* 测试设置路由器 NULL 参数 */ TEST(UvhttpServerErrorCoverageTest, SetRouterNullParams) { /* 测试 NULL 服务器 */ - uvhttp_error_t result = uvhttp_server_set_router(NULL, NULL); + uvhttp_error_t result = uvhttp_server_take_router(NULL, NULL); EXPECT_NE(result, UVHTTP_OK); /* 测试 NULL 路由器 */ @@ -77,7 +77,7 @@ TEST(UvhttpServerErrorCoverageTest, SetRouterNullParams) { result = uvhttp_server_new(uv_default_loop(), &server); ASSERT_EQ(result, UVHTTP_OK) << "server 创建失败"; ASSERT_NE(server, nullptr); - result = uvhttp_server_set_router(server, NULL); + result = uvhttp_server_take_router(server, NULL); EXPECT_EQ(result, UVHTTP_OK); uvhttp_server_free(server); } @@ -85,7 +85,7 @@ TEST(UvhttpServerErrorCoverageTest, SetRouterNullParams) { /* 测试设置上下文 NULL 参数 */ TEST(UvhttpServerErrorCoverageTest, SetContextNullParams) { /* 测试 NULL 服务器 */ - uvhttp_error_t result = uvhttp_server_set_context(NULL, NULL); + uvhttp_error_t result = uvhttp_server_take_context(NULL, NULL); EXPECT_NE(result, UVHTTP_OK); /* 测试 NULL 上下文 */ @@ -93,7 +93,7 @@ TEST(UvhttpServerErrorCoverageTest, SetContextNullParams) { result = uvhttp_server_new(uv_default_loop(), &server); ASSERT_EQ(result, UVHTTP_OK) << "server 创建失败"; ASSERT_NE(server, nullptr); - result = uvhttp_server_set_context(server, NULL); + result = uvhttp_server_take_context(server, NULL); EXPECT_EQ(result, UVHTTP_OK); uvhttp_server_free(server); } @@ -248,7 +248,7 @@ TEST(UvhttpServerErrorCoverageTest, ServerCreateAndListen) { result = uvhttp_router_new(&router); if (result == UVHTTP_OK && router != NULL) { uvhttp_router_add_route(router, "/", NULL); - uvhttp_server_set_router(server, router); + uvhttp_server_take_router(server, router); /* 尝试监听(可能失败,这是预期的) */ result = uvhttp_server_listen(server, "127.0.0.1", 18080); diff --git a/test/unit/test_server_simple_handlers_coverage.cpp b/test/unit/test_server_simple_handlers_coverage.cpp index abde2160..34ef2bb3 100644 --- a/test/unit/test_server_simple_handlers_coverage.cpp +++ b/test/unit/test_server_simple_handlers_coverage.cpp @@ -28,7 +28,7 @@ TEST(UvhttpServerSimpleHandlersTest, GetParam) { request.path = request.url; /* 获取参数 */ - const char* param = uvhttp_get_param(&request, "name"); + const char* param = uvhttp_request_get_query_param(&request, "name"); /* probe 实测正常返回。去掉 if(param) 守卫——param 为 NULL 时应显式 * 失败而非跳过断言(恒绿假阳性)。 */ ASSERT_NE(param, nullptr); @@ -37,7 +37,7 @@ TEST(UvhttpServerSimpleHandlersTest, GetParam) { /* 测试获取参数 NULL 请求 */ TEST(UvhttpServerSimpleHandlersTest, GetParamNullRequest) { - const char* param = uvhttp_get_param(NULL, "name"); + const char* param = uvhttp_request_get_query_param(NULL, "name"); EXPECT_EQ(param, nullptr); } @@ -46,7 +46,7 @@ TEST(UvhttpServerSimpleHandlersTest, GetParamNullName) { uvhttp_request_t request; memset(&request, 0, sizeof(request)); - const char* param = uvhttp_get_param(&request, NULL); + const char* param = uvhttp_request_get_query_param(&request, NULL); EXPECT_EQ(param, nullptr); } @@ -67,14 +67,14 @@ TEST(UvhttpServerSimpleHandlersTest, GetHeader) { ASSERT_EQ( uvhttp_request_add_header(&request, "Content-Type", "application/json"), UVHTTP_OK); - const char* header = uvhttp_get_header(&request, "Content-Type"); + const char* header = uvhttp_request_get_header(&request, "Content-Type"); ASSERT_NE(header, nullptr); EXPECT_STREQ(header, "application/json"); } /* 测试获取请求头 NULL 请求 */ TEST(UvhttpServerSimpleHandlersTest, GetHeaderNullRequest) { - const char* header = uvhttp_get_header(NULL, "Content-Type"); + const char* header = uvhttp_request_get_header(NULL, "Content-Type"); EXPECT_EQ(header, nullptr); } @@ -83,7 +83,7 @@ TEST(UvhttpServerSimpleHandlersTest, GetHeaderNullName) { uvhttp_request_t request; memset(&request, 0, sizeof(request)); - const char* header = uvhttp_get_header(&request, NULL); + const char* header = uvhttp_request_get_header(&request, NULL); EXPECT_EQ(header, nullptr); } @@ -98,13 +98,13 @@ TEST(UvhttpServerSimpleHandlersTest, GetBody) { request.body_length = strlen(body); /* 获取请求体 */ - const char* body_content = uvhttp_get_body(&request); + const char* body_content = uvhttp_request_get_body(&request); ASSERT_NE(body_content, nullptr); EXPECT_STREQ(body_content, "Test body content"); } /* 测试获取请求体 NULL 请求 */ TEST(UvhttpServerSimpleHandlersTest, GetBodyNullRequest) { - const char* body = uvhttp_get_body(NULL); + const char* body = uvhttp_request_get_body(NULL); EXPECT_EQ(body, nullptr); } \ No newline at end of file diff --git a/test/unit/test_stress.cpp b/test/unit/test_stress.cpp index b0c8b59f..205ef00f 100644 --- a/test/unit/test_stress.cpp +++ b/test/unit/test_stress.cpp @@ -75,8 +75,8 @@ static void setup_stress_server() { // 添加路由 uvhttp_router_add_route(router, "/", simple_handler); - result = uvhttp_server_set_router(g_stress_server, router); - ASSERT_EQ(result, UVHTTP_OK) << "uvhttp_server_set_router failed: " << result; + result = uvhttp_server_take_router(g_stress_server, router); + ASSERT_EQ(result, UVHTTP_OK) << "uvhttp_server_take_router failed: " << result; result = uvhttp_server_listen(g_stress_server, STRESS_TEST_HOST, g_stress_test_port); ASSERT_EQ(result, UVHTTP_OK) << "uvhttp_server_listen failed: " << result << " (port: " << g_stress_test_port << ")"; diff --git a/test/unit/test_zerocopy_threshold_wire.cpp b/test/unit/test_zerocopy_threshold_wire.cpp index feb750c1..eb4e390c 100644 --- a/test/unit/test_zerocopy_threshold_wire.cpp +++ b/test/unit/test_zerocopy_threshold_wire.cpp @@ -282,7 +282,7 @@ class ZerocopyThresholdWireTest : public ::testing::Test { BodyHandler), UVHTTP_OK); } - uvhttp_server_set_router(server_, router); + uvhttp_server_take_router(server_, router); ASSERT_EQ(uvhttp_server_listen(server_, "127.0.0.1", 0), UVHTTP_OK); From 6681043990ec88ef27671e9016d445f1bddfc91d Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 9 Oct 2026 09:25:29 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs(brain):=20=E8=AE=B0=E5=BD=95=20v2.10?= =?UTF-8?q?=20=E6=96=B9=E5=90=91=E4=B8=8E=20API=20=E5=8E=9F=E5=AD=90?= =?UTF-8?q?=E6=80=A7=E5=AE=A1=E8=AE=A1=EF=BC=88=E5=88=A4=E6=8D=AE=E6=8C=89?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=AE=9A=E4=B9=89=E9=87=8D=E5=86=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## api-atomization-orthogonalization(新) 方向:停止新增特性,v2.10 专做 API 原子化与正交化。 兼容策略 B(用户拍板):直接破坏性改,不留 deprecated 层。 第一性原理核实后修正了一版错误诊断。初版记的是"两条配置通道可能矛盾", 按 config->field 逐字段核实后发现真因更严重:uvhttp_config_t 26 个字段中 14 个从不被任何执行路径读取(3 个仅写不读 + 11 个零读点)。 - update_size_limits / set_max_body_size 返回 UVHTTP_OK 并打 INFO 日志, 实际执行用编译期常量 UVHTTP_MAX_BODY_SIZE —— 零应用且报告成功 - set_timeout 写 request_timeout/keepalive_timeout(全库无读点), 实际超时读 connection_timeout —— 第三个字段 - max_connections 有三个可写位置,生效值取决于用了哪个构造入口 ## api-atomicity-audit(新) 判据按用户给定定义重写:**原子性 = API 不可再分割;正交性 = API 没有重合。** (此前用的 C1-C4 提交性/幂等/跨对象半状态是事务原子性,与此无关,已作废。) 原子性违约: - N1 构造一个可用 server 要 5 步且无原子版本 —— 实证来自官方嵌入示例 - N2 发送响应 4~5 步且无原子版本 - N3 设一个配置项要"选通道 + 装配 + 设置"三步 正交性违约: - O1 请求读取双份并存,三对全是纯别名,其中 uvhttp_get_param 名字有歧义 - O2 lru_cache 与 gzip_cache 逐字同构,迫使调用方写两遍相同代码 - O3 构造入口三重叠,差异只是切分顺序 - O4 free/cleanup/simple_free 三组同义命名 + 4 种返回类型 - O5 压缩 5 个入口 + 2 个判定辅助,判定与设置未分离 两个实测确认的数据丢失 / 崩溃(探针程序,非代码推断): - **N1 连带 double-free**:官方嵌入示例 listen 失败分支先 router_free 再 server_free,而 server 已通过 set_router 接管 router。ASan 确认。 同一模式在 4 个 examples + 英中双语嵌入指南中重复出现。 - **router 迁移致全丢**:13 个不同顶层路由 + 任意 :param 路由 -> migrate_to_trie 中途失败(child_indices[12] 定长上限撞顶)-> 已迁移 条目因 use_trie 仍为 0 而永不可达,全部路由静默丢失。12 条安全, 13 条全灭。现有测试零覆盖。 已做对、不要破坏的维度:无跨实例可变全局状态(TLS entropy 挂在 context 上,多 server 隔离天然成立);response 系 setter 先校验后写入。 ## roadmap v2.10.x 段重排,第一刀从"配置双通道合并"(基于错误诊断)改为 router 数据丢失修复 + 配置静默失效全线。 --- brain/index.md | 4 +- brain/pages/api-atomicity-audit.md | 294 ++++++++++++++++++ .../api-atomization-orthogonalization.md | 209 +++++++++++++ brain/roadmap.md | 49 ++- 4 files changed, 550 insertions(+), 6 deletions(-) create mode 100644 brain/pages/api-atomicity-audit.md create mode 100644 brain/pages/api-atomization-orthogonalization.md diff --git a/brain/index.md b/brain/index.md index 31087417..5ae84e02 100644 --- a/brain/index.md +++ b/brain/index.md @@ -1,8 +1,10 @@ # Brain Index -_Auto-generated. Last updated 2026-10-09T00:06:21.638Z._ +_Auto-generated. Last updated 2026-10-09T01:08:06.656Z._ - [alloc-hotpath-measured](pages/alloc-hotpath-measured.md) — category: decision | tags: [performance, benchmark, allocator, methodology, yagni] | # 每请求分配已实测:省一次仅 0.026%,内存分配优化不做 +- [api-atomicity-audit](pages/api-atomicity-audit.md) — category: decision | tags: [api, atomicity, orthogonality, audit, v2.10] | # API 原子化与正交性审计 +- [api-atomization-orthogonalization](pages/api-atomization-orthogonalization.md) — category: decision | tags: [api, design, roadmap, v2.10] | # v2.10 方向:API 原子化与正交化 - [benchmark-thermal-throttling](pages/benchmark-thermal-throttling.md) — category: decision | tags: [performance, benchmark, methodology] | # 禁止在本机做性能测量 —— 连配对 A/B 也不行 - [build-system-make-cmake](pages/build-system-make-cmake.md) — category: decision | tags: [build-system, cmake, make, just, embedded] | ## 决策 - [chunked-encoding-absent](pages/chunked-encoding-absent.md) — category: decision | tags: [http, chunked, sse, rfc7230] | # 响应侧 chunked encoding 缺失:现状与判断 diff --git a/brain/pages/api-atomicity-audit.md b/brain/pages/api-atomicity-audit.md new file mode 100644 index 00000000..7e2ea4f5 --- /dev/null +++ b/brain/pages/api-atomicity-audit.md @@ -0,0 +1,294 @@ +--- +id: api-atomicity-audit +title: "API 原子化审计:按原子性判据逐条定位违约点(不考虑兼容)" +category: decision +status: active +tags: [api, atomicity, orthogonality, audit, v2.10] +created: "2026-10-09T00:49:13" +updated: "2026-10-09T01:08:06" +--- + + +# API 原子化与正交性审计 + +配套 [[api-atomization-orthogonalization]](方向与兼容策略 B)。 +本页按**用户给定的定义**审计,明确排除兼容性与迁移成本。 + +## 判据(用户定义,2026-10-09) + +- **原子性 = API 不可再分割。** 一个 API 若能拆成若干独立子步骤、 + 由调用方按序拼装才完成一件事,则它不是原子的。判据形式化: + **完成"一件事"的最小调用数 > 1,且子步骤之间无强制约束表达。** +- **正交性 = API 没有重合。** 两个 API 若做同一件事、或一个的能力 + 是另一个的真子集,则它们不正交。判据形式化: + **存在两个 API f、g,使得 g 能表达的 f 都能表达,且无一方是必要的。** + +> ⚠️ **本页此前用过一套错误判据**(C1 提交性 / C2 顺序无关性 / C3 幂等 / +> C4 无跨对象半状态)——那是**事务原子性**,与此处定义无关,已作废。 +> 见本页 timeline 的 reversal 条目。原判据下的发现中, +> 仍然成立的部分(A2 router 数据丢失、A4 配置静默失效)是**独立缺陷**, +> 不依赖那套判据 —— 它们是功能 bug,不是原子性问题。 +> 本次审计**不重复**它们,只做不可分割性与重合性的定位。 + +## 一、原子性:不可再分割 + +### N1 构造一个可用的 server = 5 步 ★最严重的不可分割 + +官方嵌入示例 `examples/embedding/main.c:60-100` 的实证: + +```c +uvhttp_server_new(loop, &server); // 1 +uvhttp_router_new(&router); // 2 +uvhttp_router_add_route(router, "/", h); // 3 +uvhttp_server_set_router(server, router); // 4 +uvhttp_server_listen(server, "0.0.0.0", port); // 5 +``` + +"起一个监听 8080、带一条根路由的服务器"是**一件事**,但 API 把它拆成 5 个 +可独立调用的步骤。更严重的是**这 5 步没有原子版本**:`server_create` +虽然一步建好,但它建的是 builder 而非 server,且**不带路由**。 + +代价直接可见 —— 示例里手写了 **4 个错误回滚分支**: + +```c +if (router_new 失败) { server_free(server); return 1; } +if (add_route 失败) { router_free(router); server_free(server); return 1; } +if (listen 失败) { router_free(router); server_free(server); return 1; } +``` + +第 3、4 分支还要注意:**`set_router` 之后 router 已归 server 所有**, +但 listen 失败分支仍调 `router_free(router)` —— 与 +`set_router` 的 `@note`("the server owns the router ... Do NOT call +uvhttp_router_free on it yourself")**直接冲突**。 + +**官方示例就在教违反所有权契约的写法。** 这是不可分割性缺陷最尖锐的表现: +调用方被迫知道"哪一步之后所有权转移了",而这个知识没有任何类型或 API 表达。 + +**原子形态应该是**:`uvhttp_server_listen_with(server, host, port, routes, n)` +一类,或者构造器接受路由表 —— 要么全成,要么不成。 + +### N2 发送一个响应 = 4 步,且无原子版本 + +```c +uvhttp_response_set_status(response, 200); +uvhttp_response_set_header(response, "Content-Type", "text/plain"); +uvhttp_response_set_body(response, body, len); +return uvhttp_response_send(response); +``` + +(`examples/01_basics/helloworld.c:111-127` 实证) + +这 4 步之间**没有任何约束**:`send` 一个没有 body 的响应会怎样? +`set_body` 两次会怎样(第二次释放第一次,见 A3)?`set_status` 不调直接 +`send` 会怎样(发 0 或 200)?API 不表达这些,行为由实现默认值决定。 + +`uvhttp_response_init` 还在前面(`response.h:100`),完整序列 5 步。 + +**原子形态应该是**:`uvhttp_respond(response, status, content_type, body, len)`。 + +### N3 配置生效 = 3 步且通道二选一 + +调用方要弄清:设 `server->config` 还是 `context->current_config`? +`server.c:109-120` 的 fallback 链决定生效值,而走哪条取决于用了哪个构造 +入口。**"设置一个配置项"这件事被拆成"选通道 + 装配 + 设置"三步, +且通道选择不可见。** + +### N4 请求读取:命名暗示两个对象,实为一个 + +`uvhttp_get_header(request, name)` 声明在 **`uvhttp_server.h`**(`server.h:413-415`), +操作的是 `uvhttp_request_t`。三个函数全是纯转发: + +```c +const char* uvhttp_get_header(request, name) { + return uvhttp_request_get_header(request, name); // 纯别名 +} +const char* uvhttp_get_param(request, name) { + return uvhttp_request_get_query_param(request, name); +} +const char* uvhttp_get_body(request) { + return uvhttp_request_get_body(request); +} +``` + +放在 `server.h` 里**没有理由** —— 它们不涉及 server。这本身就是 +"归属错误"的信号:API 的位置暗示了一个它并不拥有的对象。 + +## 二、正交性:没有重合 + +### O1 请求读取 API 双份并存 ★纯粹的重合 + +| 短名(`server.h:413-415`) | 正规名(`request.h:107-109`) | +|---|---| +| `uvhttp_get_header(req, name)` | `uvhttp_request_get_header(req, name)` | +| `uvhttp_get_param(req, name)` | `uvhttp_request_get_query_param(req, name)` | +| `uvhttp_get_body(req)` | `uvhttp_request_get_body(req)` | + +**三对全部是别名**,短名版函数体只有一行转发。同一能力两个入口, +签名与行为完全一致。**选哪个没有任何依据** —— 不是简写、不是废弃、 +不是不同层次。纯粹的重复。 + +而且短名版有信息损失:`uvhttp_get_param` 实际是 **query** param +(转发到 `get_query_param`),名字里的 "param" 有歧义(路由参数 `:id` +才是通常说的 param)。**重合且其中一个名字是错的。** + +### O2 两个 cache API 结构完全同构,仅类型名不同 + +`uvhttp_lru_cache` 与 `uvhttp_gzip_cache` 是**同一个抽象的两份实现**: + +| 操作 | LRU | gzip | +|---|---|---| +| create | `lru_cache_create(max_mem, max_entries, ttl, **cache)` | `gzip_cache_create(max_mem, max_entries, ttl, **cache)` | +| free | `lru_cache_free(cache)` | `gzip_cache_free(cache)` | +| put | `lru_cache_put(...)` | `gzip_cache_put(...)` | +| stats | `lru_cache_get_stats(...)` | `gzip_cache_get_stats(...)` | +| set_max_entries | `lru_cache_set_max_entries(cache, n)` | `gzip_cache_set_max_entries(cache, n)` | +| set_max_memory_usage | 同名同参 | 同名同参 | +| set_cache_ttl | 同名同参 | 同名同参 | + +**签名逐字相同。** 若把类型名参数化,这两套可以合并为一个泛型 cache。 +现在的形态迫使调用方为两种缓存写两遍几乎相同的代码。 + +配套的不一致:类型名 `cache_manager_t`(**无 `uvhttp_` 前缀**, +`lru_cache.h:24`)vs `uvhttp_gzip_cache_t`(有前缀)。同一层抽象的 +两个类型,一个遵守命名约定一个不遵守。 + +**正交化含义**:cache 应该是一个抽象(接口 + 单一实现), +gzip cache 是它的一个策略选择,而不是两个平行 API。 + +### O3 构造入口三重叠 + +| 入口 | 建 config | 建 router | listen | 返回 | +|---|---|---|---|---| +| `server_new(loop, **s)` | ✗ | ✗ | ✗ | server | +| `server_new_with_loop(**s)` | ✗ | ✗ | ✗ | server(loop 归它管)| +| `server_create(loop, host, port, **b)` | ✓ | ✓ | **✓** | builder | + +三者不是"不同功能",而是**同一件事的三种切分**。`server_create` 做的 +前两步 `server_new` 也能做,差别只是**顺序**。这是 N1 不可分割的 +另一面:因为没有原子构造,才需要多个非原子入口拼。 + +### O4 释放语义四种写法,同义多名 + +| 写法 | 返回 | 出现处 | +|---|---|---| +| `uvhttp_server_free` | `uvhttp_error_t` | `server.h:246` | +| `uvhttp_server_simple_free` | `void` | `server.h:428` | +| `uvhttp_response_free` | `void` | `response.h:303` | +| `uvhttp_response_cleanup` | `void` | `response.h:302` | +| `uvhttp_request_free` | `void` | `request.h:100` | +| `uvhttp_request_cleanup` | `void` | `request.h:101` | + +`free` / `cleanup` / `simple_free` 三组同义命名并存,**没有任何文档说明 +区别**。`response_free` 与 `response_cleanup` 同一个头文件里相隔一行。 +返回类型也不一致(一个 `uvhttp_error_t`,其余 `void`)。 + +**这是重合**:三个名字指同一件事,说明其中两个是多余的。 + +### O5 压缩控制五入口,部分互相包含 + +`uvhttp_response_set_compress` / `set_compress_algorithm` / +`set_compress_threshold` / `set_compress_by_filename` / +`set_compress_by_content_type`,加上两个判定辅助 +`uvhttp_should_compress_by_extension` / `uvhttp_should_compress_by_content_type` +(`response.h:217,248`)。 + +后两个 `should_compress_*` 是**纯函数判定**,前两个 `set_compress_by_*` +是**设置 + 内部调用判定**。同一套扩展名/Content-Type 匹配逻辑 +(`response.c:1183` `COMPRESSIBLE_EXTENSIONS` 与 +`NON_COMPRESSIBLE_EXTENSIONS` 两张表)被判定和设置两条路径共用, +但两条路径都暴露给调用方。**判定逻辑与设置动作没有分开。** + +## 三、按"不可分割 / 无重合"归并的结论 + +| 维度 | 现状 | +|---|---| +| 构造一件事 | 5 步,无原子版本,官方示例手写 4 个回滚分支且违反所有权契约 | +| 发一个响应 | 4~5 步,无原子版本,步骤间无约束 | +| 设一个配置项 | 3 步 + 通道二选一,通道不可见 | +| 读一个 header | 2 个入口,纯别名,其中一个名字有歧义 | +| 用一个 cache | 2 套逐字相同的 API,仅类型名不同 | +| 构造 server | 3 个入口,差异只是切分顺序 | +| 释放 | 3 组同义命名,4 种返回类型 | +| 控制压缩 | 5 个入口 + 2 个判定辅助,判定与设置未分离 | + +**一句话**:这个 API 的问题不是"不安全"或"有 bug",而是**它把每件事 +都拆成了可独立调用的碎片,然后把组合的责任交给了调用方**。 +调用方要读注释才能知道步骤顺序、要读注释才知道所有权何时转移、 +要读注释才知道哪个入口是"正确"的那个。 + +这解释了为什么 [[connection-limit-503-memory-safety]] 那类"字段要么权威 +要么不存在"的原则难以贯彻 —— 当 API 本身是碎片化的,字段状态就没有 +唯一可以依附的地方。 + +## 四、正交化已达成的维度(不要破坏) + +- **无跨实例可变全局状态**。全库仅 4 个 `static const` 表; + TLS entropy/drbg 挂在 `context` 上。多 server 隔离天然成立。 +- **response 系 setter 先校验后写入**,不变量在写入点建立 + (`set_status` 验范围、`set_header` 验字符与控制符)。 + 这是 cache 系 setter(直接赋值无校验)应统一的模板。 + +## 五、待定(不属本次审计范围) + +- `uvhttp_config_t` 26 字段中 14 个不生效 —— 属功能缺陷, + 见 [[api-atomization-orthogonalization]],不重复。 +- router 迁移致路由全丢(13+ 顶层路由 + 参数路由)—— 属数据丢失 bug, + 已实测复现,见 [[api-atomicity-audit]] timeline 的 evidence 条目。 + +## 关联 + +[[api-atomization-orthogonalization]](方向、兼容策略 B、配置字段全表)、 +[[embedding-guide-examples]](N1 不可分割的实证来源 —— 官方示例)、 +[[router-cache-parity]](O2 同构双缓存的既有认知)、 +[[connection-limit-503-memory-safety]](碎片化 API 如何导致字段无唯一依附点) + + +## Timeline + +- time: 2026-10-09T00:49:13 + kind: decision + summary: "Created this page: API 原子化审计:按原子性判据逐条定位违约点(不考虑兼容)" + source: "2026-10-09 按原子性判据审计全量公共 API" + affects: [api-atomicity-audit] + +- time: 2026-10-09T00:49:47 + kind: decision + summary: "原子化判据与 12 条违约点定位:构造非原子、迁移无回滚、set_body 先释放后分配、无全局状态(唯一干净维度)" + source: "2026-10-09 逐函数审计 src/ 实现" + affects: [api-atomicity-audit] + +- time: 2026-10-09T00:52:29 + kind: evidence + summary: "A2 实测确认(比原判断更严重):13 个不同顶层路由 + 任意参数路由 → migrate_to_trie 中途失败 → 全部路由丢失且 find_handler 返回 NULL,无任何报错" + source: "2026-10-09 独立探针程序实测(4 个 probe,逐个排除后定位)" + affects: [api-atomicity-audit] + +- time: 2026-10-09T00:52:56 + kind: decision + summary: "A2 实测升级为已复现的数据丢失:13+ 顶层路由 + 参数路由致全部路由静默丢失,附机制解剖与修复方向" + source: "2026-10-09 探针实测 + 机制追至 child_indices[12] 定长上限" + affects: [api-atomicity-audit] + +- time: 2026-10-09T00:53:01 + kind: reversal + summary: "修正早前判断:初版称 A2 仅『面临丢失风险』且迁移后新路由缺失,量级判断不足 —— 实测为已注册路由全部丢失" + source: "2026-10-09 探针实测推翻" + affects: [api-atomicity-audit] + +- time: 2026-10-09T00:56:28 + kind: decision + summary: "按用户定义重写:原子性=API 不可再分割,正交性=API 无重合。原 C1-C4 事务语义判据作废" + source: "2026-10-09 用户纠正判据定义后重写" + affects: [api-atomicity-audit] + +- time: 2026-10-09T00:56:32 + kind: reversal + summary: "判据定义作废重写:此前用的 C1-C4(提交性/顺序无关/幂等/跨对象半状态)是事务原子性,与用户所指(API 不可再分割)无关" + source: "2026-10-09 用户纠正定义" + affects: [api-atomicity-audit] + +- time: 2026-10-09T01:08:06 + kind: evidence + summary: "N1 的实证升级:examples/embedding/main.c 的 listen 失败回滚分支在 ASan 下确认 double-free(set_router 已转移所有权,示例仍 router_free)" + source: "2026-10-09 ASan 探针复现,栈指向 uvhttp_server_free→uvhttp_router_free" + affects: [api-atomicity-audit] diff --git a/brain/pages/api-atomization-orthogonalization.md b/brain/pages/api-atomization-orthogonalization.md new file mode 100644 index 00000000..10f8494d --- /dev/null +++ b/brain/pages/api-atomization-orthogonalization.md @@ -0,0 +1,209 @@ +--- +id: api-atomization-orthogonalization +title: "v2.10 方向:API 原子化与正交化,停止新增特性" +category: decision +status: active +tags: [api, design, roadmap, v2.10] +created: "2026-10-09T00:27:12" +updated: "2026-10-09T00:46:40" +--- + + +# v2.10 方向:API 原子化与正交化 + +## 决定 + +1. **停止新增特性。** v2.10.x 唯一方向是把现有公共 API 做**原子化**与 + **正交化**改造 —— 不加能力,把 2.x 攒下的耦合在 3.0 之前还清。 + +2. **兼容策略 = B:2.10 直接破坏性改**(2026-10-09 用户拍板)。 + **不留 deprecated 兼容层,不做新旧 API 并行。** + +3. **第一性原理结论:v2.10 的主攻方向不是"合并配置通道",而是 + "让配置成为真的控制面,或者删掉它假装的那部分"**(2026-10-09 核实后修正)。 + 原 roadmap 把"配置双通道合并"列为第一刀是基于错误诊断 —— 见下。 + +## 为什么是现在 + +v2.9.x 的实测结论把"加功能"这条路堵死了:性能([[alloc-hotpath-measured]] +0.026%、[[io-uring-evaluation]] 架构不可达)、测试([[silent-skip-audit-101-to-18]] +101→18 已完成)、CI([[sanitizer-feature-blindspot]] 盲区已修)三条抓手各自见底。 +剩下唯一有价值的抓手是 API 自身的形状 —— 但见下,**它比预想的大得多**。 + +## 核心发现:config 是装饰品,不是控制面 + +### 诊断的修正过程 + +初版诊断是"两条配置通道(`server->config` vs `context->current_config`) +可能互相矛盾"。**这个诊断是错的**,因为它假设 config 大体上是活的、 +只是有两个来源。 + +逐字段核实(脚本按 `config->field` 限定匹配,排除 `conn->field` 同名字段干扰) +后得到的真相:**`uvhttp_config_t` 26 个字段中,14 个从不被任何执行路径读取。** + +| 分类 | 数量 | 字段 | +|---|---|---| +| 真读(config→执行点) | 11 | `max_connections` `backlog` `connection_timeout` `max_file_size` `trust_proxy_headers` `websocket_max_frame_size/message_size/ping_interval/ping_timeout` `tcp_keepalive_timeout` `sendfile_timeout_ms` `sendfile_max_retry` | +| **仅写不读**(API 写进去,无人读) | 3 | `max_body_size` `request_timeout` `keepalive_timeout` | +| **零读点**(外部代码从不访问) | 11 | `read_buffer_size` `max_header_size` `max_url_size` `max_requests_per_connection` `rate_limit_window` `rate_limit_max_requests` `rate_limit_max_window_seconds` `rate_limit_min_timeout_seconds` `cache_default_max_entries` `cache_default_ttl` `lru_cache_batch_eviction_size` | + +### 为什么这是设计缺陷而非疏漏 + +`uvhttp_config_set_defaults` 给全部 26 个字段赋默认值, +`uvhttp_config_validate` 校验全部 26 个的范围,`uvhttp_config_print` 打印全部 +26 个。**没有任何一个环节承认其中 14 个不影响行为。** + +于是形成这个模式: + +- `uvhttp_set_max_body_size(builder, n)` → 写 `config->max_body_size` → 返回 builder +- `uvhttp_config_update_size_limits(ctx, n, m)` → 写同一字段 → **打 INFO 日志 + "Limits updated - Body: %zu -> %zu"** → 返回 `UVHTTP_OK` +- 实际执行:`request.c:308` 用编译期常量 `UVHTTP_MAX_BODY_SIZE` 判断 body 超限 + +**两个公共 API 报告成功、打日志、返回 OK,而对服务器行为零影响。** +这不是"可能失效",是**必然失效且静默** —— 比初版诊断严重一个量级。 + +`uvhttp_set_timeout` 同样:`request_timeout` / `keepalive_timeout` 仅写不读, +实际超时由 `connection.c:1553` 读 `config->connection_timeout`(**第三个字段**)。 +所以 builder 的 `uvhttp_set_timeout` 设的数和实际生效的数**不是同一个字段**。 + +限流整块也是:`config` 里 4 个 `rate_limit_*` 字段零读点, +`uvhttp_server_enable_rate_limit` 把值写进 `server->rate_limit_*` 字段。 +配置对象和真实状态是两套,且配置那套完全没人看。 + +### 第一性原理:这是"配置"还是"文档"? + +`uvhttp_config_t` 目前的行为更接近**一份服务器规格说明书**,而非控制面: +它的字段被默认值填充、被校验、被打印、被写,然后被忽略。 + +由此得出 v2.10 的真问题,不是"怎么合并两个通道",而是二选一: + +- **让它成真**:14 个字段要么接到执行点,要么删掉。接到执行点意味着 + `read_buffer_size` / `max_body_size` 要进 per-connection 分配与热路径 —— + **这是新增运行时可调能力,与"特性冻结"矛盾,且触及性能敏感区**。 +- **承认它只是规格**:删掉假的(14 个字段 + 3 个假 API),只留真生效的 11 个。 + 删代码而非加代码,符合本轮"还债"定位,且零性能风险。 + +**倾向后者**:本轮目标是还债不是加功能;把 `read_buffer_size` 变成运行时可调 +是**新特性**,不是原子化。而"删掉假装能用的东西"恰恰是正交化的实质 —— +一个不生效的配置通道不是通道,是噪声。 + +## 现状证据(其余耦合点) + +### 1. 构造入口三个,语义重叠 + +- `uvhttp_server_new(loop, **server)` —— `server->config` 保持 NULL +- `uvhttp_server_new_with_loop(**server)` +- `uvhttp_server_create(loop, host, port, **builder)` —— **唯一会建 config 的入口** + +关键后果:`server->config` 对裸 `server_new` 用户恒为 NULL,于是 +`server.c:109-120` 的 fallback 链决定行为 —— 同一 API 调用在不同装配下 +结果不同。且 builder 写的 3 个字段仅在 builder 路径下存在。 + +### 2. 释放路径非原子:靠调用方手动置 NULL + +`uvhttp_server_create` 错误回滚(`src/uvhttp_server.c:795-848`)逐步手工置 +`server->config = NULL` / `server->router = NULL`。注释自承 +"Do NOT null them here - that would defeat the free and leak" —— +**同一字段在相邻错误分支一会儿置一会儿不置**,正确性靠注释维护。 + +### 3. 所有权契约只存在于文档注释 + +`set_router` / `set_context` 的 `@note` 强调"server 接管所有权", +但入参是裸指针,类型系统无表达。三处注释描述同一所有权图且互相引用不同 +(`config.c:508` / `context.c:302` / `server.h:238-242`)。 + +### 4. 限流:API 是 per-client,实现是 per-server + +| 函数 | 参数暗示 | 实际 | +|---|---|---| +| `get_rate_limit_status(server, client_ip, ...)` | 查某客户端 | 忽略 ip,返回全局计数 | +| `reset_rate_limit_client(server, client_ip)` | 重置某客户端 | 忽略 ip,注释自承 "simplified implementation: reset entire server's" | +| `check_rate_limit(server)` | —— | 无 ip 参数,全局单计数器 | + +白名单却真按 IP(`request.c:358`)。**per-IP 白名单 + per-server 计数** +混在一套 API 里。A 打满 → B 一起 429。 + +### 5. `server` 是 6 cache line 的大 struct + +限流状态、WS 连接管理、gzip cache、protocol registry 全部内联 +(`#if` + `_padding` 拼布局)。四件独立的事共享一个生命周期。 + +## 改造原则(按 B 校准) + +1. **配置只留真生效的** —— 删 14 个零读点字段 + 3 个假 setter, + 或者接线。**需用户在"删"与"接"之间拍板**(见下方待决)。 +2. **单一构造入口**:`loop` 归谁由参数表达,不由函数名表达。 + builder 与其 10 个全局方法一并移除。 +3. **释放原子**:失败回滚不需调用方置 NULL。 +4. **所有权进类型**:接管用 `**` 或明确标注。 +5. **限流语义二选一**:真 per-client,或改签名承认 per-server, + **用语义测试锁死**。 +6. **子模块独立生命周期**:限流 / TLS / WS / 压缩拆成独立对象。 + +## 待用户拍板(阻塞第一刀) + +**`uvhttp_config_t` 那 14 个不生效的字段:删掉,还是接线?** + +- 删:符合"还债不加班"定位,零性能风险,但缩减了表面能力 + (`uvhttp_set_max_body_size` 等要一并删或改语义) +- 接:是**新特性**(运行时可调 body/buffer 上限),与特性冻结矛盾, + 且动 per-connection 分配,需基准门禁验证 + +这是路线级选择,不是实现细节,不替他决定。 + +## 影响范围 + +- `include/uvhttp_config.h`(26 字段)、`uvhttp_server.h`(52 符号) +- `src/uvhttp_config.c`(defaults/validate/print 全部 26 字段) +- `src/uvhttp_server.c`(构造/回滚/fallback 链/限流) +- 全部测试:`test_config_*` 有大量用例断言"更新成功",删字段需同步删 +- `examples/01_basics/*` 用了 `uvhttp_config_set_current` +- `docs/` API 参考需同步;迁移说明落 README + CHANGELOG + docs/ + +## 关联 + +[[embedding-guide-examples]]、[[embedding-cmake-public-deps]]、 +[[chunked-encoding-absent]](同样"能力有但封装缺"的判断)、 +[[test-validity-always-green]]、[[connection-limit-503-memory-safety]] +(该页定的原则"公共结构体字段要么是权威真源、要么不存在"—— +**本发现正是该原则在 config 上的系统性违反**) + + +## Timeline + +- time: 2026-10-09T00:27:12 + kind: decision + summary: "Created this page: v2.10 方向:API 原子化与正交化,停止新增特性" + source: "2026-10-09 与用户讨论确定的下个版本方向" + affects: [api-atomization-orthogonalization] + +- time: 2026-10-09T00:27:30 + kind: decision + summary: "确立 v2.10 方向:停止新增特性,公共 API 原子化与正交化" + source: "读 include/ 现状(299 符号 / 28 头文件)+ 追踪 server.c / request.c / config.c 实现" + affects: [api-atomization-orthogonalization] + +- time: 2026-10-09T00:39:16 + kind: decision + summary: "兼容策略定为 B:2.10 直接破坏性改,不留 deprecated 兼容层" + source: "用户拍板(2026-10-09)" + affects: [api-atomization-orthogonalization] + +- time: 2026-10-09T00:39:33 + kind: decision + summary: "确定兼容策略 B:2.10 直接破坏性改造,不留兼容层" + source: "用户拍板 2026-10-09" + affects: [api-atomization-orthogonalization] + +- time: 2026-10-09T00:43:09 + kind: evidence + summary: "深入核实后修正:配置问题不止双通道 —— 三个 update_* 中有两个写的字段在全库无任何执行点读它们" + source: "2026-10-09 grep 全 src 读点核实" + affects: [api-atomization-orthogonalization] + +- time: 2026-10-09T00:46:40 + kind: decision + summary: "第一性原理重估:配置问题的真因不是双通道,而是 uvhttp_config_t 26 字段中 14 个从不被执行路径读取 —— config 是装饰品,不是控制面" + source: "2026-10-09 逐字段读点核实(脚本按 config 对象限定匹配)" + affects: [api-atomization-orthogonalization] diff --git a/brain/roadmap.md b/brain/roadmap.md index 639d1bd8..9baab503 100644 --- a/brain/roadmap.md +++ b/brain/roadmap.md @@ -2,7 +2,7 @@ slug: roadmap title: Roadmap role: milestones -updated: "2026-10-01T02:42:15" +updated: "2026-10-09T00:53:21" --- # Roadmap @@ -28,8 +28,17 @@ gantt section v2.9.x — 生态 文档多语言完善 :done, c1, after b2, 14d Fuzz 测试增强 :done, c4, after b2, 14d - 社区贡献指南 :done, c4b, after c4, 3d - 测试基建清理 :done, c4c, after c4b, 1d + 社区贡献指南 :done, c4b, after c4, 3d + 测试基建清理 :done, c4c, after c4b, 1d + section v2.10.x — API 原子化与正交化(特性冻结) + router 迁移致路由全丢(修复) :active, d1, 2026-10-09, 2d + 配置静默失效全线(14 字段) :active, d2, after d1, 4d + 限流语义修正 per-client :pending, d3, after d2, 3d + set_body 原子性 + cache setter 校验 :pending, d4, after d3, 2d + 构造/释放原子化 :pending, d5, after d4, 5d + 所有权进类型 + builder 收敛 :pending, d6, after d5, 5d + 子模块独立生命周期 :pending, d7, after d6, 7d + API 参考与示例同步 :pending, d8, after d7, 3d ``` ### v2.7.x — 质量巩固与嵌入验证(2026 Q3) @@ -60,7 +69,7 @@ gantt | io_uring 探索 | P2 | ❌ 已关闭(架构不可达) | libuv 1.52 不覆盖 sendfile,uvhttp 静态文件热路径全走 sendfile+线程池;要受益需绕过 libuv 自管 ring fd,属架构重构。基准门禁 3 端点均为内存 body 也验证不到。见 [[io-uring-evaluation]] | | 内存分配优化 | P2 | ❌ 已关闭(实测收益过低) | keep-alive 3 次分配/请求且无泄漏;唯一可省的 1024B scratch 上限 **0.026%**(12ns ÷ 45.7µs),远低于 runner 方差 ~40%,不做。见 [[alloc-hotpath-measured]] | -### v2.9.x — 生态扩展(2027 Q1) +### v2.9.x — 生态扩展(2026 Q4 — 2027 Q1) | 目标 | 优先级 | 状态 | 说明 | |------|--------|------|------| @@ -68,17 +77,47 @@ gantt | Fuzz 测试增强 | P1 | ✅ 已完成 | `fuzz_static_path` 补路径解析面(#408)——变异验证证明 harness 有牙齿(移除包含检查后 1s 抓逃逸),60s 自由 fuzz 未发现可利用穿越;`lru_cache` dead-store 修复(#409)让四 harness 在 CI 稳定跑 | | 社区贡献指南 | P1 | ✅ 已完成 | 代码审查清单 + 测试形态选择(#401);补 integration 真相、assert 在 Release 下失效、本地构建盲区(#410) | | 测试基建清理 | P2 | ✅ 已完成 | `test/integration/` 19 个文件移到 `manual/`,删 4 个纯 assert 文件(42 处断言在 NDEBUG 下全失效,exit=0 是虚假绿灯);CMake glob 同步(#411) | +| 行为测试补全 | P1 | ✅ 已完成 | 11 组 132 个测试(v2.9.2):JSON 错误转义、连接 close、TLS 证书三 PR(#461-#463) | +| 测试有效性治理 | P0 | ✅ 已完成 | 静默跳过 101 → 18(#453-#456);恒绿零验证修复(#457);TLS 覆盖率 60.3% → 67.9%(#461-#463);PR 测试移至 pre-release 门禁(#465) | +| Sanitizer 盲区修复 | P0 | ✅ 已完成 | STATIC_FILES(#449)与 ROUTER_CACHE(#468)纳入 ASan/UBSan/coverage——此前 feature 默认 OFF 致守卫源文件整文件不编译 | + +### v2.10.x — API 原子化与正交化(特性冻结) + +**方向已定:不加新特性。** 详见 [[api-atomization-orthogonalization]]。 +理由:性能、测试、CI 三条抓手在 v2.9.x 已各自走到实测收益为零或治理完毕, +剩下唯一有价值的抓手是 API 自身的形状。 + +| 目标 | 优先级 | 状态 | 说明 | +|------|--------|------|------| +| **router 迁移致路由全丢** | **P0** | **📋 下一步(已实测复现)** | **13+ 个不同顶层路由 + 任意 `:param` 路由 → `migrate_to_trie` 中途失败 → 全部已注册路由静默丢失,`find_handler` 返回 NULL。** 机制:`child_indices[12]` 定长上限撞顶后,失败分支 detach 并释放 `old_routes`,但已迁入 trie 的条目因 `use_trie` 仍为 0 而永不可达。当前**无测试覆盖** | +| 配置静默失效全线 | P0 | 📋 待办 | 诊断已修正:`uvhttp_config_t` 26 字段中 **14 个从不被执行路径读取**(3 个"仅写不读" + 11 个零读点)。`update_size_limits` / `set_max_body_size` 返回 `UVHTTP_OK` 并打 INFO 日志,实际执行用编译期常量 —— **零应用且报告成功**。非"双通道矛盾" | +| set_body 原子性 | P1 | 📋 待办 | `uvhttp_response_set_body` 先 `free` 旧 body 再 `alloc`,OOM 下旧值不可恢复。同对象上 `set_header` 的 realloc 路径却是原子的 —— 两个 setter 失败语义不一致 | +| cache setter 入参校验 | P2 | 📋 待办 | `lru_cache_set_max_entries` / `set_cache_ttl` 等直接赋值无范围校验,不变量不在写入点建立(response 系已是正确范式,可作模板) | +| 限流语义修正 | P0 | 📋 待办 | 签名 per-client(`client_ip` 参数)实现 per-server 全局单计数;`reset_rate_limit_client` 注释自承 "simplified implementation: reset entire server's"。A 打满会连带 429 B | +| 构造/释放原子化 | P1 | 📋 待办 | `uvhttp_server_create` 错误回滚靠手工置 `server->config/router = NULL`,相邻分支一会儿置一会儿不置,正确性靠注释维护 | +| 所有权进类型 + builder 收敛 | P1 | 📋 待办 | **破坏性**:`set_router`/`set_context` 接管所有权只写在 `@note`;`uvhttp_get/post/put/delete` 等 10 个全局函数当 builder 方法,**随 builder 一并移除**(`uvhttp_delete` 命名空间污染随之消失) | +| 子模块独立生命周期 | P2 | 📋 待办 | 限流 / TLS / WS / 压缩内联在同一个 6-cache-line `server` struct 内,`#if` + `_padding` 拼布局 | +| 构造入口收敛 | P2 | 📋 待办 | **破坏性**:`server_new` / `server_new_with_loop` / `server_create` 三个平级入口删成只剩一个,差别只在"谁管 loop" | +| API 参考与示例同步 | P1 | 📋 待办 | 改造后 `docs/` API 参考与 `examples/` 需同步 | + +> 完整的原子性判据、12 条违约点与清理清单见 [[api-atomicity-audit]]。 + +> ✅ **兼容策略已定:直接破坏性改(B)**(2026-10-09)。不留 deprecated 兼容层, +> 不做新旧 API 并行 —— 双份测试与"哪个是新的"心智负担正是本轮要消灭的东西。 +> 一次改干净,v2.10 即干净的新 API,3.0 不再背清理义务。版本号保留 2.10 +> 只为不跳号,靠 CHANGELOG + 迁移说明交代。 ### 最低优先级(长期 / 按需) > macOS / FreeBSD 平台支持已降级为最低优先级:不进入近期里程碑排期,按需推进。 +> 在 API 原子化完成前,平台适配会放大改动面,故排在后面。 | 目标 | 优先级 | 状态 | 说明 | |------|--------|------|------| | macOS 支持 | 最低 | 📋 待办(无排期) | kqueue 适配、sendfile 兼容、CI 测试;按需推进 | | FreeBSD 支持 | 最低 | 📋 待办(无排期) | kqueue 已有经验,适配 FreeBSD 差异;按需推进 | -### 完成项(v2.6.x ~ v2.8.x) +### 完成项(v2.6.x ~ v2.9.x) - ✅ HTTP/1.1 服务器(CI 记录基线 ~83K RPS;绝对值仅作趋势记录,不当门禁) - ✅ WebSocket 全双工通信(RFC 6455) From fef7c3c7d71c5d04668b5a05a3f52e3255d37655 Mon Sep 17 00:00:00 2001 From: adam Date: Sat, 10 Oct 2026 16:27:57 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(api):=20=E8=AF=84=E5=AE=A1=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20=E2=80=94=E2=80=94=20=E5=A5=91=E7=BA=A6=E8=AF=9A?= =?UTF-8?q?=E5=AE=9E=E5=BA=A6=EF=BC=88spec/README/=E6=B3=A8=E9=87=8A/test?= =?UTF-8?q?=20=E8=87=AA=E9=AA=97=EF=BC=89=E4=B8=8E=203=20=E5=A4=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=B0=8F=E4=BF=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对 v2.10 第一刀(#470)的评审结论修复。无内存安全问题,全是契约与 文档的诚实度问题: 1. docs/spec/server-api.md 之前只改了函数名,Postconditions 仍写 "server does not own the router; the caller must free it" —— 与头文件 "OWNERSHIP TRANSFERS HERE" 正相反,正是本 PR 要消灭的那个误解(它 让官方示例 double-free)。同时补上: - take_context 一节(原 set_context 条目引用的函数已不存在) - listen_routes 一节(atomic 构造的契约:*server 失败恒 NULL、路由拷贝 不引用调用方数组、构造后 router 句柄不可达) 2. README.md / README_CN.md 未被 #470 触及,快速开始仍调用已改名的 uvhttp_server_set_router —— 合并后照抄编译不过。改为 take_router 并 在注释里说明所有权转移。 3. ListenFailureLeavesNothingBehind 原来用特权端口 1 制造 listen 失败, root 下 GTEST_SKIP → 本 PR 的核心回归(listen 失败不二次释放)在该 环境完全不被验证。改为先 uv_listen 占住一个端点端口再传同端口,必然 EADDRINUSE,与运行身份无关。(GTEST_SKIP 属静默跳过,与 silent-skip 治理同一类问题。) 4. take_router/take_context 的 "NULL 清空" 语义只在尚未持有时有效,已有 对象时同样被拒 —— 头文件注释此前未说,两处测试注释还在宣称 "clears router"/"用于清除路由"。头文件补 "no detach operation" 说明,四处 测试注释改为事实,新增 4 个用例锁定语义(NULL after take 拒绝且不动 已持有对象 / 未持有时为空操作)。 5. 代码小修三处: - listen_routes 校验 method 范围:越界 method 会登记一条永不匹配的 死路由(所有查找都是等值比较),原子构造不应建出一张说谎的路由表 (变异测试:移除该校验 → OutOfRangeMethodIsRejected 变红) - take_router/take_context 的失败日志原建议 "free it first" —— 调用方 无权单独释放 server 持有的对象(free 了就是 UAF),且 NULL 也卸不掉。 改为给出可执行的出路(同对象幂等重传或用新 server) - uvhttp_server_free 释放 router 后未置 NULL(其余字段都置),补上, 与 "字段要么权威要么不存在" 对齐 6. 测试补强:RoutesAreActuallyInstalledInTheRouter —— 此前 14 个新测试 只断言 server != nullptr,"构造成功但路由没装进去"这类回归全绿(router 迁移丢路由正是本仓同族 P0);现在按路径逐条断言 handler 与方法边界。 7. 注释纠错:uvhttp_server.h / uvhttp_request.h 声称两头文件 "include each other transitively"(实际互不 include,无环),uvhttp_server.c 的自引用 "(was uvhttp_server_take_router)" 改为 set_router。 验证:默认构建 ctest 120/120;ASan 构建 test_server_atomic_construct 20/20 干净(含 listen 失败路径);变异测试确认 method 校验有牙齿。 --- README.md | 5 +- README_CN.md | 5 +- docs/api/API_REFERENCE.md | 49 ++++++ docs/spec/server-api.md | 33 ++-- include/uvhttp_request.h | 7 +- include/uvhttp_server.h | 19 +- src/uvhttp_server.c | 28 ++- test/unit/test_server_api_coverage.cpp | 4 +- test/unit/test_server_atomic_construct.cpp | 195 +++++++++++++++++++-- test/unit/test_server_boost_coverage.cpp | 4 +- 10 files changed, 309 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 03ca109f..c7fc7662 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,10 @@ int main() { fprintf(stderr, "Failed to create router: %s\n", uvhttp_error_string(result)); return 1; } - uvhttp_server_set_router(server, router); + // Hand the router to the server; the server owns it from here and frees + // it in uvhttp_server_free (v2.10: renamed from uvhttp_server_set_router, + // which implied a borrow while the behavior was always a transfer) + uvhttp_server_take_router(server, router); // Add route result = uvhttp_router_add_route(router, "/hello", hello_handler); diff --git a/README_CN.md b/README_CN.md index 6d717f09..3b16e6b5 100644 --- a/README_CN.md +++ b/README_CN.md @@ -246,7 +246,10 @@ int main() { fprintf(stderr, "创建路由器失败: %s\n", uvhttp_error_string(result)); return 1; } - uvhttp_server_set_router(server, router); + // 把 router 交给服务器;从此刻起服务器持有它,并在 uvhttp_server_free + // 中释放(v2.10:由 uvhttp_server_set_router 改名而来——旧名字暗示借用, + // 而行为一直是所有权转移) + uvhttp_server_take_router(server, router); // 添加路由 result = uvhttp_router_add_route(router, "/hello", hello_handler); diff --git a/docs/api/API_REFERENCE.md b/docs/api/API_REFERENCE.md index 52ffdf14..368c6cd9 100644 --- a/docs/api/API_REFERENCE.md +++ b/docs/api/API_REFERENCE.md @@ -134,6 +134,55 @@ if (result != UVHTTP_OK) { } ``` +### uvhttp_server_listen_routes + +```c +uvhttp_error_t uvhttp_server_listen_routes(uv_loop_t* loop, + const uvhttp_route_t* routes, + size_t route_count, + const char* host, + int port, + uvhttp_server_t** server); +``` + +Atomic construction: creates a server, installs the route table, and starts +listening in one call. On success `*server` is a fully listening server with +every route registered; on any failure `*server` is NULL and nothing is +allocated, so the caller has no rollback branches to write. Route entries are +copied, so the `routes` array can be freed after the call. The router becomes +internal to the server (its handle is not reachable afterwards). + +**Parameters**: +- `loop`: Initialized event loop +- `routes`: Route table; may be NULL only when `route_count` is 0 +- `route_count`: Number of entries in `routes` +- `host`: Listen address (e.g., "0.0.0.0") +- `port`: Listen port +- `server`: Out pointer; NULL on failure + +**Return Value**: +- `UVHTTP_OK`: Success - fully listening server +- `UVHTTP_ERROR_INVALID_PARAM`: NULL `loop`/`host`/`server`, or a route entry with NULL `path`/`handler` or an out-of-range `method` +- `UVHTTP_ERROR_OUT_OF_MEMORY`: Allocation or route registration failed +- `UVHTTP_ERROR_SERVER_LISTEN`: bind or listen syscall failed + +**Example**: +```c +const uvhttp_route_t routes[] = { + {"/", UVHTTP_ANY, hello_handler}, + {"/health", UVHTTP_GET, health_handler}, +}; +uvhttp_server_t* server = NULL; +uvhttp_error_t result = + uvhttp_server_listen_routes(loop, routes, 2, "0.0.0.0", 8080, &server); +if (result != UVHTTP_OK) { + fprintf(stderr, "Failed to start: %s\n", uvhttp_error_string(result)); + return 1; +} +uv_run(loop, UV_RUN_DEFAULT); +uvhttp_server_free(server); +``` + ## Router API ### uvhttp_router_new diff --git a/docs/spec/server-api.md b/docs/spec/server-api.md index e46de6ad..417c5743 100644 --- a/docs/spec/server-api.md +++ b/docs/spec/server-api.md @@ -60,21 +60,34 @@ connection management. ### uvhttp_server_take_router - **Signature**: `uvhttp_error_t uvhttp_server_take_router(uvhttp_server_t* server, uvhttp_router_t* router)` -- **Purpose**: Attach a router for path-based request dispatching -- **Preconditions**: `server` must be valid. `router` must be a valid router. -- **Postconditions**: `server->router` is set. The server does not own the router; the caller must free it after the server. +- **Purpose**: Hand a router over to the server for path-based request dispatching. Ownership transfers. +- **Preconditions**: `server` must be valid. `router` must be a valid router, or NULL when the server holds no router. +- **Postconditions**: `server->router` is set. **The server owns the router from this point and releases it in `uvhttp_server_free`; the caller must NOT call `uvhttp_router_free` on it** (doing so is a double free). There is no detach operation: a taken router can neither be replaced nor cleared. Passing the same router again is idempotent (returns `UVHTTP_OK`). - **Error conditions**: - - `UVHTTP_ERROR_INVALID_PARAM`: `server` or `router` is NULL + - `UVHTTP_ERROR_INVALID_PARAM`: `server` is NULL, or the server already owns a different router (overwriting would leak the previous one) - **Thread safety**: Not thread-safe. +- **History**: Renamed from `uvhttp_server_set_router` in v2.10. "set" implied a borrow while the behavior was a transfer - that mismatch made the official embedding example double-free on its listen-failure path. -### uvhttp_server_set_context -- **Signature**: `uvhttp_error_t uvhttp_server_set_context(uvhttp_server_t* server, struct uvhttp_context* context)` -- **Purpose**: Attach a context object for shared state -- **Preconditions**: `server` must be valid. `context` must be a valid context. -- **Postconditions**: `server->context` is set. +### uvhttp_server_take_context +- **Signature**: `uvhttp_error_t uvhttp_server_take_context(uvhttp_server_t* server, struct uvhttp_context* context)` +- **Purpose**: Hand a context object over to the server for shared state. Ownership transfers. +- **Preconditions**: `server` must be valid. `context` must be a valid context, or NULL when the server holds none. +- **Postconditions**: `server->context` is set. The server owns the context and releases it in `uvhttp_server_free`; the caller must NOT free it. No detach operation exists; passing the same context again is idempotent. - **Error conditions**: - - `UVHTTP_ERROR_INVALID_PARAM`: `server` or `context` is NULL + - `UVHTTP_ERROR_INVALID_PARAM`: `server` is NULL, or the server already owns a different context - **Thread safety**: Not thread-safe. +- **History**: Renamed from `uvhttp_server_set_context` in v2.10, for the same reason as `uvhttp_server_take_router`. + +### uvhttp_server_listen_routes +- **Signature**: `uvhttp_error_t uvhttp_server_listen_routes(uv_loop_t* loop, const uvhttp_route_t* routes, size_t route_count, const char* host, int port, uvhttp_server_t** server)` +- **Purpose**: Atomic construction: create a server, install a route table, and start listening in one call - all-or-nothing. +- **Preconditions**: `loop` must be a valid, initialized `uv_loop_t`. `host` must be non-NULL. `routes` must be non-NULL when `route_count > 0`. Each entry needs a non-NULL `path` and `handler` and a method within `UVHTTP_ANY..UVHTTP_PATCH`. `server` must be a non-NULL out pointer. +- **Postconditions**: On success, `*server` points to a fully listening server with all routes installed. On any failure, `*server` is NULL and nothing is allocated - there is no partial state for the caller to roll back. Route entries are copied (the `routes` array may be freed by the caller afterwards); the router is internal to the server and its handle is not reachable afterwards, so later routes must be registered on a router before calling this (or via the five-step sequence). +- **Error conditions**: + - `UVHTTP_ERROR_INVALID_PARAM`: `loop`, `host`, or `server` is NULL; `route_count > 0` with NULL `routes`; an entry with NULL `path`/`handler` or an out-of-range method + - `UVHTTP_ERROR_OUT_OF_MEMORY`: server or router allocation failed, or a route failed to register + - `UVHTTP_ERROR_SERVER_LISTEN`: bind or listen syscall failed +- **Thread safety**: Not thread-safe. Must be called from the event loop thread. ### uvhttp_server_enable_tls / uvhttp_server_disable_tls - **Signature**: `uvhttp_error_t uvhttp_server_enable_tls(uvhttp_server_t* server, uvhttp_tls_context_t* tls_ctx)` / `uvhttp_error_t uvhttp_server_disable_tls(uvhttp_server_t* server)` diff --git a/include/uvhttp_request.h b/include/uvhttp_request.h index b34831f2..9e14f685 100644 --- a/include/uvhttp_request.h +++ b/include/uvhttp_request.h @@ -20,9 +20,10 @@ extern "C" { #define MAX_URL_LEN 2048 #define MAX_BODY_LEN (1024 * 1024) // 1MB -/* uvhttp_method_t now lives in uvhttp_common.h — it is needed by both - * uvhttp_request.h and uvhttp_server.h, and those two include each other - * transitively, so the enum belongs in the shared header. */ +/* uvhttp_method_t lives in uvhttp_common.h — both this header and + * uvhttp_server.h need it (uvhttp_route_t embeds the handler type), and + * neither header includes the other, so the shared header is the only place + * the type can live. */ typedef struct uvhttp_request uvhttp_request_t; diff --git a/include/uvhttp_server.h b/include/uvhttp_server.h index 038a48d4..c3e4d163 100644 --- a/include/uvhttp_server.h +++ b/include/uvhttp_server.h @@ -25,9 +25,10 @@ typedef struct uvhttp_response uvhttp_response_t; typedef struct uvhttp_router uvhttp_router_t; typedef struct uvhttp_connection uvhttp_connection_t; -/* uvhttp_method_t comes from uvhttp_common.h (moved there in v2.10 to break - * the request.h <-> server.h cycle). uvhttp_request_handler_t is declared in - * uvhttp_request.h, which this header must not include. */ +/* uvhttp_method_t and uvhttp_request_handler_t come from uvhttp_common.h so + * this header can declare uvhttp_route_t without including uvhttp_request.h. + * Neither header includes the other: request/response are forward-declared + * above, and only .c files need their full definitions. */ #if UVHTTP_FEATURE_TLS typedef struct uvhttp_tls_context uvhttp_tls_context_t; @@ -323,6 +324,14 @@ uvhttp_error_t uvhttp_server_set_handler(uvhttp_server_t* server, * * @note Returns UVHTTP_ERROR_INVALID_PARAM if the server already owns a * different router, rather than silently leaking the previous one. + * Passing the same router again is idempotent (teardown paths rely on + * it). + * + * @note There is no detach operation: a taken router cannot be handed back, + * replaced, or unset by the caller - the server owns it until + * uvhttp_server_free. A NULL router is only a no-op when the server + * holds none already; once a router has been taken, NULL is rejected + * like any other pointer. */ uvhttp_error_t uvhttp_server_take_router(uvhttp_server_t* server, uvhttp_router_t* router); @@ -337,7 +346,9 @@ uvhttp_error_t uvhttp_server_take_router(uvhttp_server_t* server, * and releases it in uvhttp_server_free. Do NOT free it yourself. * * @note Renamed from uvhttp_server_set_context in v2.10, for the same reason - * as uvhttp_server_take_router. + * as uvhttp_server_take_router. No detach operation exists: a NULL + * context is only a no-op when the server holds none already, and is + * rejected once one has been taken. */ uvhttp_error_t uvhttp_server_take_context(uvhttp_server_t* server, struct uvhttp_context* context); diff --git a/src/uvhttp_server.c b/src/uvhttp_server.c index 6c18919e..06386410 100644 --- a/src/uvhttp_server.c +++ b/src/uvhttp_server.c @@ -415,6 +415,7 @@ uvhttp_error_t uvhttp_server_free(uvhttp_server_t* server) { /* Clean connection pool */ if (server->router) { uvhttp_router_free(server->router); + server->router = NULL; /* same post-free reset as the fields below */ } #if UVHTTP_FEATURE_TLS if (server->tls_ctx) { @@ -688,6 +689,16 @@ uvhttp_error_t uvhttp_server_listen_routes(uv_loop_t* loop, uvhttp_server_free(s); return UVHTTP_ERROR_INVALID_PARAM; } + if (routes[i].method < UVHTTP_ANY || + routes[i].method > UVHTTP_PATCH) { + /* An out-of-range method would register a route that can + * never match - every lookup is an equality test - i.e. a + * silently dead route. Reject it here rather than build a + * server whose route table lies. */ + uvhttp_router_free(router); + uvhttp_server_free(s); + return UVHTTP_ERROR_INVALID_PARAM; + } err = routes[i].method == UVHTTP_ANY ? uvhttp_router_add_route(router, routes[i].path, routes[i].handler) @@ -732,17 +743,21 @@ uvhttp_error_t uvhttp_server_take_router(uvhttp_server_t* server, } /* Ownership transfer is deliberate and is now named as such - * (uvhttp_server_take_router, v2.10; was uvhttp_server_take_router). + * (uvhttp_server_take_router, v2.10; was uvhttp_server_set_router). * "set" implied a borrow, but the server frees the router in * uvhttp_server_free - a mismatch that made the official embedding * example double-free on its listen-failure path. * * Handing over a router that is already owned leaves the previous one - * leaked; reject it rather than silently drop the reference. */ + * leaked; reject it rather than silently drop the reference. NULL is + * only a no-op when the server holds no router yet - there is no detach + * operation, so a router that has been taken can neither be replaced + * nor cleared. */ if (server->router && server->router != router) { UVHTTP_LOG_ERROR( - "server already owns a router - free it first or use a server " - "that has none"); + "server already owns a router; pass the same router again " + "(idempotent) or use a new server - a taken router cannot be " + "detached, and freeing it from the caller is a use-after-free"); return UVHTTP_ERROR_INVALID_PARAM; } @@ -794,7 +809,10 @@ uvhttp_error_t uvhttp_server_take_context(uvhttp_server_t* server, /* Ownership transfer - see uvhttp_server_take_router. */ if (server->context && server->context != context) { - UVHTTP_LOG_ERROR("server already owns a context - free it first"); + UVHTTP_LOG_ERROR( + "server already owns a context; pass the same context again " + "(idempotent) or use a new server - a taken context cannot be " + "detached, and freeing it from the caller is a use-after-free"); return UVHTTP_ERROR_INVALID_PARAM; } diff --git a/test/unit/test_server_api_coverage.cpp b/test/unit/test_server_api_coverage.cpp index d6e1ed15..e8d3a49e 100644 --- a/test/unit/test_server_api_coverage.cpp +++ b/test/unit/test_server_api_coverage.cpp @@ -121,7 +121,9 @@ TEST_F(UvhttpServerApiTest, ServerSetRouterNullRouter) { /* 创建服务器 */ ASSERT_EQ(create_server(), UVHTTP_OK); - /* 设置NULL router是允许的,用于清除路由 */ + /* NULL router 仅在服务器尚未持有 router 时是空操作(没有 detach 操作, + * 已 take 的 router 不可摘除或替换,传 NULL 同样返回 INVALID_PARAM)—— + * 见 test_server_atomic_construct.cpp 的 NullRouterAfterTakeIsRejected */ uvhttp_error_t result = uvhttp_server_take_router(server, NULL); EXPECT_EQ(result, UVHTTP_OK); EXPECT_EQ(server->router, nullptr); diff --git a/test/unit/test_server_atomic_construct.cpp b/test/unit/test_server_atomic_construct.cpp index 70884843..3ddbc1ed 100644 --- a/test/unit/test_server_atomic_construct.cpp +++ b/test/unit/test_server_atomic_construct.cpp @@ -21,6 +21,9 @@ extern "C" { #include "uvhttp_context.h" } +#include +#include + namespace { int ok_handler(uvhttp_request_t* req, uvhttp_response_t* resp) { @@ -29,6 +32,20 @@ int ok_handler(uvhttp_request_t* req, uvhttp_response_t* resp) { return 0; } +/* Distinct handlers, so "the route landed" can be checked per path instead + * of just "some handler is somewhere in the table". */ +int health_handler(uvhttp_request_t* req, uvhttp_response_t* resp) { + (void)req; + (void)resp; + return 0; +} + +int submit_handler(uvhttp_request_t* req, uvhttp_response_t* resp) { + (void)req; + (void)resp; + return 0; +} + } // namespace /* ---------- success path ---------- */ @@ -68,6 +85,40 @@ TEST(UvhttpServerAtomicConstruct, ZeroRoutesIsAllowed) { uvhttp_server_free(server); } +/* A listening server is not evidence that its routes landed: a regression + * that swallowed add_route results would still produce a listening server + * here whose every path 404s, and every assertion above would still pass. + * Same bug class as the router migration that silently dropped every + * registered route - pin the route table itself. */ +TEST(UvhttpServerAtomicConstruct, RoutesAreActuallyInstalledInTheRouter) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + const uvhttp_route_t routes[] = { + {"/health", UVHTTP_GET, health_handler}, + {"/submit", UVHTTP_POST, submit_handler}, + }; + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_listen_routes(loop, routes, 2, "127.0.0.1", 0, + &server), + UVHTTP_OK); + ASSERT_NE(server, nullptr); + ASSERT_NE(server->router, nullptr); + + EXPECT_EQ(uvhttp_router_find_handler(server->router, "/health", "GET"), + health_handler) + << "the route that was just registered must resolve to its handler"; + EXPECT_EQ(uvhttp_router_find_handler(server->router, "/submit", "POST"), + submit_handler); + /* A method mismatch must not resolve - guards against routes that match + * every method by accident. */ + EXPECT_EQ(uvhttp_router_find_handler(server->router, "/health", "POST"), + nullptr); + + uvhttp_server_free(server); +} + /* ---------- failure paths: *server must be NULL, nothing allocated ---------- */ TEST(UvhttpServerAtomicConstruct, NullOutParamIsRejected) { @@ -144,32 +195,69 @@ TEST(UvhttpServerAtomicConstruct, NullHandlerInRouteTableIsRejected) { EXPECT_EQ(server, nullptr); } -/* Port 1 is privileged, so bind fails. This is the exact path the embedding - * example got wrong: the server already owned the router at this point, so - * the cleanup must be server_free ALONE. Under ASan/valgrind a double free - * here is the regression signal. */ +/* An out-of-range method registers a route that can never match: every + * lookup is an equality test against the method enum. That is a silently + * dead route, so it must be rejected instead of built into the table. */ +TEST(UvhttpServerAtomicConstruct, OutOfRangeMethodIsRejected) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + const uvhttp_route_t routes[] = { + {"/", (uvhttp_method_t)99, ok_handler}, + }; + + uvhttp_server_t* server = nullptr; + EXPECT_EQ(uvhttp_server_listen_routes(loop, routes, 1, "127.0.0.1", 0, + &server), + UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ(server, nullptr); +} + +/* Deterministic bind failure: occupy an ephemeral port with our own + * listening socket first, then hand the same port to listen_routes - it must + * fail with EADDRINUSE no matter who runs the test. (The previous version + * bound privileged port 1 instead, which succeeds under root: the test then + * GTEST_SKIPs and this exact path - the one the embedding example got wrong - + * goes unverified.) The server already owned the router at this point, so the + * cleanup must be server_free ALONE; under ASan a double free here is the + * regression signal. */ TEST(UvhttpServerAtomicConstruct, ListenFailureLeavesNothingBehind) { uv_loop_t* loop = uv_default_loop(); ASSERT_NE(loop, nullptr); + uv_tcp_t blocker; + ASSERT_EQ(uv_tcp_init(loop, &blocker), 0); + struct sockaddr_in bind_addr; + ASSERT_EQ(uv_ip4_addr("127.0.0.1", 0, &bind_addr), 0); + ASSERT_EQ(uv_tcp_bind(&blocker, (const struct sockaddr*)&bind_addr, 0), 0); + ASSERT_EQ(uv_listen((uv_stream_t*)&blocker, 1, + [](uv_stream_t*, int) {}), + 0); + + struct sockaddr_in bound_addr; + int addr_len = sizeof(bound_addr); + ASSERT_EQ(uv_tcp_getsockname(&blocker, (struct sockaddr*)&bound_addr, + &addr_len), + 0); + const int occupied_port = ntohs(bound_addr.sin_port); + ASSERT_GT(occupied_port, 0); + const uvhttp_route_t routes[] = { {"/", UVHTTP_ANY, ok_handler}, }; uvhttp_server_t* server = nullptr; - const uvhttp_error_t err = - uvhttp_server_listen_routes(loop, routes, 1, "127.0.0.1", 1, &server); - - /* Binding a privileged port must fail. If it somehow succeeds, the test - * would leak the server, so free it and skip rather than assert. */ - if (err == UVHTTP_OK) { - uvhttp_server_free(server); - GTEST_SKIP() << "running as root: privileged port bind succeeded"; - } + const uvhttp_error_t err = uvhttp_server_listen_routes( + loop, routes, 1, "127.0.0.1", occupied_port, &server); + EXPECT_NE(err, UVHTTP_OK) << "port " << occupied_port + << " is occupied; listen must fail"; EXPECT_EQ(server, nullptr) << "out param must be NULL after a failed bind; the router the " "server had already taken ownership of must not be freed twice"; + + uv_close((uv_handle_t*)&blocker, nullptr); + uv_run(loop, UV_RUN_NOWAIT); } /* ---------- ownership: take_router ---------- */ @@ -223,7 +311,10 @@ TEST(UvhttpServerOwnership, RetakingSameRouterIsIdempotent) { ASSERT_EQ(uvhttp_router_add_route(router, "/", ok_handler), UVHTTP_OK); ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); - EXPECT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); + EXPECT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK) + << "re-taking the same router is a no-op, not an error"; + EXPECT_EQ(server->router, router) + << "the no-op must leave the owned router in place"; uvhttp_server_free(server); } @@ -247,3 +338,79 @@ TEST(UvhttpServerOwnership, SecondContextIsRejected) { uvhttp_context_destroy(second); uvhttp_server_free(server); } + +/* ---------- ownership: NULL is not a detach ---------- + * + * Before v2.10, take_router / take_context were "set_*" and passing NULL was + * documented as clearing the field. That only ever worked when the server + * held nothing: once an object is taken, the reject-if-different guard turns + * NULL into an error too. There is no detach operation - a taken object + * stays owned by the server until uvhttp_server_free. These tests pin that + * (the misinterpretation is what made callers free a router the server still + * owns). */ + +TEST(UvhttpServerOwnership, NullRouterAfterTakeIsRejected) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_new(loop, &server), UVHTTP_OK); + + uvhttp_router_t* router = nullptr; + ASSERT_EQ(uvhttp_router_new(&router), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); + + EXPECT_EQ(uvhttp_server_take_router(server, nullptr), + UVHTTP_ERROR_INVALID_PARAM) + << "there is no detach: a taken router stays owned by the server"; + EXPECT_EQ(server->router, router) + << "the rejected NULL must leave the owned router in place"; + + uvhttp_server_free(server); +} + +TEST(UvhttpServerOwnership, NullRouterWithoutRouterIsNoOp) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_new(loop, &server), UVHTTP_OK); + + EXPECT_EQ(uvhttp_server_take_router(server, nullptr), UVHTTP_OK) + << "NULL is only a no-op when the server holds no router"; + EXPECT_EQ(server->router, nullptr); + + uvhttp_server_free(server); +} + +TEST(UvhttpServerOwnership, NullContextAfterTakeIsRejected) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_new(loop, &server), UVHTTP_OK); + + uvhttp_context_t* context = nullptr; + ASSERT_EQ(uvhttp_context_create(loop, &context), UVHTTP_OK); + ASSERT_EQ(uvhttp_server_take_context(server, context), UVHTTP_OK); + + EXPECT_EQ(uvhttp_server_take_context(server, nullptr), + UVHTTP_ERROR_INVALID_PARAM) + << "there is no detach: a taken context stays owned by the server"; + EXPECT_EQ(server->context, context); + + uvhttp_server_free(server); +} + +TEST(UvhttpServerOwnership, NullContextWithoutContextIsNoOp) { + uv_loop_t* loop = uv_default_loop(); + ASSERT_NE(loop, nullptr); + + uvhttp_server_t* server = nullptr; + ASSERT_EQ(uvhttp_server_new(loop, &server), UVHTTP_OK); + + EXPECT_EQ(uvhttp_server_take_context(server, nullptr), UVHTTP_OK); + EXPECT_EQ(server->context, nullptr); + + uvhttp_server_free(server); +} diff --git a/test/unit/test_server_boost_coverage.cpp b/test/unit/test_server_boost_coverage.cpp index 32768329..0fb51759 100644 --- a/test/unit/test_server_boost_coverage.cpp +++ b/test/unit/test_server_boost_coverage.cpp @@ -103,7 +103,9 @@ TEST_F(ServerSetFunctionsTest, SetRouter_ValidServer) { TEST_F(ServerSetFunctionsTest, SetRouter_NullServer) { uvhttp_error_t err = uvhttp_server_take_router(server, nullptr); - // NULL router is a valid operation (clears router) + // NULL is only a no-op when the server holds no router (there is no + // detach operation); once a router is taken, NULL is rejected just like + // any other pointer - see test_server_atomic_construct.cpp. EXPECT_EQ(err, UVHTTP_OK); } From bf8ef677f394e1de34738c00c12d2d482c41763b Mon Sep 17 00:00:00 2001 From: adam Date: Sat, 10 Oct 2026 16:31:49 +0800 Subject: [PATCH 4/4] =?UTF-8?q?style(test):=20clang-format=2018.1.8=20?= =?UTF-8?q?=E9=87=8D=E6=8E=92=E6=9C=AC=20PR=20=E6=94=B9=E5=8A=A8=E7=9A=84?= =?UTF-8?q?=2011=20=E4=B8=AA=20C/C++=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release 门禁(scripts/ci/release_checklist.sh)对 main...HEAD 改动的 C/C++ 文件逐一跑 clang-format --dry-run --Werror。#470 引入的机械改名 带进了既有违规(test_server_atomic_construct.cpp 提交时即 23 处), 发布前必须清零——本 PR 不改,它们也是 "本次改动文件"。 全量清单与处数见提交说明:11 文件、469+/338-。无逻辑改动,ctest 120/120 复跑通过(纯空白重排)。与 #416 的存量全量重格式化同一处理方式, 区别是这里只覆盖本 PR 触达的文件。 --- include/uvhttp_server.h | 3 +- test/unit/test_boundary_null_safety.cpp | 15 +- .../test_connection_integration_coverage.cpp | 25 +- test/unit/test_death.cpp | 36 ++- test/unit/test_memory.cpp | 14 +- .../test_protocol_upgrade_boost_coverage.cpp | 155 ++++++----- test/unit/test_request_response_coverage.cpp | 87 ++++--- test/unit/test_server_atomic_construct.cpp | 73 +++--- test/unit/test_server_boost_coverage.cpp | 243 ++++++++++-------- test/unit/test_server_boost_coverage2.cpp | 116 ++++++--- test/unit/test_stress.cpp | 40 +-- 11 files changed, 469 insertions(+), 338 deletions(-) diff --git a/include/uvhttp_server.h b/include/uvhttp_server.h index c3e4d163..233e2f66 100644 --- a/include/uvhttp_server.h +++ b/include/uvhttp_server.h @@ -276,8 +276,7 @@ uvhttp_error_t uvhttp_server_stop(uvhttp_server_t* server); uvhttp_error_t uvhttp_server_listen_routes(uv_loop_t* loop, const uvhttp_route_t* routes, size_t route_count, const char* host, - int port, - uvhttp_server_t** server); + int port, uvhttp_server_t** server); #if UVHTTP_FEATURE_TLS uvhttp_error_t uvhttp_server_enable_tls(uvhttp_server_t* server, uvhttp_tls_context_t* tls_ctx); diff --git a/test/unit/test_boundary_null_safety.cpp b/test/unit/test_boundary_null_safety.cpp index b9c15d7f..f107f41d 100644 --- a/test/unit/test_boundary_null_safety.cpp +++ b/test/unit/test_boundary_null_safety.cpp @@ -4,18 +4,19 @@ * zero-length buffers, and NULL parameter paths. */ -#include -#include "uvhttp_server.h" +#include "uvhttp_allocator.h" +#include "uvhttp_config.h" #include "uvhttp_connection.h" +#include "uvhttp_context.h" +#include "uvhttp_error.h" #include "uvhttp_request.h" #include "uvhttp_response.h" #include "uvhttp_router.h" -#include "uvhttp_config.h" -#include "uvhttp_context.h" -#include "uvhttp_error.h" -#include "uvhttp_allocator.h" -#include +#include "uvhttp_server.h" + +#include #include +#include /* ========== Null-pointer safety for public APIs ========== */ diff --git a/test/unit/test_connection_integration_coverage.cpp b/test/unit/test_connection_integration_coverage.cpp index 4bf962ef..b752fe0d 100644 --- a/test/unit/test_connection_integration_coverage.cpp +++ b/test/unit/test_connection_integration_coverage.cpp @@ -4,19 +4,20 @@ * Creates a real server and TCP client to trigger internal libuv callbacks. */ -#include +#include "uvhttp_allocator.h" #include "uvhttp_connection.h" -#include "uvhttp_server.h" #include "uvhttp_context.h" #include "uvhttp_error.h" -#include "uvhttp_allocator.h" #include "uvhttp_router.h" +#include "uvhttp_server.h" + +#include +#include +#include #include -#include #include -#include #include -#include +#include static int test_handler(uvhttp_request_t* req, uvhttp_response_t* resp) { (void)req; @@ -28,7 +29,8 @@ static int test_handler(uvhttp_request_t* req, uvhttp_response_t* resp) { static int connect_to_port(int port) { int fd = socket(AF_INET, SOCK_STREAM, 0); - if (fd < 0) return -1; + if (fd < 0) + return -1; struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; @@ -59,7 +61,8 @@ static void run_loop_with_timeout(uv_loop_t* loop, int ms) { /* Setup server and get port */ static int setup_server(uv_loop_t* loop, uvhttp_server_t** server) { uvhttp_error_t err = uvhttp_server_new(loop, server); - if (err != UVHTTP_OK) return -1; + if (err != UVHTTP_OK) + return -1; uvhttp_router_t* router = nullptr; uvhttp_router_new(&router); @@ -69,12 +72,14 @@ static int setup_server(uv_loop_t* loop, uvhttp_server_t** server) { } err = uvhttp_server_listen(*server, "127.0.0.1", 0); - if (err != UVHTTP_OK) return -1; + if (err != UVHTTP_OK) + return -1; struct sockaddr_in addr; int namelen = sizeof(addr); memset(&addr, 0, sizeof(addr)); - uv_tcp_getsockname(&(*server)->tcp_handle, (struct sockaddr*)&addr, &namelen); + uv_tcp_getsockname(&(*server)->tcp_handle, (struct sockaddr*)&addr, + &namelen); return ntohs(addr.sin_port); } diff --git a/test/unit/test_death.cpp b/test/unit/test_death.cpp index 3b7854f7..1d45a95f 100644 --- a/test/unit/test_death.cpp +++ b/test/unit/test_death.cpp @@ -6,16 +6,18 @@ * 注意:由于项目限制,这里只测试 NULL 指针处理,不触发实际崩溃 */ -#include -#include -#include "uvhttp.h" -#include "uvhttp_server.h" -#include "uvhttp_router.h" -#include "uvhttp_response.h" -#include "uvhttp_config.h" -#include "uvhttp_error.h" #include "uvhttp_allocator.h" +#include "uvhttp_config.h" #include "uvhttp_context.h" +#include "uvhttp_error.h" +#include "uvhttp_response.h" +#include "uvhttp_router.h" +#include "uvhttp_server.h" + +#include "uvhttp.h" + +#include +#include /** * @brief 测试 NULL 服务器销毁(不崩溃,安全无操作) @@ -76,7 +78,8 @@ TEST(DeathTest, NullServerSetRouter) { * @brief 测试 NULL 路由器添加路由 */ TEST(DeathTest, NullRouterAddRoute) { - uvhttp_request_handler_t handler = [](uvhttp_request_t* req, uvhttp_response_t* res) -> int { + uvhttp_request_handler_t handler = [](uvhttp_request_t* req, + uvhttp_response_t* res) -> int { uvhttp_response_set_status(res, 200); return uvhttp_response_send(res); }; @@ -93,7 +96,8 @@ TEST(DeathTest, NullPathAddRoute) { uvhttp_error_t result = uvhttp_router_new(&router); ASSERT_EQ(result, UVHTTP_OK); ASSERT_NE(router, nullptr); - uvhttp_request_handler_t handler = [](uvhttp_request_t* req, uvhttp_response_t* res) -> int { + uvhttp_request_handler_t handler = [](uvhttp_request_t* req, + uvhttp_response_t* res) -> int { uvhttp_response_set_status(res, 200); return uvhttp_response_send(res); }; @@ -128,7 +132,8 @@ TEST(DeathTest, NullResponseSetStatus) { * @brief 测试 NULL 响应设置头 */ TEST(DeathTest, NullResponseSetHeader) { - uvhttp_error_t result = uvhttp_response_set_header(nullptr, "Content-Type", "text/plain"); + uvhttp_error_t result = + uvhttp_response_set_header(nullptr, "Content-Type", "text/plain"); EXPECT_NE(result, UVHTTP_OK); } @@ -233,17 +238,20 @@ TEST(DeathTest, ErrorCodes) { EXPECT_STRNE(error_str, ""); // 不应该是空字符串 // 测试错误分类 - const char* category = uvhttp_error_category_string(UVHTTP_ERROR_INVALID_PARAM); + const char* category = + uvhttp_error_category_string(UVHTTP_ERROR_INVALID_PARAM); ASSERT_NE(category, nullptr); EXPECT_STRNE(category, ""); // 不应该是空字符串 // 测试错误描述 - const char* description = uvhttp_error_description(UVHTTP_ERROR_INVALID_PARAM); + const char* description = + uvhttp_error_description(UVHTTP_ERROR_INVALID_PARAM); ASSERT_NE(description, nullptr); EXPECT_STRNE(description, ""); // 不应该是空字符串 // 测试错误建议 - const char* suggestion = uvhttp_error_suggestion(UVHTTP_ERROR_INVALID_PARAM); + const char* suggestion = + uvhttp_error_suggestion(UVHTTP_ERROR_INVALID_PARAM); ASSERT_NE(suggestion, nullptr); EXPECT_STRNE(suggestion, ""); // 不应该是空字符串 diff --git a/test/unit/test_memory.cpp b/test/unit/test_memory.cpp index 181253cf..a17fb75b 100644 --- a/test/unit/test_memory.cpp +++ b/test/unit/test_memory.cpp @@ -5,15 +5,17 @@ * 内存测试用于验证系统的内存使用情况 */ +#include "uvhttp_allocator.h" +#include "uvhttp_config.h" +#include "uvhttp_response.h" +#include "uvhttp_router.h" +#include "uvhttp_server.h" + +#include "uvhttp.h" + #include #include #include -#include "uvhttp.h" -#include "uvhttp_server.h" -#include "uvhttp_router.h" -#include "uvhttp_response.h" -#include "uvhttp_config.h" -#include "uvhttp_allocator.h" // 内存测试配置 #define MEMORY_TEST_ALLOCATIONS 1000 diff --git a/test/unit/test_protocol_upgrade_boost_coverage.cpp b/test/unit/test_protocol_upgrade_boost_coverage.cpp index c8d5c562..cefe0c25 100644 --- a/test/unit/test_protocol_upgrade_boost_coverage.cpp +++ b/test/unit/test_protocol_upgrade_boost_coverage.cpp @@ -16,12 +16,12 @@ extern "C" { #include "uvhttp_server.h" } -#include +#include #include +#include #include #include #include -#include // Dummy detector and handler for testing static int test_detector(uvhttp_request_t* request, char* protocol_name, @@ -46,7 +46,7 @@ static uvhttp_error_t test_handler(uvhttp_connection_t* conn, } class ProtocolUpgradeTest : public ::testing::Test { -protected: + protected: uv_loop_t loop; uvhttp_server_t* server = nullptr; @@ -83,15 +83,17 @@ class ProtocolUpgradeTest : public ::testing::Test { // ========== uvhttp_server_register_protocol_upgrade ========== TEST_F(ProtocolUpgradeTest, Register_NullServer_ReturnsError) { - EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - nullptr, "test", "websocket", test_detector, test_handler, nullptr), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_register_protocol_upgrade( + nullptr, "test", "websocket", test_detector, test_handler, nullptr), + UVHTTP_ERROR_INVALID_PARAM); } TEST_F(ProtocolUpgradeTest, Register_NullName_ReturnsError) { - EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, nullptr, "websocket", test_detector, test_handler, nullptr), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_register_protocol_upgrade( + server, nullptr, "websocket", test_detector, test_handler, nullptr), + UVHTTP_ERROR_INVALID_PARAM); } TEST_F(ProtocolUpgradeTest, Register_NullDetector_ReturnsError) { @@ -110,46 +112,53 @@ TEST_F(ProtocolUpgradeTest, Register_NameTooLong_ReturnsError) { char long_name[64]; memset(long_name, 'a', sizeof(long_name) - 1); long_name[sizeof(long_name) - 1] = '\0'; - EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, long_name, nullptr, test_detector, test_handler, nullptr), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_register_protocol_upgrade( + server, long_name, nullptr, test_detector, test_handler, nullptr), + UVHTTP_ERROR_INVALID_PARAM); } TEST_F(ProtocolUpgradeTest, Register_UpgradeHeaderTooLong_ReturnsError) { char long_header[128]; memset(long_header, 'b', sizeof(long_header) - 1); long_header[sizeof(long_header) - 1] = '\0'; - EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, "test", long_header, test_detector, test_handler, nullptr), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_register_protocol_upgrade( + server, "test", long_header, test_detector, test_handler, nullptr), + UVHTTP_ERROR_INVALID_PARAM); } TEST_F(ProtocolUpgradeTest, Register_Valid_CreatesRegistry) { // Server pre-registers websocket, so use a different name EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, "custom-proto", "custom-upgrade", test_detector, test_handler, nullptr), + server, "custom-proto", "custom-upgrade", test_detector, + test_handler, nullptr), UVHTTP_OK); EXPECT_NE(server->protocol_registry, nullptr); } TEST_F(ProtocolUpgradeTest, Register_NullUpgradeHeader_Success) { - EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, "custom", nullptr, test_detector, test_handler, nullptr), - UVHTTP_OK); + EXPECT_EQ( + uvhttp_server_register_protocol_upgrade( + server, "custom", nullptr, test_detector, test_handler, nullptr), + UVHTTP_OK); } TEST_F(ProtocolUpgradeTest, Register_DuplicateName_ReturnsAlreadyExists) { EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, "myproto", "myupgrade", test_detector, test_handler, nullptr), + server, "myproto", "myupgrade", test_detector, test_handler, + nullptr), UVHTTP_OK); EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, "myproto", "myupgrade", test_detector, test_handler, nullptr), + server, "myproto", "myupgrade", test_detector, test_handler, + nullptr), UVHTTP_ERROR_ALREADY_EXISTS); } TEST_F(ProtocolUpgradeTest, Register_NameNormalizedToLowercase) { EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, "MyCustomProto", "MyUpgradeHeader", test_detector, test_handler, nullptr), + server, "MyCustomProto", "MyUpgradeHeader", test_detector, + test_handler, nullptr), UVHTTP_OK); uvhttp_protocol_registry_t* reg = @@ -166,14 +175,17 @@ TEST_F(ProtocolUpgradeTest, Register_MultipleProtocols_InsertedAtHead) { { uvhttp_protocol_registry_t* reg = (uvhttp_protocol_registry_t*)server->protocol_registry; - if (reg) initial_count = reg->protocol_count; + if (reg) + initial_count = reg->protocol_count; } + EXPECT_EQ( + uvhttp_server_register_protocol_upgrade( + server, "proto1", "upgrade1", test_detector, test_handler, nullptr), + UVHTTP_OK); EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, "proto1", "upgrade1", test_detector, test_handler, nullptr), - UVHTTP_OK); - EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, "proto2", "upgrade2", test_detector, test_handler, (void*)0x42), + server, "proto2", "upgrade2", test_detector, test_handler, + (void*)0x42), UVHTTP_OK); uvhttp_protocol_registry_t* reg = @@ -190,13 +202,15 @@ TEST_F(ProtocolUpgradeTest, Register_MaxProtocols_ReturnsError) { for (int i = 0; i < 9; i++) { char name[32]; snprintf(name, sizeof(name), "proto%d", i); - EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, name, nullptr, test_detector, test_handler, nullptr), - UVHTTP_OK); + EXPECT_EQ( + uvhttp_server_register_protocol_upgrade( + server, name, nullptr, test_detector, test_handler, nullptr), + UVHTTP_OK); } // 11th total (10 + 1 existing) should fail - EXPECT_EQ(uvhttp_server_register_protocol_upgrade( - server, "proto_extra", nullptr, test_detector, test_handler, nullptr), + EXPECT_EQ(uvhttp_server_register_protocol_upgrade(server, "proto_extra", + nullptr, test_detector, + test_handler, nullptr), UVHTTP_ERROR_INVALID_PARAM); } @@ -227,8 +241,9 @@ TEST_F(ProtocolUpgradeTest, Unregister_Valid_RemovesProtocol) { (uvhttp_protocol_registry_t*)server->protocol_registry; size_t initial = reg->protocol_count; - uvhttp_server_register_protocol_upgrade( - server, "tempproto", "tempupgrade", test_detector, test_handler, nullptr); + uvhttp_server_register_protocol_upgrade(server, "tempproto", "tempupgrade", + test_detector, test_handler, + nullptr); EXPECT_EQ(reg->protocol_count, initial + 1); EXPECT_EQ(uvhttp_server_unregister_protocol_upgrade(server, "tempproto"), @@ -297,7 +312,8 @@ TEST_F(ProtocolUpgradeTest, SetLifecycle_NullLifecycle_ReturnsError) { TEST_F(ProtocolUpgradeTest, GetFd_NullConn_ReturnsError) { int fd = 0; - EXPECT_EQ(uvhttp_connection_get_fd(nullptr, &fd), UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ(uvhttp_connection_get_fd(nullptr, &fd), + UVHTTP_ERROR_INVALID_PARAM); } TEST_F(ProtocolUpgradeTest, GetFd_NullFd_ReturnsError) { @@ -342,8 +358,9 @@ TEST_F(ProtocolUpgradeTest, TransferOwnership_NullConn_ReturnsError) { TEST_F(ProtocolUpgradeTest, TransferOwnership_NullCallback_ReturnsError) { uvhttp_connection_t dummy_conn; memset(&dummy_conn, 0, sizeof(dummy_conn)); - EXPECT_EQ(uvhttp_connection_transfer_ownership(&dummy_conn, nullptr, nullptr), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_connection_transfer_ownership(&dummy_conn, nullptr, nullptr), + UVHTTP_ERROR_INVALID_PARAM); } // ========== uvhttp_connection_transfer_ownership wrong state ========== @@ -404,18 +421,20 @@ TEST_F(ProtocolUpgradeTest, SetLifecycle_Overwrite_Works) { uvhttp_connection_free(conn); } -// ========== uvhttp_connection_get_fd and get_peer_address with accepted connection ========== +// ========== uvhttp_connection_get_fd and get_peer_address with accepted +// connection ========== // Helper: connect callback for protocol upgrade tests static void proto_upgrade_on_connect(uv_connect_t* req, int status) { - if (status < 0) return; + if (status < 0) + return; bool* flag = (bool*)req->data; *flag = true; } // Helper: alloc callback for protocol upgrade read static void proto_upgrade_alloc_cb(uv_handle_t* handle, size_t suggested_size, - uv_buf_t* buf) { + uv_buf_t* buf) { (void)handle; (void)suggested_size; static char slab[4096]; @@ -445,7 +464,8 @@ static void proto_upgrade_pump_loop(uv_loop_t* loop, int timeout_ms) { clock_gettime(CLOCK_MONOTONIC, &now); long elapsed_ms = (now.tv_sec - start.tv_sec) * 1000 + (now.tv_nsec - start.tv_nsec) / 1000000; - if (elapsed_ms >= timeout_ms) break; + if (elapsed_ms >= timeout_ms) + break; usleep(1000); // 1ms } } @@ -459,7 +479,7 @@ static socklen_t g_handler_peer_addr_len = 0; // Handler that exercises get_fd and get_peer_address on the connection static int fd_peeraddr_handler(uvhttp_request_t* request, - uvhttp_response_t* response) { + uvhttp_response_t* response) { (void)response; // Get the connection from request->client using offsetof @@ -491,7 +511,7 @@ static int fd_peeraddr_handler(uvhttp_request_t* request, } class ConnectionHelperTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; uvhttp_router_t* router = nullptr; @@ -510,7 +530,8 @@ class ConnectionHelperTest : public ::testing::Test { void TearDown() override { uv_walk(&loop, proto_upgrade_close_walk_cb, nullptr); for (int i = 0; i < 20; i++) { - if (uv_run(&loop, UV_RUN_NOWAIT) == 0) break; + if (uv_run(&loop, UV_RUN_NOWAIT) == 0) + break; } if (router) { server->router = nullptr; @@ -530,7 +551,7 @@ TEST_F(ConnectionHelperTest, GetFd_WithAcceptedConnection_ReturnsValidFd) { uvhttp_error_t rerr = uvhttp_router_new(&router); ASSERT_EQ(rerr, UVHTTP_OK); rerr = uvhttp_router_add_route_method(router, "/test", UVHTTP_GET, - fd_peeraddr_handler); + fd_peeraddr_handler); ASSERT_EQ(rerr, UVHTTP_OK); uvhttp_server_take_router(server, router); @@ -542,7 +563,7 @@ TEST_F(ConnectionHelperTest, GetFd_WithAcceptedConnection_ReturnsValidFd) { struct sockaddr_in bound_addr; int namelen = sizeof(bound_addr); int ret = uv_tcp_getsockname(&server->tcp_handle, - (struct sockaddr*)&bound_addr, &namelen); + (struct sockaddr*)&bound_addr, &namelen); ASSERT_EQ(ret, 0); int port = ntohs(bound_addr.sin_port); @@ -556,18 +577,17 @@ TEST_F(ConnectionHelperTest, GetFd_WithAcceptedConnection_ReturnsValidFd) { bool connected = false; connect_req.data = &connected; ret = uv_tcp_connect(&connect_req, &client, - (const struct sockaddr*)&connect_addr, - proto_upgrade_on_connect); + (const struct sockaddr*)&connect_addr, + proto_upgrade_on_connect); ASSERT_EQ(ret, 0); proto_upgrade_pump_loop(&loop, 200); ASSERT_TRUE(connected); // Send HTTP request - const char* http_request = - "GET /test HTTP/1.1\r\n" - "Host: 127.0.0.1\r\n" - "Connection: close\r\n" - "\r\n"; + const char* http_request = "GET /test HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; uv_buf_t write_buf = uv_buf_init((char*)http_request, strlen(http_request)); uv_write_t write_req; bool write_done = false; @@ -600,19 +620,19 @@ TEST_F(ConnectionHelperTest, GetFd_WithAcceptedConnection_ReturnsValidFd) { // Verify get_fd was called successfully EXPECT_EQ(g_handler_fd_result, UVHTTP_OK) << "uvhttp_connection_get_fd should succeed on accepted connection"; - EXPECT_GE(g_handler_fd, 0) - << "File descriptor should be non-negative"; + EXPECT_GE(g_handler_fd, 0) << "File descriptor should be non-negative"; uv_close((uv_handle_t*)&client, proto_upgrade_on_close); proto_upgrade_pump_loop(&loop, 100); } -TEST_F(ConnectionHelperTest, GetPeerAddress_WithAcceptedConnection_ReturnsAddr) { +TEST_F(ConnectionHelperTest, + GetPeerAddress_WithAcceptedConnection_ReturnsAddr) { // Set up router with handler that calls get_peer_address uvhttp_error_t rerr = uvhttp_router_new(&router); ASSERT_EQ(rerr, UVHTTP_OK); rerr = uvhttp_router_add_route_method(router, "/test", UVHTTP_GET, - fd_peeraddr_handler); + fd_peeraddr_handler); ASSERT_EQ(rerr, UVHTTP_OK); uvhttp_server_take_router(server, router); @@ -624,7 +644,7 @@ TEST_F(ConnectionHelperTest, GetPeerAddress_WithAcceptedConnection_ReturnsAddr) struct sockaddr_in bound_addr; int namelen = sizeof(bound_addr); int ret = uv_tcp_getsockname(&server->tcp_handle, - (struct sockaddr*)&bound_addr, &namelen); + (struct sockaddr*)&bound_addr, &namelen); ASSERT_EQ(ret, 0); int port = ntohs(bound_addr.sin_port); @@ -638,18 +658,17 @@ TEST_F(ConnectionHelperTest, GetPeerAddress_WithAcceptedConnection_ReturnsAddr) bool connected = false; connect_req.data = &connected; ret = uv_tcp_connect(&connect_req, &client, - (const struct sockaddr*)&connect_addr, - proto_upgrade_on_connect); + (const struct sockaddr*)&connect_addr, + proto_upgrade_on_connect); ASSERT_EQ(ret, 0); proto_upgrade_pump_loop(&loop, 200); ASSERT_TRUE(connected); // Send HTTP request - const char* http_request = - "GET /test HTTP/1.1\r\n" - "Host: 127.0.0.1\r\n" - "Connection: close\r\n" - "\r\n"; + const char* http_request = "GET /test HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; uv_buf_t write_buf = uv_buf_init((char*)http_request, strlen(http_request)); uv_write_t write_req; bool write_done = false; @@ -680,7 +699,8 @@ TEST_F(ConnectionHelperTest, GetPeerAddress_WithAcceptedConnection_ReturnsAddr) // Verify get_peer_address was called successfully EXPECT_EQ(g_handler_peer_result, UVHTTP_OK) - << "uvhttp_connection_get_peer_address should succeed on accepted connection"; + << "uvhttp_connection_get_peer_address should succeed on accepted " + "connection"; EXPECT_GT(g_handler_peer_addr_len, (socklen_t)0) << "Address length should be positive"; @@ -698,7 +718,8 @@ TEST_F(ConnectionHelperTest, GetPeerAddress_WithAcceptedConnection_ReturnsAddr) inet_ntop(AF_INET6, &peer_in6->sin6_addr, ip_str, sizeof(ip_str)); EXPECT_STREQ(ip_str, "::1"); } else { - FAIL() << "Unexpected address family: " << g_handler_peer_addr.ss_family; + FAIL() << "Unexpected address family: " + << g_handler_peer_addr.ss_family; } uv_close((uv_handle_t*)&client, proto_upgrade_on_close); diff --git a/test/unit/test_request_response_coverage.cpp b/test/unit/test_request_response_coverage.cpp index 64d293c7..4f56464b 100644 --- a/test/unit/test_request_response_coverage.cpp +++ b/test/unit/test_request_response_coverage.cpp @@ -34,7 +34,7 @@ extern "C" { // ============================================================================ class ResponseCoverageTest : public ::testing::Test { -protected: + protected: uvhttp_response_t* resp = nullptr; void SetUp() override { @@ -175,8 +175,9 @@ TEST_F(ResponseCoverageTest, BuildData_HeaderWithDEL_Skipped) { // Response: null header in build_response_headers iteration (line 159-160) // // get_header_at returns NULL when index >= UVHTTP_INLINE_HEADERS_CAPACITY -// and headers_extra is NULL. Setting header_count > UVHTTP_INLINE_HEADERS_CAPACITY -// without allocating headers_extra triggers this path. +// and headers_extra is NULL. Setting header_count > +// UVHTTP_INLINE_HEADERS_CAPACITY without allocating headers_extra triggers this +// path. // ============================================================================ TEST_F(ResponseCoverageTest, BuildData_NullHeaderInIteration_Skipped) { @@ -220,10 +221,11 @@ TEST_F(ResponseCoverageTest, SetHeader_ReallocPath_TriggersRealloc) { // old_extra_count = 40 - 32 = 8 (non-zero) -> realloc path const size_t fake_capacity = 40; - const size_t fake_extra_count = fake_capacity - UVHTTP_INLINE_HEADERS_CAPACITY; + const size_t fake_extra_count = + fake_capacity - UVHTTP_INLINE_HEADERS_CAPACITY; - resp->headers_extra = - (uvhttp_header_t*)uvhttp_calloc(fake_extra_count, sizeof(uvhttp_header_t)); + resp->headers_extra = (uvhttp_header_t*)uvhttp_calloc( + fake_extra_count, sizeof(uvhttp_header_t)); ASSERT_NE(resp->headers_extra, nullptr); resp->headers_capacity = fake_capacity; @@ -240,7 +242,8 @@ TEST_F(ResponseCoverageTest, SetHeader_ReallocPath_TriggersRealloc) { EXPECT_NE(resp->headers_extra, nullptr); // Adding one more should trigger realloc (lines 337-338) - uvhttp_error_t err = uvhttp_response_set_header(resp, "X-Extra", "reallocated"); + uvhttp_error_t err = + uvhttp_response_set_header(resp, "X-Extra", "reallocated"); EXPECT_EQ(err, UVHTTP_OK); EXPECT_EQ(resp->header_count, fake_capacity + 1); @@ -266,7 +269,7 @@ TEST_F(ResponseCoverageTest, SetHeader_ReallocPath_TriggersRealloc) { // ============================================================================ class RequestCoverageTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; uvhttp_connection_t* conn = nullptr; @@ -315,8 +318,7 @@ static int s_cov_detector_called = 0; static int s_cov_handler_called = 0; static uvhttp_error_t s_cov_handler_return = UVHTTP_OK; -static int cov_detector_matches(uvhttp_request_t* request, - char* protocol_name, +static int cov_detector_matches(uvhttp_request_t* request, char* protocol_name, size_t protocol_name_len, const char* upgrade_header, const char* connection_header) { @@ -330,8 +332,7 @@ static int cov_detector_matches(uvhttp_request_t* request, return 0; } -static int cov_detector_always(uvhttp_request_t* request, - char* protocol_name, +static int cov_detector_always(uvhttp_request_t* request, char* protocol_name, size_t protocol_name_len, const char* upgrade_header, const char* connection_header) { @@ -457,13 +458,14 @@ TEST_F(RequestCoverageTest, ProtoUpgrade_DefaultWebSocket_SingleProtoPath) { // llhttp returns HPE_PAUSED_UPGRADE for Upgrade requests EXPECT_TRUE(rc == 0 || rc == HPE_PAUSED_UPGRADE); - // The default WebSocket handler was invoked (single proto path lines 392-400) - // The handler may succeed or fail depending on the handshake logic, - // but the code path is covered. + // The default WebSocket handler was invoked (single proto path lines + // 392-400) The handler may succeed or fail depending on the handshake + // logic, but the code path is covered. } // ============================================================================ -// Protocol upgrade: single protocol, no upgrade_header specified (lines 415-437) +// Protocol upgrade: single protocol, no upgrade_header specified (lines +// 415-437) // // Unregister the default websocket protocol, then register a custom one // with empty upgrade_header so proto->upgrade_header[0] == '\0', @@ -673,8 +675,10 @@ TEST_F(RequestCoverageTest, EnsureValidUrl_EmptyUrl_DefaultsToRoot) { // Route handler that checks the request URL static const char* captured_path = nullptr; - auto url_capture = [](uvhttp_request_t* req, uvhttp_response_t* resp) -> int { - // This is a lambda but we need a function pointer; use a static variable + auto url_capture = [](uvhttp_request_t* req, + uvhttp_response_t* resp) -> int { + // This is a lambda but we need a function pointer; use a static + // variable return 0; }; @@ -702,7 +706,7 @@ TEST_F(RequestCoverageTest, EnsureValidUrl_EmptyUrl_DefaultsToRoot) { // ============================================================================ class ClientIpTcpTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uv_tcp_t server_handle{}; uv_tcp_t client_handle{}; @@ -739,7 +743,8 @@ class ClientIpTcpTest : public ::testing::Test { } // Run loop to process close callbacks for (int i = 0; i < 20; i++) { - if (uv_run(&loop, UV_RUN_NOWAIT) == 0) break; + if (uv_run(&loop, UV_RUN_NOWAIT) == 0) + break; } if (accepted_handle) { uvhttp_free(accepted_handle); @@ -769,18 +774,22 @@ class ClientIpTcpTest : public ::testing::Test { bool SetupConnectedSocket() { // Initialize server TCP handle int r = uv_tcp_init(&loop, &server_handle); - if (r != 0) return false; + if (r != 0) + return false; server_initialized = true; struct sockaddr_in addr; uv_ip4_addr("127.0.0.1", 0, &addr); // port 0 = ephemeral r = uv_tcp_bind(&server_handle, (const struct sockaddr*)&addr, 0); - if (r != 0) return false; + if (r != 0) + return false; // Get the actual bound port int namelen = sizeof(addr); - r = uv_tcp_getsockname(&server_handle, (struct sockaddr*)&addr, &namelen); - if (r != 0) return false; + r = uv_tcp_getsockname(&server_handle, (struct sockaddr*)&addr, + &namelen); + if (r != 0) + return false; int port = ntohs(addr.sin_port); // Set server data pointer for callback @@ -788,18 +797,22 @@ class ClientIpTcpTest : public ::testing::Test { // Pre-allocate accepted handle before listen accepted_handle = (uv_tcp_t*)uvhttp_alloc(sizeof(uv_tcp_t)); - if (!accepted_handle) return false; + if (!accepted_handle) + return false; r = uv_tcp_init(&loop, accepted_handle); - if (r != 0) return false; + if (r != 0) + return false; accepted_initialized = true; // Listen with callback r = uv_listen((uv_stream_t*)&server_handle, 1, on_connection); - if (r != 0) return false; + if (r != 0) + return false; // Initialize client TCP handle r = uv_tcp_init(&loop, &client_handle); - if (r != 0) return false; + if (r != 0) + return false; client_initialized = true; // Connect client to server using libuv async connect @@ -809,7 +822,8 @@ class ClientIpTcpTest : public ::testing::Test { r = uv_tcp_connect(&connect_req, &client_handle, (const struct sockaddr*)&connect_addr, on_connect); - if (r != 0) return false; + if (r != 0) + return false; // Run the event loop until both connect and accept complete for (int i = 0; i < 100 && !(connect_done && accepted_done); i++) { @@ -925,8 +939,7 @@ TEST_F(ClientIpTcpTest, RateLimitWhitelist_DisabledRateLimit) { // upgrade_header but proto has one set (skip path, lines 387-391) // ============================================================================ -static int cov_detector_never(uvhttp_request_t* request, - char* protocol_name, +static int cov_detector_never(uvhttp_request_t* request, char* protocol_name, size_t protocol_name_len, const char* upgrade_header, const char* connection_header) { @@ -939,7 +952,8 @@ static int cov_detector_never(uvhttp_request_t* request, return 0; } -TEST_F(RequestCoverageTest, ProtoUpgrade_SingleProto_HeaderMismatch_DetectorNotCalled) { +TEST_F(RequestCoverageTest, + ProtoUpgrade_SingleProto_HeaderMismatch_DetectorNotCalled) { s_cov_detector_called = 0; s_cov_handler_called = 0; @@ -980,8 +994,9 @@ TEST_F(ResponseCoverageTest, GetHeaderAt_BeyondCapacity_NoExtra) { // header_count > UVHTTP_INLINE_HEADERS_CAPACITY but no headers_extra resp->header_count = UVHTTP_INLINE_HEADERS_CAPACITY + 5; resp->headers_extra = nullptr; - EXPECT_EQ(uvhttp_response_get_header_at(resp, UVHTTP_INLINE_HEADERS_CAPACITY + 1), - nullptr); + EXPECT_EQ( + uvhttp_response_get_header_at(resp, UVHTTP_INLINE_HEADERS_CAPACITY + 1), + nullptr); } // ============================================================================ @@ -1040,7 +1055,7 @@ TEST_F(RequestCoverageTest, Router_NoMatch_404Response) { uvhttp_router_t* router = nullptr; ASSERT_EQ(uvhttp_router_new(&router), UVHTTP_OK); ASSERT_EQ(uvhttp_router_add_route_method(router, "/other", UVHTTP_GET, - cov_route_handler), + cov_route_handler), UVHTTP_OK); ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); @@ -1057,7 +1072,7 @@ TEST_F(RequestCoverageTest, Router_Match_HandlerCalled) { uvhttp_router_t* router = nullptr; ASSERT_EQ(uvhttp_router_new(&router), UVHTTP_OK); ASSERT_EQ(uvhttp_router_add_route_method(router, "/api/test", UVHTTP_GET, - cov_route_handler), + cov_route_handler), UVHTTP_OK); ASSERT_EQ(uvhttp_server_take_router(server, router), UVHTTP_OK); diff --git a/test/unit/test_server_atomic_construct.cpp b/test/unit/test_server_atomic_construct.cpp index 3ddbc1ed..60f2834b 100644 --- a/test/unit/test_server_atomic_construct.cpp +++ b/test/unit/test_server_atomic_construct.cpp @@ -17,8 +17,9 @@ #include extern "C" { -#include "uvhttp.h" #include "uvhttp_context.h" + +#include "uvhttp.h" } #include @@ -61,9 +62,9 @@ TEST(UvhttpServerAtomicConstruct, BuildsListeningServerWithRoutes) { }; uvhttp_server_t* server = nullptr; - ASSERT_EQ(uvhttp_server_listen_routes(loop, routes, 3, "127.0.0.1", 0, - &server), - UVHTTP_OK) + ASSERT_EQ( + uvhttp_server_listen_routes(loop, routes, 3, "127.0.0.1", 0, &server), + UVHTTP_OK) << "atomic construction should succeed"; ASSERT_NE(server, nullptr); @@ -77,9 +78,9 @@ TEST(UvhttpServerAtomicConstruct, ZeroRoutesIsAllowed) { ASSERT_NE(loop, nullptr); uvhttp_server_t* server = nullptr; - ASSERT_EQ(uvhttp_server_listen_routes(loop, nullptr, 0, "127.0.0.1", 0, - &server), - UVHTTP_OK); + ASSERT_EQ( + uvhttp_server_listen_routes(loop, nullptr, 0, "127.0.0.1", 0, &server), + UVHTTP_OK); ASSERT_NE(server, nullptr); uvhttp_server_free(server); @@ -100,9 +101,9 @@ TEST(UvhttpServerAtomicConstruct, RoutesAreActuallyInstalledInTheRouter) { }; uvhttp_server_t* server = nullptr; - ASSERT_EQ(uvhttp_server_listen_routes(loop, routes, 2, "127.0.0.1", 0, - &server), - UVHTTP_OK); + ASSERT_EQ( + uvhttp_server_listen_routes(loop, routes, 2, "127.0.0.1", 0, &server), + UVHTTP_OK); ASSERT_NE(server, nullptr); ASSERT_NE(server->router, nullptr); @@ -119,15 +120,16 @@ TEST(UvhttpServerAtomicConstruct, RoutesAreActuallyInstalledInTheRouter) { uvhttp_server_free(server); } -/* ---------- failure paths: *server must be NULL, nothing allocated ---------- */ +/* ---------- failure paths: *server must be NULL, nothing allocated ---------- + */ TEST(UvhttpServerAtomicConstruct, NullOutParamIsRejected) { uv_loop_t* loop = uv_default_loop(); ASSERT_NE(loop, nullptr); - EXPECT_EQ(uvhttp_server_listen_routes(loop, nullptr, 0, "127.0.0.1", 0, - nullptr), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_listen_routes(loop, nullptr, 0, "127.0.0.1", 0, nullptr), + UVHTTP_ERROR_INVALID_PARAM); } TEST(UvhttpServerAtomicConstruct, NullLoopIsRejected) { @@ -143,9 +145,9 @@ TEST(UvhttpServerAtomicConstruct, NullHostIsRejected) { ASSERT_NE(loop, nullptr); uvhttp_server_t* server = nullptr; - EXPECT_EQ(uvhttp_server_listen_routes(loop, nullptr, 0, nullptr, 0, - &server), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_listen_routes(loop, nullptr, 0, nullptr, 0, &server), + UVHTTP_ERROR_INVALID_PARAM); EXPECT_EQ(server, nullptr); } @@ -155,9 +157,9 @@ TEST(UvhttpServerAtomicConstruct, NullRoutesWithNonZeroCountIsRejected) { ASSERT_NE(loop, nullptr); uvhttp_server_t* server = nullptr; - EXPECT_EQ(uvhttp_server_listen_routes(loop, nullptr, 3, "127.0.0.1", 0, - &server), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_listen_routes(loop, nullptr, 3, "127.0.0.1", 0, &server), + UVHTTP_ERROR_INVALID_PARAM); EXPECT_EQ(server, nullptr); } @@ -173,9 +175,9 @@ TEST(UvhttpServerAtomicConstruct, NullPathInRouteTableIsRejected) { }; uvhttp_server_t* server = nullptr; - EXPECT_EQ(uvhttp_server_listen_routes(loop, routes, 2, "127.0.0.1", 0, - &server), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_listen_routes(loop, routes, 2, "127.0.0.1", 0, &server), + UVHTTP_ERROR_INVALID_PARAM); EXPECT_EQ(server, nullptr) << "a bad entry must abort the whole construction, not half-apply it"; } @@ -189,9 +191,9 @@ TEST(UvhttpServerAtomicConstruct, NullHandlerInRouteTableIsRejected) { }; uvhttp_server_t* server = nullptr; - EXPECT_EQ(uvhttp_server_listen_routes(loop, routes, 1, "127.0.0.1", 0, - &server), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_listen_routes(loop, routes, 1, "127.0.0.1", 0, &server), + UVHTTP_ERROR_INVALID_PARAM); EXPECT_EQ(server, nullptr); } @@ -207,9 +209,9 @@ TEST(UvhttpServerAtomicConstruct, OutOfRangeMethodIsRejected) { }; uvhttp_server_t* server = nullptr; - EXPECT_EQ(uvhttp_server_listen_routes(loop, routes, 1, "127.0.0.1", 0, - &server), - UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ( + uvhttp_server_listen_routes(loop, routes, 1, "127.0.0.1", 0, &server), + UVHTTP_ERROR_INVALID_PARAM); EXPECT_EQ(server, nullptr); } @@ -230,15 +232,14 @@ TEST(UvhttpServerAtomicConstruct, ListenFailureLeavesNothingBehind) { struct sockaddr_in bind_addr; ASSERT_EQ(uv_ip4_addr("127.0.0.1", 0, &bind_addr), 0); ASSERT_EQ(uv_tcp_bind(&blocker, (const struct sockaddr*)&bind_addr, 0), 0); - ASSERT_EQ(uv_listen((uv_stream_t*)&blocker, 1, - [](uv_stream_t*, int) {}), + ASSERT_EQ(uv_listen((uv_stream_t*)&blocker, 1, [](uv_stream_t*, int) {}), 0); struct sockaddr_in bound_addr; int addr_len = sizeof(bound_addr); - ASSERT_EQ(uv_tcp_getsockname(&blocker, (struct sockaddr*)&bound_addr, - &addr_len), - 0); + ASSERT_EQ( + uv_tcp_getsockname(&blocker, (struct sockaddr*)&bound_addr, &addr_len), + 0); const int occupied_port = ntohs(bound_addr.sin_port); ASSERT_GT(occupied_port, 0); @@ -250,8 +251,8 @@ TEST(UvhttpServerAtomicConstruct, ListenFailureLeavesNothingBehind) { const uvhttp_error_t err = uvhttp_server_listen_routes( loop, routes, 1, "127.0.0.1", occupied_port, &server); - EXPECT_NE(err, UVHTTP_OK) << "port " << occupied_port - << " is occupied; listen must fail"; + EXPECT_NE(err, UVHTTP_OK) + << "port " << occupied_port << " is occupied; listen must fail"; EXPECT_EQ(server, nullptr) << "out param must be NULL after a failed bind; the router the " "server had already taken ownership of must not be freed twice"; diff --git a/test/unit/test_server_boost_coverage.cpp b/test/unit/test_server_boost_coverage.cpp index 0fb51759..c1186a94 100644 --- a/test/unit/test_server_boost_coverage.cpp +++ b/test/unit/test_server_boost_coverage.cpp @@ -13,21 +13,22 @@ #include extern "C" { -#include "uvhttp.h" #include "uvhttp_allocator.h" #include "uvhttp_server.h" + +#include "uvhttp.h" #if UVHTTP_FEATURE_TLS -#include "uvhttp_tls.h" +# include "uvhttp_tls.h" #endif } -#include -#include -#include #include +#include +#include #include #include -#include +#include +#include // Declaration not in any public header - defined in uvhttp_server.c extern "C" { @@ -50,7 +51,7 @@ static int dummy_handler(uvhttp_request_t* request, // Server Set Functions (lines 487-529) // ============================================================================ class ServerSetFunctionsTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -135,7 +136,7 @@ TEST_F(ServerSetFunctionsTest, Stop_NotListening) { // Timeout Callback (lines 1650-1661) // ============================================================================ class TimeoutCallbackTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -165,8 +166,8 @@ static void test_timeout_callback(uvhttp_server_t* srv, } TEST_F(TimeoutCallbackTest, SetTimeoutCallback_NullServer) { - uvhttp_error_t err = - uvhttp_server_set_timeout_callback(nullptr, test_timeout_callback, NULL); + uvhttp_error_t err = uvhttp_server_set_timeout_callback( + nullptr, test_timeout_callback, NULL); EXPECT_EQ(err, UVHTTP_ERROR_INVALID_PARAM); } @@ -191,7 +192,7 @@ TEST_F(TimeoutCallbackTest, SetTimeoutCallback_NullCallback) { // Builder API (lines 685-778) // ============================================================================ class BuilderAPITest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_builder_t* builder = nullptr; @@ -222,51 +223,61 @@ class BuilderAPITest : public ::testing::Test { // --- Chained route API null checks --- TEST_F(BuilderAPITest, Get_NullServer) { - uvhttp_server_builder_t* result = uvhttp_get(nullptr, "/test", dummy_handler); + uvhttp_server_builder_t* result = + uvhttp_get(nullptr, "/test", dummy_handler); EXPECT_EQ(result, nullptr); } TEST_F(BuilderAPITest, Get_NullPath) { - if (!builder) GTEST_SKIP(); - uvhttp_server_builder_t* result = uvhttp_get(builder, nullptr, dummy_handler); + if (!builder) + GTEST_SKIP(); + uvhttp_server_builder_t* result = + uvhttp_get(builder, nullptr, dummy_handler); EXPECT_EQ(result, builder); // returns server unchanged } TEST_F(BuilderAPITest, Get_NullHandler) { - if (!builder) GTEST_SKIP(); + if (!builder) + GTEST_SKIP(); uvhttp_server_builder_t* result = uvhttp_get(builder, "/test", nullptr); EXPECT_EQ(result, builder); // returns server unchanged } TEST_F(BuilderAPITest, Get_Valid) { - if (!builder) GTEST_SKIP(); - uvhttp_server_builder_t* result = uvhttp_get(builder, "/test", dummy_handler); + if (!builder) + GTEST_SKIP(); + uvhttp_server_builder_t* result = + uvhttp_get(builder, "/test", dummy_handler); EXPECT_EQ(result, builder); } TEST_F(BuilderAPITest, Post_Valid) { - if (!builder) GTEST_SKIP(); + if (!builder) + GTEST_SKIP(); uvhttp_server_builder_t* result = uvhttp_post(builder, "/submit", dummy_handler); EXPECT_EQ(result, builder); } TEST_F(BuilderAPITest, Put_Valid) { - if (!builder) GTEST_SKIP(); + if (!builder) + GTEST_SKIP(); uvhttp_server_builder_t* result = uvhttp_put(builder, "/update", dummy_handler); EXPECT_EQ(result, builder); } TEST_F(BuilderAPITest, Delete_Valid) { - if (!builder) GTEST_SKIP(); + if (!builder) + GTEST_SKIP(); uvhttp_server_builder_t* result = uvhttp_delete(builder, "/remove", dummy_handler); EXPECT_EQ(result, builder); } TEST_F(BuilderAPITest, Any_Valid) { - if (!builder) GTEST_SKIP(); + if (!builder) + GTEST_SKIP(); uvhttp_server_builder_t* result = uvhttp_any(builder, "/catch-all", dummy_handler); EXPECT_EQ(result, builder); @@ -280,7 +291,8 @@ TEST_F(BuilderAPITest, SetMaxConnections_NullServer) { } TEST_F(BuilderAPITest, SetMaxConnections_Valid) { - if (!builder) GTEST_SKIP(); + if (!builder) + GTEST_SKIP(); uvhttp_server_builder_t* result = uvhttp_set_max_connections(builder, 500); EXPECT_EQ(result, builder); EXPECT_EQ(builder->config->max_connections, 500); @@ -292,7 +304,8 @@ TEST_F(BuilderAPITest, SetTimeout_NullServer) { } TEST_F(BuilderAPITest, SetTimeout_Valid) { - if (!builder) GTEST_SKIP(); + if (!builder) + GTEST_SKIP(); uvhttp_server_builder_t* result = uvhttp_set_timeout(builder, 60); EXPECT_EQ(result, builder); EXPECT_EQ(builder->config->request_timeout, 60); @@ -305,9 +318,9 @@ TEST_F(BuilderAPITest, SetMaxBodySize_NullServer) { } TEST_F(BuilderAPITest, SetMaxBodySize_Valid) { - if (!builder) GTEST_SKIP(); - uvhttp_server_builder_t* result = - uvhttp_set_max_body_size(builder, 2048); + if (!builder) + GTEST_SKIP(); + uvhttp_server_builder_t* result = uvhttp_set_max_body_size(builder, 2048); EXPECT_EQ(result, builder); EXPECT_EQ(builder->config->max_body_size, (size_t)2048); } @@ -349,7 +362,8 @@ TEST_F(BuilderAPITest, ServerStopSimple_NullServer) { } TEST_F(BuilderAPITest, ServerStopSimple_ValidServer) { - if (!builder) GTEST_SKIP(); + if (!builder) + GTEST_SKIP(); // Server is not listening, but stop_simple just delegates uvhttp_server_stop_simple(builder); SUCCEED(); @@ -365,7 +379,7 @@ TEST_F(BuilderAPITest, ServerSimpleFree_Null) { // ============================================================================ #if UVHTTP_FEATURE_RATE_LIMIT class RateLimitBoostTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -556,20 +570,20 @@ TEST_F(RateLimitBoostTest, GetStatus_NullServer) { int remaining = 0; uint64_t reset_time = 0; EXPECT_EQ(uvhttp_server_get_rate_limit_status(nullptr, "127.0.0.1", - &remaining, &reset_time), + &remaining, &reset_time), UVHTTP_ERROR_INVALID_PARAM); } TEST_F(RateLimitBoostTest, GetStatus_NullRemaining) { - EXPECT_EQ(uvhttp_server_get_rate_limit_status(server, "127.0.0.1", - nullptr, nullptr), + EXPECT_EQ(uvhttp_server_get_rate_limit_status(server, "127.0.0.1", nullptr, + nullptr), UVHTTP_ERROR_INVALID_PARAM); } TEST_F(RateLimitBoostTest, GetStatus_NotEnabled) { int remaining = 0; EXPECT_EQ(uvhttp_server_get_rate_limit_status(server, "127.0.0.1", - &remaining, nullptr), + &remaining, nullptr), UVHTTP_OK); EXPECT_EQ(remaining, -1); } @@ -579,7 +593,7 @@ TEST_F(RateLimitBoostTest, GetStatus_Enabled) { int remaining = 0; uint64_t reset_time = 0; EXPECT_EQ(uvhttp_server_get_rate_limit_status(server, "127.0.0.1", - &remaining, &reset_time), + &remaining, &reset_time), UVHTTP_OK); EXPECT_EQ(remaining, 100); // reset_time should be > 0 since window_start_time is 0 and window is 60s @@ -592,7 +606,7 @@ TEST_F(RateLimitBoostTest, GetStatus_WithNullResetTime) { int remaining = 0; // Pass nullptr for reset_time - should still succeed EXPECT_EQ(uvhttp_server_get_rate_limit_status(server, "127.0.0.1", - &remaining, nullptr), + &remaining, nullptr), UVHTTP_OK); EXPECT_EQ(remaining, 50); } @@ -678,7 +692,7 @@ TEST_F(RateLimitBoostTest, DisableAndReenable) { // ============================================================================ #if UVHTTP_FEATURE_WEBSOCKET class WSConnectionManagementTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -693,11 +707,12 @@ class WSConnectionManagementTest : public ::testing::Test { if (server) { // Disable connection management if active (clears timers) if (server->ws_connection_manager) { - // Many tests register stack-local uvhttp_ws_connection_t objects - // that go out of scope when the test body ends. Null out the - // stored ws_conn pointers so uvhttp_server_ws_disable_connection_management - // does not dereference them (use-after-scope) when sending close - // frames. (Same pattern used by the CloseAll_* tests below.) + // Many tests register stack-local uvhttp_ws_connection_t + // objects that go out of scope when the test body ends. Null + // out the stored ws_conn pointers so + // uvhttp_server_ws_disable_connection_management does not + // dereference them (use-after-scope) when sending close frames. + // (Same pattern used by the CloseAll_* tests below.) ws_connection_node_t* node = server->ws_connection_manager->connections; while (node) { @@ -829,7 +844,8 @@ TEST_F(WSConnectionManagementTest, GetCountByPath_NullServer) { TEST_F(WSConnectionManagementTest, GetCountByPath_NullPath) { uvhttp_server_ws_enable_connection_management(server, 60, 30); - EXPECT_EQ(uvhttp_server_ws_get_connection_count_by_path(server, nullptr), 0); + EXPECT_EQ(uvhttp_server_ws_get_connection_count_by_path(server, nullptr), + 0); } TEST_F(WSConnectionManagementTest, GetCountByPath_NoManager) { @@ -1167,7 +1183,7 @@ TEST_F(WSConnectionManagementTest, CloseAll_PathNoMatch) { // WS Handler Registration (lines 850-889 in server.c) // ============================================================================ class WsHandlerRegistrationTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -1272,7 +1288,7 @@ TEST_F(WsHandlerRegistrationTest, FindWsHandler_NullPath) { // WS Send/Close null checks (lines 913-966 in server.c) // ============================================================================ class WsSendCloseTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -1318,7 +1334,7 @@ TEST_F(WsSendCloseTest, WsClose_NullConn) { // WS Disable management with no manager (no-op path) // ============================================================================ class WsDisableMgmtTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -1362,7 +1378,7 @@ int uvhttp_server_is_tls_enabled(uvhttp_server_t* server); } class TlsBoostTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -1445,12 +1461,16 @@ TEST_F(TlsBoostTest, DisableTls_ClearsFlag) { // uvhttp_serve parameter validation (lines 804-842 in server.c) // ============================================================================ class ServeParamValidationTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; - void SetUp() override { uv_loop_init(&loop); } + void SetUp() override { + uv_loop_init(&loop); + } - void TearDown() override { uv_loop_close(&loop); } + void TearDown() override { + uv_loop_close(&loop); + } }; TEST_F(ServeParamValidationTest, Serve_NullLoop) { @@ -1485,7 +1505,7 @@ TEST_F(ServeParamValidationTest, Serve_InvalidPortMaxInt) { #if UVHTTP_FEATURE_TLS class TlsRealContextTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -1610,7 +1630,7 @@ static void test_read_cb(uv_stream_t* stream, ssize_t nread, } class OnConnectionTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -1626,7 +1646,8 @@ class OnConnectionTest : public ::testing::Test { // 503 path) uv_walk(&loop, test_close_walk_cb, nullptr); for (int i = 0; i < 20; i++) { - if (uv_run(&loop, UV_RUN_NOWAIT) == 0) break; + if (uv_run(&loop, UV_RUN_NOWAIT) == 0) + break; } if (server) { @@ -1647,7 +1668,8 @@ static void pump_loop(uv_loop_t* loop, int timeout_ms) { clock_gettime(CLOCK_MONOTONIC, &now); long elapsed_ms = (now.tv_sec - start.tv_sec) * 1000 + (now.tv_nsec - start.tv_nsec) / 1000000; - if (elapsed_ms >= timeout_ms) break; + if (elapsed_ms >= timeout_ms) + break; usleep(1000); // 1ms } } @@ -1682,8 +1704,9 @@ TEST_F(OnConnectionTest, NormalConnectionAcceptance) { uv_ip4_addr("127.0.0.1", port, &connect_addr); uv_connect_t connect_req; - ret = uv_tcp_connect(&connect_req, &client, - (const struct sockaddr*)&connect_addr, test_on_connect); + ret = + uv_tcp_connect(&connect_req, &client, + (const struct sockaddr*)&connect_addr, test_on_connect); ASSERT_EQ(ret, 0); // Pump the loop for 100ms to process connect and on_connection @@ -1730,8 +1753,9 @@ TEST_F(OnConnectionTest, MaxConnectionsReached_503Response) { uv_ip4_addr("127.0.0.1", port, &connect_addr); uv_connect_t connect_req; - ret = uv_tcp_connect(&connect_req, &client, - (const struct sockaddr*)&connect_addr, test_on_connect); + ret = + uv_tcp_connect(&connect_req, &client, + (const struct sockaddr*)&connect_addr, test_on_connect); ASSERT_EQ(ret, 0); // Pump the loop for 100ms to process connect, on_connection (503 path), @@ -1841,7 +1865,7 @@ static int hello_handler(uvhttp_request_t* request, } class TcpHttpCycleTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; uvhttp_router_t* router = nullptr; @@ -1857,7 +1881,8 @@ class TcpHttpCycleTest : public ::testing::Test { // Walk and close any remaining handles uv_walk(&loop, test_close_walk_cb, nullptr); for (int i = 0; i < 20; i++) { - if (uv_run(&loop, UV_RUN_NOWAIT) == 0) break; + if (uv_run(&loop, UV_RUN_NOWAIT) == 0) + break; } if (server) { @@ -1885,7 +1910,7 @@ TEST_F(TcpHttpCycleTest, FullHttpRequestResponseCycle) { ASSERT_NE(router, nullptr); rerr = uvhttp_router_add_route_method(router, "/hello", UVHTTP_GET, - hello_handler); + hello_handler); ASSERT_EQ(rerr, UVHTTP_OK); // Attach router to server @@ -1923,13 +1948,15 @@ TEST_F(TcpHttpCycleTest, FullHttpRequestResponseCycle) { bool connected = false; // Reuse a connect callback that sets a flag via connect_req.data auto on_connect_send = [](uv_connect_t* req, int status) { - if (status < 0) return; + if (status < 0) + return; bool* flag = (bool*)req->data; *flag = true; }; connect_req.data = &connected; - ret = uv_tcp_connect(&connect_req, &client, - (const struct sockaddr*)&connect_addr, on_connect_send); + ret = + uv_tcp_connect(&connect_req, &client, + (const struct sockaddr*)&connect_addr, on_connect_send); ASSERT_EQ(ret, 0); // Pump until connected @@ -1937,11 +1964,10 @@ TEST_F(TcpHttpCycleTest, FullHttpRequestResponseCycle) { ASSERT_TRUE(connected); // Send a raw HTTP GET request via uv_write - const char* http_request = - "GET /hello HTTP/1.1\r\n" - "Host: 127.0.0.1\r\n" - "Connection: close\r\n" - "\r\n"; + const char* http_request = "GET /hello HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; uv_buf_t write_buf = uv_buf_init((char*)http_request, strlen(http_request)); uv_write_t write_req; bool write_done = false; @@ -2005,7 +2031,7 @@ TEST_F(TcpHttpCycleTest, FullHttpRequestResponseCycle) { // Handler that mimics what default_handler does (covers the same response API // code paths: set_status, set_header, set_body, send) static int default_like_handler(uvhttp_request_t* request, - uvhttp_response_t* response) { + uvhttp_response_t* response) { const char* method = uvhttp_request_get_method(request); const char* url = uvhttp_request_get_url(request); @@ -2027,7 +2053,7 @@ static int default_like_handler(uvhttp_request_t* request, } class DefaultHandlerTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; uvhttp_server_builder_t* builder = nullptr; @@ -2039,7 +2065,8 @@ class DefaultHandlerTest : public ::testing::Test { void TearDown() override { uv_walk(&loop, test_close_walk_cb, nullptr); for (int i = 0; i < 20; i++) { - if (uv_run(&loop, UV_RUN_NOWAIT) == 0) break; + if (uv_run(&loop, UV_RUN_NOWAIT) == 0) + break; } if (builder) { // uvhttp_server_simple_free -> uvhttp_server_free frees @@ -2065,7 +2092,7 @@ TEST_F(DefaultHandlerTest, DefaultRoute_RespondsWithUnifiedApiBody) { struct sockaddr_in bound_addr; int namelen = sizeof(bound_addr); int ret = uv_tcp_getsockname(&builder->server->tcp_handle, - (struct sockaddr*)&bound_addr, &namelen); + (struct sockaddr*)&bound_addr, &namelen); ASSERT_EQ(ret, 0); int port = ntohs(bound_addr.sin_port); ASSERT_GT(port, 0); @@ -2086,21 +2113,21 @@ TEST_F(DefaultHandlerTest, DefaultRoute_RespondsWithUnifiedApiBody) { connect_req.data = &connected; auto on_conn = [](uv_connect_t* req, int status) { - if (status < 0) return; + if (status < 0) + return; *(bool*)req->data = true; }; ret = uv_tcp_connect(&connect_req, &client, - (const struct sockaddr*)&connect_addr, on_conn); + (const struct sockaddr*)&connect_addr, on_conn); ASSERT_EQ(ret, 0); pump_loop(&loop, 200); ASSERT_TRUE(connected); // Send GET / HTTP/1.1 - const char* http_request = - "GET / HTTP/1.1\r\n" - "Host: 127.0.0.1\r\n" - "Connection: close\r\n" - "\r\n"; + const char* http_request = "GET / HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; uv_buf_t write_buf = uv_buf_init((char*)http_request, strlen(http_request)); uv_write_t write_req; bool write_done = false; @@ -2115,7 +2142,8 @@ TEST_F(DefaultHandlerTest, DefaultRoute_RespondsWithUnifiedApiBody) { ASSERT_TRUE(write_done); // Read the response - use a read callback that checks for "UVHTTP unified - // API" instead of the shared client_read_cb which looks for "Hello, uvhttp!" + // API" instead of the shared client_read_cb which looks for "Hello, + // uvhttp!" auto default_read_cb = [](uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { ClientReadCtx* rctx = (ClientReadCtx*)stream->data; @@ -2130,7 +2158,8 @@ TEST_F(DefaultHandlerTest, DefaultRoute_RespondsWithUnifiedApiBody) { uv_read_stop(stream); } }; - ret = uv_read_start((uv_stream_t*)&client, client_alloc_cb, default_read_cb); + ret = + uv_read_start((uv_stream_t*)&client, client_alloc_cb, default_read_cb); ASSERT_EQ(ret, 0); pump_loop(&loop, 500); uv_read_stop((uv_stream_t*)&client); @@ -2170,7 +2199,8 @@ TEST_F(DefaultHandlerTest, DefaultRoute_ServeSuccessPath_CoversDefaultHandler) { // Drain uv_walk(&loop, test_close_walk_cb, nullptr); for (int i = 0; i < 50; i++) { - if (uv_run(&loop, UV_RUN_NOWAIT) == 0) break; + if (uv_run(&loop, UV_RUN_NOWAIT) == 0) + break; } } @@ -2193,7 +2223,8 @@ static int send_data_direct_handler(uvhttp_request_t* request, // Use uvhttp_send_response_data to write raw data const char* body = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n" "Connection: close\r\n\r\nDirect write!"; - uvhttp_error_t err = uvhttp_send_response_data(response, body, strlen(body)); + uvhttp_error_t err = + uvhttp_send_response_data(response, body, strlen(body)); (void)err; return 0; } @@ -2205,7 +2236,7 @@ TEST_F(TcpHttpCycleTest, SendResponseData_DirectCall_WithRealConnection) { ASSERT_NE(router, nullptr); rerr = uvhttp_router_add_route_method(router, "/direct", UVHTTP_GET, - send_data_direct_handler); + send_data_direct_handler); ASSERT_EQ(rerr, UVHTTP_OK); uvhttp_error_t serr = uvhttp_server_take_router(server, router); @@ -2239,21 +2270,21 @@ TEST_F(TcpHttpCycleTest, SendResponseData_DirectCall_WithRealConnection) { bool connected = false; connect_req.data = &connected; auto on_conn = [](uv_connect_t* req, int status) { - if (status < 0) return; + if (status < 0) + return; *(bool*)req->data = true; }; ret = uv_tcp_connect(&connect_req, &client, - (const struct sockaddr*)&connect_addr, on_conn); + (const struct sockaddr*)&connect_addr, on_conn); ASSERT_EQ(ret, 0); pump_loop(&loop, 200); ASSERT_TRUE(connected); // Send GET /direct HTTP/1.1 - const char* http_request = - "GET /direct HTTP/1.1\r\n" - "Host: 127.0.0.1\r\n" - "Connection: close\r\n" - "\r\n"; + const char* http_request = "GET /direct HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; uv_buf_t write_buf = uv_buf_init((char*)http_request, strlen(http_request)); uv_write_t write_req; bool write_done = false; @@ -2321,7 +2352,7 @@ TEST_F(TcpHttpCycleTest, ResponseSendRaw_KeepAlive0_SetsConnKeepAlive) { ASSERT_NE(router, nullptr); rerr = uvhttp_router_add_route_method(router, "/close", UVHTTP_GET, - keepalive_close_handler); + keepalive_close_handler); ASSERT_EQ(rerr, UVHTTP_OK); uvhttp_error_t serr = uvhttp_server_take_router(server, router); @@ -2355,21 +2386,21 @@ TEST_F(TcpHttpCycleTest, ResponseSendRaw_KeepAlive0_SetsConnKeepAlive) { bool connected = false; connect_req.data = &connected; auto on_conn = [](uv_connect_t* req, int status) { - if (status < 0) return; + if (status < 0) + return; *(bool*)req->data = true; }; ret = uv_tcp_connect(&connect_req, &client, - (const struct sockaddr*)&connect_addr, on_conn); + (const struct sockaddr*)&connect_addr, on_conn); ASSERT_EQ(ret, 0); pump_loop(&loop, 200); ASSERT_TRUE(connected); // Send GET /close HTTP/1.1 - const char* http_request = - "GET /close HTTP/1.1\r\n" - "Host: 127.0.0.1\r\n" - "Connection: close\r\n" - "\r\n"; + const char* http_request = "GET /close HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; uv_buf_t write_buf = uv_buf_init((char*)http_request, strlen(http_request)); uv_write_t write_req; bool write_done = false; @@ -2395,7 +2426,8 @@ TEST_F(TcpHttpCycleTest, ResponseSendRaw_KeepAlive0_SetsConnKeepAlive) { // Verify Connection: close appears in the response EXPECT_NE(read_ctx.data.find("Connection: close"), std::string::npos) - << "Response should have Connection: close header, got: " << read_ctx.data; + << "Response should have Connection: close header, got: " + << read_ctx.data; uv_close((uv_handle_t*)&client, test_on_close); pump_loop(&loop, 100); @@ -2454,17 +2486,16 @@ TEST(DefaultHandlerCoverageTest, DefaultHandler_InvokedByServedRequest) { while (uv_run(&serve_loop, UV_RUN_NOWAIT) > 0) { } uv_loop_close(&serve_loop); - FAIL() << "Failed to connect to server on port " << port - << ": " << strerror(errno); + FAIL() << "Failed to connect to server on port " << port << ": " + << strerror(errno); } ASSERT_EQ(connect_ret, 0); // Send GET / HTTP/1.1 - const char* request = - "GET / HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: close\r\n" - "\r\n"; + const char* request = "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: close\r\n" + "\r\n"; ssize_t sent = send(sock, request, strlen(request), 0); ASSERT_GT(sent, 0); diff --git a/test/unit/test_server_boost_coverage2.cpp b/test/unit/test_server_boost_coverage2.cpp index a6e1ccc7..b99b1aa7 100644 --- a/test/unit/test_server_boost_coverage2.cpp +++ b/test/unit/test_server_boost_coverage2.cpp @@ -8,23 +8,25 @@ * - uvhttp_server_ws_close_all with non-null ws_conn (line 1512) * - ws_timeout_timer_callback (lines 1174-1208) * - ws_heartbeat_timer_callback (lines 1221-1255) - * - uvhttp_server_ws_enable_connection_management success path (lines 1307-1349) + * - uvhttp_server_ws_enable_connection_management success path (lines + * 1307-1349) */ #include extern "C" { -#include "uvhttp.h" #include "uvhttp_allocator.h" -#include "uvhttp_server.h" -#include "uvhttp_context.h" #include "uvhttp_config.h" +#include "uvhttp_context.h" +#include "uvhttp_server.h" + +#include "uvhttp.h" } #include -#include -#include #include +#include +#include // ============================================================================ // Helper: dummy request handler @@ -47,7 +49,8 @@ static void pump_loop(uv_loop_t* loop, int timeout_ms) { clock_gettime(CLOCK_MONOTONIC, &now); long elapsed_ms = (now.tv_sec - start.tv_sec) * 1000 + (now.tv_nsec - start.tv_nsec) / 1000000; - if (elapsed_ms >= timeout_ms) break; + if (elapsed_ms >= timeout_ms) + break; usleep(1000); // 1ms } } @@ -71,7 +74,7 @@ static void boost2_close_walk_cb(uv_handle_t* handle, void* arg) { // uvhttp_server_listen with context config (lines 467, 470-472) // ============================================================================ class ListenWithContextConfigTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; uvhttp_context_t* context = nullptr; @@ -88,7 +91,8 @@ class ListenWithContextConfigTest : public ::testing::Test { // Walk and close any remaining handles uv_walk(&loop, boost2_close_walk_cb, nullptr); for (int i = 0; i < 20; i++) { - if (uv_run(&loop, UV_RUN_NOWAIT) == 0) break; + if (uv_run(&loop, UV_RUN_NOWAIT) == 0) + break; } if (server) { @@ -136,7 +140,8 @@ TEST_F(ListenWithContextConfigTest, Listen_WithContextConfigBacklog) { err = uvhttp_server_set_handler(server, boost2_dummy_handler); ASSERT_EQ(err, UVHTTP_OK); - // Listen - this should use the context's config backlog (lines 467, 470-472) + // Listen - this should use the context's config backlog (lines 467, + // 470-472) err = uvhttp_server_listen(server, "127.0.0.1", 0); ASSERT_EQ(err, UVHTTP_OK); ASSERT_EQ(server->is_listening, 1); @@ -208,7 +213,7 @@ TEST_F(ListenWithContextConfigTest, Listen_WithContextConfigBacklogZero) { // uvhttp_server_ws_broadcast with OPEN connections (lines 1474-1475) // ============================================================================ class WsBroadcastOpenTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; int sock_fds[2]{-1, -1}; @@ -228,8 +233,14 @@ class WsBroadcastOpenTest : public ::testing::Test { uvhttp_server_free(server); server = nullptr; } - if (sock_fds[0] >= 0) { close(sock_fds[0]); sock_fds[0] = -1; } - if (sock_fds[1] >= 0) { close(sock_fds[1]); sock_fds[1] = -1; } + if (sock_fds[0] >= 0) { + close(sock_fds[0]); + sock_fds[0] = -1; + } + if (sock_fds[1] >= 0) { + close(sock_fds[1]); + sock_fds[1] = -1; + } uv_loop_close(&loop); } }; @@ -260,7 +271,8 @@ TEST_F(WsBroadcastOpenTest, Broadcast_OpenConnection) { err = uvhttp_server_ws_broadcast(server, "/chat", "hello", 5); EXPECT_EQ(err, UVHTTP_OK); - // Read the sent data from the other end of the socketpair to prevent buffer fill + // Read the sent data from the other end of the socketpair to prevent buffer + // fill char buf[64]; ssize_t n = recv(sock_fds[1], buf, sizeof(buf), 0); EXPECT_GT(n, 0); // Should have received the WebSocket frame @@ -315,9 +327,12 @@ TEST_F(WsBroadcastOpenTest, Broadcast_MultipleOpenConnections) { // Drain sockets char buf[64]; - while (recv(sock_fds[1], buf, sizeof(buf), MSG_DONTWAIT) > 0) {} - while (recv(fds_a[1], buf, sizeof(buf), MSG_DONTWAIT) > 0) {} - while (recv(fds_b[1], buf, sizeof(buf), MSG_DONTWAIT) > 0) {} + while (recv(sock_fds[1], buf, sizeof(buf), MSG_DONTWAIT) > 0) { + } + while (recv(fds_a[1], buf, sizeof(buf), MSG_DONTWAIT) > 0) { + } + while (recv(fds_b[1], buf, sizeof(buf), MSG_DONTWAIT) > 0) { + } // Disable management before freeing the borrowed connections. uvhttp_server_ws_disable_connection_management(server); @@ -325,15 +340,17 @@ TEST_F(WsBroadcastOpenTest, Broadcast_MultipleOpenConnections) { delete ws2; delete ws3; - close(fds_a[0]); close(fds_a[1]); - close(fds_b[0]); close(fds_b[1]); + close(fds_a[0]); + close(fds_a[1]); + close(fds_b[0]); + close(fds_b[1]); } // ============================================================================ // uvhttp_server_ws_close_all with non-null ws_conn (line 1512) // ============================================================================ class WsCloseAllWithConnTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; int sock_fds[2]{-1, -1}; @@ -353,8 +370,14 @@ class WsCloseAllWithConnTest : public ::testing::Test { uvhttp_server_free(server); server = nullptr; } - if (sock_fds[0] >= 0) { close(sock_fds[0]); sock_fds[0] = -1; } - if (sock_fds[1] >= 0) { close(sock_fds[1]); sock_fds[1] = -1; } + if (sock_fds[0] >= 0) { + close(sock_fds[0]); + sock_fds[0] = -1; + } + if (sock_fds[1] >= 0) { + close(sock_fds[1]); + sock_fds[1] = -1; + } uv_loop_close(&loop); } }; @@ -377,7 +400,8 @@ TEST_F(WsCloseAllWithConnTest, CloseAll_WithNonNullWsConn) { EXPECT_EQ(server->ws_connection_manager->connection_count, 1); // close_all with a matching path - exercises line 1512 - // uvhttp_ws_close(NULL, ws_conn, 1000, "Server closed connection") is called + // uvhttp_ws_close(NULL, ws_conn, 1000, "Server closed connection") is + // called err = uvhttp_server_ws_close_all(server, "/chat"); EXPECT_EQ(err, UVHTTP_OK); EXPECT_EQ(server->ws_connection_manager->connection_count, 0); @@ -422,14 +446,15 @@ TEST_F(WsCloseAllWithConnTest, CloseAll_NullPathClosesAll_WithWsConn) { delete ws1; delete ws2; - close(fds2[0]); close(fds2[1]); + close(fds2[0]); + close(fds2[1]); } // ============================================================================ // ws_timeout_timer_callback (lines 1174-1208) // ============================================================================ class WsTimeoutTimerTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; @@ -465,7 +490,8 @@ TEST_F(WsTimeoutTimerTest, TimeoutTimer_ClosesTimedOutConnection) { // the pointer (never frees it), so heap-allocate the connection. uvhttp_ws_connection_t* ws_conn = new uvhttp_ws_connection_t{}; ws_conn->state = UVHTTP_WS_STATE_OPEN; - ws_conn->fd = -1; // Will fail send, but timeout just needs to detect staleness + ws_conn->fd = + -1; // Will fail send, but timeout just needs to detect staleness ws_conn->is_server = 1; uvhttp_server_ws_add_connection(server, ws_conn, "/ws"); @@ -502,8 +528,9 @@ TEST_F(WsTimeoutTimerTest, TimeoutTimer_KeepsActiveConnection) { ws_connection_manager_t* mgr = server->ws_connection_manager; ASSERT_NE(mgr, nullptr); - // Add a connection with a recent last_activity (will be set by add_connection). - // The manager borrows the pointer (never frees it), so heap-allocate. + // Add a connection with a recent last_activity (will be set by + // add_connection). The manager borrows the pointer (never frees it), so + // heap-allocate. uvhttp_ws_connection_t* ws_conn = new uvhttp_ws_connection_t{}; ws_conn->state = UVHTTP_WS_STATE_OPEN; ws_conn->fd = -1; @@ -545,9 +572,15 @@ TEST_F(WsTimeoutTimerTest, TimeoutTimer_MultipleConnections) { uvhttp_ws_connection_t* ws1 = new uvhttp_ws_connection_t{}; uvhttp_ws_connection_t* ws2 = new uvhttp_ws_connection_t{}; uvhttp_ws_connection_t* ws3 = new uvhttp_ws_connection_t{}; - ws1->state = UVHTTP_WS_STATE_OPEN; ws1->fd = -1; ws1->is_server = 1; - ws2->state = UVHTTP_WS_STATE_OPEN; ws2->fd = -1; ws2->is_server = 1; - ws3->state = UVHTTP_WS_STATE_OPEN; ws3->fd = -1; ws3->is_server = 1; + ws1->state = UVHTTP_WS_STATE_OPEN; + ws1->fd = -1; + ws1->is_server = 1; + ws2->state = UVHTTP_WS_STATE_OPEN; + ws2->fd = -1; + ws2->is_server = 1; + ws3->state = UVHTTP_WS_STATE_OPEN; + ws3->fd = -1; + ws3->is_server = 1; uvhttp_server_ws_add_connection(server, ws1, "/ws"); uvhttp_server_ws_add_connection(server, ws2, "/ws"); @@ -590,7 +623,7 @@ TEST_F(WsTimeoutTimerTest, TimeoutTimer_MultipleConnections) { // ws_heartbeat_timer_callback (lines 1221-1255) // ============================================================================ class WsHeartbeatTimerTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; int sock_fds[2]{-1, -1}; @@ -611,8 +644,14 @@ class WsHeartbeatTimerTest : public ::testing::Test { uvhttp_server_free(server); server = nullptr; } - if (sock_fds[0] >= 0) { close(sock_fds[0]); sock_fds[0] = -1; } - if (sock_fds[1] >= 0) { close(sock_fds[1]); sock_fds[1] = -1; } + if (sock_fds[0] >= 0) { + close(sock_fds[0]); + sock_fds[0] = -1; + } + if (sock_fds[1] >= 0) { + close(sock_fds[1]); + sock_fds[1] = -1; + } uv_loop_close(&loop); } }; @@ -652,7 +691,8 @@ TEST_F(WsHeartbeatTimerTest, Heartbeat_SendsPingToOpenConnection) { // Drain any data sent to the socketpair char buf[64]; - while (recv(sock_fds[1], buf, sizeof(buf), MSG_DONTWAIT) > 0) {} + while (recv(sock_fds[1], buf, sizeof(buf), MSG_DONTWAIT) > 0) { + } // The connection is still registered. Disable management (drops the // borrowed reference) before freeing it. @@ -693,8 +733,8 @@ TEST_F(WsHeartbeatTimerTest, Heartbeat_PingTimeoutClosesConnection) { // Pump the loop so the heartbeat fires pump_loop(&loop, 200); - // The heartbeat should have detected the ping timeout and closed the connection - // (sets state to CLOSING, but node remains in list) + // The heartbeat should have detected the ping timeout and closed the + // connection (sets state to CLOSING, but node remains in list) EXPECT_EQ(ws_conn->state, UVHTTP_WS_STATE_CLOSING); // The node remains in the list. Disable management (drops the borrowed @@ -744,7 +784,7 @@ TEST_F(WsHeartbeatTimerTest, Heartbeat_SkipsNonOpenConnection) { // uvhttp_server_ws_enable_connection_management success path (lines 1307-1349) // ============================================================================ class WsEnableConnMgmtSuccessTest : public ::testing::Test { -protected: + protected: uv_loop_t loop{}; uvhttp_server_t* server = nullptr; diff --git a/test/unit/test_stress.cpp b/test/unit/test_stress.cpp index 205ef00f..34b78be3 100644 --- a/test/unit/test_stress.cpp +++ b/test/unit/test_stress.cpp @@ -5,18 +5,20 @@ * 压力测试用于验证系统在高负载下的稳定性和性能 */ +#include "uvhttp_allocator.h" +#include "uvhttp_config.h" +#include "uvhttp_response.h" +#include "uvhttp_router.h" +#include "uvhttp_server.h" + +#include "uvhttp.h" + +#include +#include #include -#include #include +#include #include -#include -#include -#include "uvhttp.h" -#include "uvhttp_server.h" -#include "uvhttp_router.h" -#include "uvhttp_response.h" -#include "uvhttp_config.h" -#include "uvhttp_allocator.h" // 压力测试配置 #define STRESS_TEST_HOST "127.0.0.1" @@ -59,12 +61,15 @@ static void setup_stress_server() { // 创建独立的循环,避免与默认循环冲突 g_stress_loop = (uv_loop_t*)uvhttp_alloc(sizeof(uv_loop_t)); ASSERT_NE(g_stress_loop, nullptr); - + int loop_init_result = uv_loop_init(g_stress_loop); - ASSERT_EQ(loop_init_result, 0) << "uv_loop_init failed: " << loop_init_result; + ASSERT_EQ(loop_init_result, 0) + << "uv_loop_init failed: " << loop_init_result; - uvhttp_error_t server_result = uvhttp_server_new(g_stress_loop, &g_stress_server); - ASSERT_EQ(server_result, UVHTTP_OK) << "uvhttp_server_new failed: " << server_result; + uvhttp_error_t server_result = + uvhttp_server_new(g_stress_loop, &g_stress_server); + ASSERT_EQ(server_result, UVHTTP_OK) + << "uvhttp_server_new failed: " << server_result; ASSERT_NE(g_stress_server, nullptr); uvhttp_router_t* router = NULL; @@ -76,10 +81,13 @@ static void setup_stress_server() { uvhttp_router_add_route(router, "/", simple_handler); result = uvhttp_server_take_router(g_stress_server, router); - ASSERT_EQ(result, UVHTTP_OK) << "uvhttp_server_take_router failed: " << result; + ASSERT_EQ(result, UVHTTP_OK) + << "uvhttp_server_take_router failed: " << result; - result = uvhttp_server_listen(g_stress_server, STRESS_TEST_HOST, g_stress_test_port); - ASSERT_EQ(result, UVHTTP_OK) << "uvhttp_server_listen failed: " << result << " (port: " << g_stress_test_port << ")"; + result = uvhttp_server_listen(g_stress_server, STRESS_TEST_HOST, + g_stress_test_port); + ASSERT_EQ(result, UVHTTP_OK) << "uvhttp_server_listen failed: " << result + << " (port: " << g_stress_test_port << ")"; } /**