@@ -65,9 +65,10 @@ static int test_register_replaces_duplicate()
6565{
6666 std::cout << " test_register_replaces_duplicate..." << std::endl;
6767 FastMCP app (" a" , " 1.0.0" );
68- app.add_custom_route (make_route (" GET " , " /x" , " first" ));
68+ app.add_custom_route (make_route (" get " , " /x" , " first" ));
6969 app.add_custom_route (make_route (" GET" , " /x" , " second" ));
7070 ASSERT_EQ (app.custom_routes ().size (), 1u , " still one route" );
71+ ASSERT_EQ (app.custom_routes ().front ().method , std::string (" GET" ), " method normalized to uppercase" );
7172 auto resp = app.custom_routes ().front ().handler ({" GET" , " /x" , " " , {}});
7273 ASSERT_EQ (resp.body , std::string (" second" ), " second handler wins" );
7374 std::cout << " PASS" << std::endl;
@@ -89,6 +90,28 @@ static int test_validation_rejects_bad_inputs()
8990 }
9091 ASSERT_TRUE (threw, " missing leading slash rejected" );
9192
93+ threw = false ;
94+ try
95+ {
96+ app.add_custom_route (make_route (" " , " /x" , " x" ));
97+ }
98+ catch (const fastmcpp::ValidationError&)
99+ {
100+ threw = true ;
101+ }
102+ ASSERT_TRUE (threw, " missing method rejected" );
103+
104+ threw = false ;
105+ try
106+ {
107+ app.add_custom_route (make_route (" HEAD" , " /x" , " x" ));
108+ }
109+ catch (const fastmcpp::ValidationError&)
110+ {
111+ threw = true ;
112+ }
113+ ASSERT_TRUE (threw, " unsupported method rejected" );
114+
92115 threw = false ;
93116 CustomRoute no_handler;
94117 no_handler.method = " GET" ;
@@ -106,6 +129,28 @@ static int test_validation_rejects_bad_inputs()
106129 return 0 ;
107130}
108131
132+ static int test_http_wrapper_rejects_unsupported_custom_route_method ()
133+ {
134+ std::cout << " test_http_wrapper_rejects_unsupported_custom_route_method..." << std::endl;
135+
136+ auto core = std::make_shared<server::Server>();
137+ server::HttpServerWrapper http (core, " 127.0.0.1" , 0 );
138+
139+ bool threw = false ;
140+ try
141+ {
142+ http.set_custom_routes ({make_route (" HEAD" , " /health" , " ok" )});
143+ }
144+ catch (const fastmcpp::ValidationError&)
145+ {
146+ threw = true ;
147+ }
148+
149+ ASSERT_TRUE (threw, " direct wrapper route registration rejects unsupported methods" );
150+ std::cout << " PASS" << std::endl;
151+ return 0 ;
152+ }
153+
109154static int test_aggregate_from_mounted_child ()
110155{
111156 std::cout << " test_aggregate_from_mounted_child..." << std::endl;
@@ -198,6 +243,93 @@ static int test_http_end_to_end_serves_route()
198243 return 0 ;
199244}
200245
246+ static int test_http_custom_route_preserves_query_params ()
247+ {
248+ std::cout << " test_http_custom_route_preserves_query_params..." << std::endl;
249+
250+ CustomRouteRequest captured;
251+ bool called = false ;
252+
253+ FastMCP child (" child" , " 1.0.0" );
254+ CustomRoute query_route;
255+ query_route.method = " GET" ;
256+ query_route.path = " /search" ;
257+ query_route.handler = [&](const CustomRouteRequest& req)
258+ {
259+ called = true ;
260+ captured = req;
261+
262+ CustomRouteResponse resp;
263+ resp.body = " query ok" ;
264+ resp.content_type = " text/plain" ;
265+ return resp;
266+ };
267+ child.add_custom_route (std::move (query_route));
268+
269+ FastMCP parent (" parent" , " 1.0.0" );
270+ parent.mount (child, " kids" );
271+
272+ auto core = std::make_shared<server::Server>(parent.server ());
273+
274+ int port = 0 ;
275+ std::unique_ptr<server::HttpServerWrapper> http;
276+ for (int candidate = 18481 ; candidate <= 18500 ; ++candidate)
277+ {
278+ auto trial = std::make_unique<server::HttpServerWrapper>(core, " 127.0.0.1" , candidate);
279+ trial->set_custom_routes (parent.all_custom_routes ());
280+ if (trial->start ())
281+ {
282+ port = trial->port ();
283+ http = std::move (trial);
284+ break ;
285+ }
286+ }
287+ ASSERT_TRUE (http && port > 0 , " HTTP server started" );
288+
289+ std::this_thread::sleep_for (std::chrono::milliseconds (150 ));
290+
291+ httplib::Client client (" 127.0.0.1" , port);
292+ client.set_connection_timeout (std::chrono::seconds (2 ));
293+ client.set_read_timeout (std::chrono::seconds (2 ));
294+
295+ auto resp = client.Get (" /kids/search?q=a&q=b&lang=en" );
296+ ASSERT_TRUE (resp != nullptr , " GET with query params returned a response" );
297+ ASSERT_EQ (resp->status , 200 , " query route served" );
298+ ASSERT_EQ (resp->body , std::string (" query ok" ), " query route body" );
299+
300+ ASSERT_TRUE (called, " handler was invoked" );
301+ ASSERT_EQ (captured.method , std::string (" GET" ), " request method preserved" );
302+ ASSERT_EQ (captured.path , std::string (" /kids/search" ), " path preserved without query string" );
303+ ASSERT_EQ (captured.target , std::string (" /kids/search?q=a&q=b&lang=en" ),
304+ " raw target preserves query string" );
305+ ASSERT_EQ (captured.query_params .count (" q" ), 2u , " repeated query param preserved" );
306+ ASSERT_EQ (captured.query_params .count (" lang" ), 1u , " single query param preserved" );
307+
308+ auto q_range = captured.query_params .equal_range (" q" );
309+ bool seen_q_a = false ;
310+ bool seen_q_b = false ;
311+ size_t q_values = 0 ;
312+ for (auto it = q_range.first ; it != q_range.second ; ++it)
313+ {
314+ ++q_values;
315+ if (it->second == " a" )
316+ seen_q_a = true ;
317+ if (it->second == " b" )
318+ seen_q_b = true ;
319+ }
320+ ASSERT_EQ (q_values, 2u , " two q values captured" );
321+ ASSERT_TRUE (seen_q_a, " q=a preserved" );
322+ ASSERT_TRUE (seen_q_b, " q=b preserved" );
323+
324+ auto lang_it = captured.query_params .find (" lang" );
325+ ASSERT_TRUE (lang_it != captured.query_params .end (), " lang key present" );
326+ ASSERT_EQ (lang_it->second , std::string (" en" ), " lang value preserved" );
327+
328+ http->stop ();
329+ std::cout << " PASS" << std::endl;
330+ return 0 ;
331+ }
332+
201333static int test_http_custom_route_requires_auth ()
202334{
203335 std::cout << " test_http_custom_route_requires_auth..." << std::endl;
@@ -296,9 +428,11 @@ int main()
296428 failures += test_register_basic ();
297429 failures += test_register_replaces_duplicate ();
298430 failures += test_validation_rejects_bad_inputs ();
431+ failures += test_http_wrapper_rejects_unsupported_custom_route_method ();
299432 failures += test_aggregate_from_mounted_child ();
300433 failures += test_aggregate_dedups_collisions ();
301434 failures += test_http_end_to_end_serves_route ();
435+ failures += test_http_custom_route_preserves_query_params ();
302436 failures += test_http_custom_route_requires_auth ();
303437 failures += test_http_custom_route_options_advertises_methods ();
304438 std::cout << std::endl;
0 commit comments