diff --git a/include/uvhttp_server.h b/include/uvhttp_server.h index 323ed980..3fe8428e 100644 --- a/include/uvhttp_server.h +++ b/include/uvhttp_server.h @@ -262,6 +262,13 @@ uvhttp_error_t uvhttp_server_stop(uvhttp_server_t* server); * @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. * + * @note This entry builds a bare server: no `uvhttp_context_t` and no + * `uvhttp_config_t` are attached. Every config-backed setting on the + * result therefore falls back to its compile-time constant (backlog, + * tcp_keepalive_timeout). If you need those tunable, use the five-step + * sequence with a context instead - wiring a config into this path is + * tracked separately. + * * @example * uvhttp_server_t* server = NULL; * uvhttp_route_t routes[] = { @@ -331,6 +338,12 @@ uvhttp_error_t uvhttp_server_set_handler(uvhttp_server_t* server, * 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. + * + * @note The guard above is per-server, so it does NOT stop the same router + * from being handed to two different servers. Do that and both + * uvhttp_server_free calls release it: the second one is a double free + * (ASan reports it at free time, far from the assignment). One router + * belongs to exactly one server. */ uvhttp_error_t uvhttp_server_take_router(uvhttp_server_t* server, uvhttp_router_t* router); @@ -348,6 +361,10 @@ uvhttp_error_t uvhttp_server_take_router(uvhttp_server_t* server, * 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. + * + * @note Like take_router, the guard is per-server: handing the same context + * to two servers makes both destroy it - a double free at free time. + * One context belongs to exactly one server. */ uvhttp_error_t uvhttp_server_take_context(uvhttp_server_t* server, struct uvhttp_context* context); diff --git a/src/uvhttp_lru_cache.c b/src/uvhttp_lru_cache.c index 34f0d027..4ffefb66 100644 --- a/src/uvhttp_lru_cache.c +++ b/src/uvhttp_lru_cache.c @@ -677,12 +677,26 @@ void uvhttp_lru_cache_reset_stats(cache_manager_t* cache) { /** * set maximum file size for cache + * + * The write point is the ONLY guard for this field: uvhttp_lru_cache_create + * takes no max_file_size argument (it hardcodes a 1GB default), and put() + * applies it as a hard cutoff (content_length > max_file_size, see the + * "check if file size exceeds limit" branch). A zero written here therefore + * bricks the cache - every later put returns UVHTTP_ERROR_INVALID_PARAM with + * only a WARN as explanation - so it is rejected like the two setters above + * instead of being written silently. */ void uvhttp_lru_cache_set_max_file_size(cache_manager_t* cache, size_t max_file_size) { if (!cache) return; + if (max_file_size == 0) { + UVHTTP_LOG_WARN( + "Ignoring max_file_size=0 (put() would reject every entry)"); + return; + } + cache->max_file_size = max_file_size; } diff --git a/src/uvhttp_router.c b/src/uvhttp_router.c index 374997df..d19837ea 100644 --- a/src/uvhttp_router.c +++ b/src/uvhttp_router.c @@ -352,8 +352,15 @@ static uvhttp_error_t migrate_to_trie(uvhttp_router_t* router) { * already had; only the route that triggered migration fails. * * The trie nodes built so far stay in the pool unreferenced - * (use_trie is 0, so no lookup reaches them) and are reused by - * the next attempt, which walks the array again from the root. + * (use_trie is 0, so no lookup reaches them); they are neither + * freed nor reachable, so the pool grows monotonically until a + * migration succeeds or the router is freed. A retry re-walks + * the array from the root and find_or_create_child matches by + * segment name, so it reuses the nodes already there (traced + * by reading, not by a test - the failure here is the 12-child + * cap, which no route removal can relieve, so the + * "fail then succeed" transition has no test). + * * Freeing or detaching old_routes here would destroy every * previously registered route. */ return UVHTTP_ERROR_OUT_OF_MEMORY; diff --git a/test/unit/test_lru_cache_capacity.cpp b/test/unit/test_lru_cache_capacity.cpp index e3c70a0a..c837edb0 100644 --- a/test/unit/test_lru_cache_capacity.cpp +++ b/test/unit/test_lru_cache_capacity.cpp @@ -196,4 +196,50 @@ TEST_F(LruCacheCapacity, SetMaxEntriesAcceptsPositiveValue) { EXPECT_EQ(cache_->max_entries, 3); } +/* ========== set_max_file_size:写点不变量 ========== */ + +/* + * put() 在「check if file size exceeds limit」分支用 + * if (content_length > cache->max_file_size) → INVALID_PARAM + * 对内容长度做硬截断。而 create() 根本不接收 max_file_size(默认 1GB 写死在 + * lru_cache.c:82),所以 setter 是这个字段唯一的守卫。 + * + * 评审探针实证:set_max_file_size(cache, 0) 之后,每次 put 都以 -1 失败 —— + * 缓存被砖化,调用方只看到一串 "File size exceeds limit" 的 WARN。 + * 本次补上与另两个 setter 同范式的下限校验。 + */ + +TEST_F(LruCacheCapacity, SetMaxFileSizeRejectsZero) { + Create(kBudget); + + uvhttp_lru_cache_set_max_file_size(cache_, 0); + + EXPECT_EQ(cache_->max_file_size, 1024u * 1024 * 1024) + << "create 把默认值写死为 1GB;setter 不得把 0 写进去"; +} + +/* 核心回归:0 不得砖化缓存。砖化的表现是 put 永久 INVALID_PARAM。 */ +TEST_F(LruCacheCapacity, ZeroMaxFileSizeDoesNotBrickTheCache) { + Create(kBudget); + + uvhttp_lru_cache_set_max_file_size(cache_, 0); + + EXPECT_EQ(Put("/after.bin", 64, 's'), UVHTTP_OK) + << "写入 0 之后 put 必须仍能成功(评审探针在此处实测返回 -1)"; + EXPECT_EQ(cache_->entry_count, 1); +} + +/* 合法值仍然立即生效:小于新上限的进得去,大于的被拒 */ +TEST_F(LruCacheCapacity, SetMaxFileSizeAcceptsPositiveValue) { + Create(kBudget); + + uvhttp_lru_cache_set_max_file_size(cache_, 1024); + EXPECT_EQ(cache_->max_file_size, 1024u); + + EXPECT_EQ(Put("/small.bin", 512, 's'), UVHTTP_OK) + << "小于新上限的内容应能放入"; + EXPECT_EQ(Put("/big.bin", 4096, 'b'), UVHTTP_ERROR_INVALID_PARAM) + << "大于新上限的内容应被拒(文件大小判定先于内存预算判定)"; +} + #endif /* UVHTTP_FEATURE_STATIC_FILES */