Skip to content

Unify streaming/WebSocket routes into http_router with path params#2559

Merged
stephenberry merged 2 commits into
mainfrom
feat/router-streaming-websocket-routes
May 10, 2026
Merged

Unify streaming/WebSocket routes into http_router with path params#2559
stephenberry merged 2 commits into
mainfrom
feat/router-streaming-websocket-routes

Conversation

@stephenberry
Copy link
Copy Markdown
Owner

Summary

Addresses the design feedback in #2550 (from @JnCrMx). Moves streaming and WebSocket route storage off http_server's flat path → handler maps and into basic_http_router, where they share the radix-tree matcher with normal routes. As a result, streaming and WebSocket routes now support :param and *wildcard path segments, and routers carrying any mix of route kinds can be passed to server.mount().

The requested API now works:

glz::http_router router;
router.get("/users/:id/profile", normal_handler);
router.stream_get("/users/:id/events", streaming_handler);
router.websocket("/rooms/:room/ws", ws_server);

glz::http_server server;
server.mount("/api", router);

What changed

  • Extracted route_table<H> from basic_http_router; the router now holds three of them (normal_routes, streaming_routes, websocket_routes) sharing one radix-tree implementation.
  • New router methods: stream, stream_get, stream_post, websocket, match_streaming, match_websocket.
  • http_server::stream* / http_server::websocket delegate to root_router. The flat streaming_handlers_ and websocket_handlers_ maps are removed.
  • mount() propagates all three route kinds; each is rewritten with the mount prefix and added to the server's root router.
  • Hot-path fast-path: process_full_request_with_conn skips the streaming match when no streaming routes are registered, restoring per-request cost for the common case.
  • Bug fixes picked up during the refactor:
    • route_table::match_node now erases the wildcard capture on every failure path (was leaking on is_endpoint=false, wrong method, or constraint failure — parity with the existing :param branch).
    • route_table::add installs in the radix tree first, then populates the routes map, so a route conflict thrown from add_route no longer leaves a half-registered entry that iteration sees but match() never reaches.

Behavior change

Previously streaming_handlers_ was an exact-path map, so stream_get(\"/items/:id\", ...) was effectively dead and a request to /items/42 fell through to the normal router. After the unification, streaming routes are matched before normal routes, so a streaming /items/:id will intercept requests that a normal /items/42 would otherwise handle. Documented in docs/networking/http-router.md (Streaming Routes section + Route Priority section).

Migration

router.routes (public field) is now router.normal_routes.routes. A routes() accessor method is provided for source compatibility (a reference data member would dangle through the implicit move constructor). Updated callers: http_server::mount, http_server::enable_openapi_spec, tests/networking_tests/openapi_test, and docs/networking/rest-registry.md.

Test plan

  • Streaming path-param coverage in tests/networking_tests/http_chunked_test/http_chunked_test.cpp: :param direct on server, :param via http_router + mount(), mixing normal + streaming on one router, wildcard segment, :param + query string combo, URL-decoded :param, route_spec::constraints rejection (200 vs 404).
  • WebSocket path-param coverage in tests/networking_tests/websocket_test/websocket_client_test.cpp: :param direct on server, :param via router + mount, wildcard segment, all-three-kinds (normal + streaming + WebSocket) coexisting on one mounted router.
  • Replaced bare sleep_for(200ms) server-readiness waits in the new streaming tests with a TCP-connect-probe helper for robustness on busy CI.
  • Suites pass:
    • http_chunked_test: 57/57 tests, 230 asserts.
    • websocket_client_test: 34/34 tests, 146 asserts.
    • http_router_test: 21/21 tests, 81 asserts.
    • http_server_api_tests: 49/49 tests, 152 asserts.
    • openapi_test: 1/1 tests, 5 asserts.
  • clang-format pass on touched headers and test files.

Docs updated

  • docs/networking/http-router.md — new "Streaming Routes" and "WebSocket Routes" sections; "Route Priority" extended to cover cross-kind dispatch order.
  • docs/networking/http-server.mdmount() propagation note in "Mounting Sub-routers".
  • docs/networking/advanced-networking.mdrouter.websocket(...) + path-param note in the WebSocket section.
  • docs/networking/rest-registry.md — field rename in the endpoint-discovery example.

Move streaming and WebSocket route storage from flat path -> handler
maps on http_server into basic_http_router, sharing the radix-tree
matcher with normal routes. Streaming and WebSocket routes now support
:param and *wildcard segments, and routers carrying any mix of route
kinds can be passed to server.mount().

- Extract route_table<H> from basic_http_router; the router holds three
  (normal/streaming/websocket) sharing the radix tree.
- Add stream/stream_get/stream_post/websocket on the router, plus
  match_streaming/match_websocket lookup methods.
- http_server::stream*/websocket delegate to root_router; the flat
  streaming_handlers_/websocket_handlers_ maps are removed.
- mount() propagates all three kinds.
- Fast-path: skip streaming match in process_full_request_with_conn
  when no streaming routes are registered, avoiding per-request
  allocations for the common case.
- Fix wildcard params leak: match_node now erases the wildcard capture
  on every failure path (parity with the :param branch).
- Fix half-register on throw: route_table::add now installs in the
  radix tree first, then populates the routes map.
- Migration: router.routes (field) is now router.normal_routes.routes;
  routes() accessor method provided for source compatibility.

Behavior change: streaming routes are now matched before normal routes,
so a streaming /items/:id will intercept what a normal /items/42 would
have handled. Previously streaming was an exact-path map and :param
streaming routes were effectively dead.
… redundant tree header

Address PR review nits:
- route_table::split_path is documented as the canonical implementation;
  basic_http_router::split_path delegates to it.
- route_table::print_tree no longer prints "Radix Tree Structure:" since
  basic_http_router::print_tree already groups three sub-trees under labelled
  [normal routes]/[streaming routes]/[websocket routes] sections.
@stephenberry stephenberry merged commit db9f461 into main May 10, 2026
51 checks passed
@stephenberry stephenberry deleted the feat/router-streaming-websocket-routes branch May 10, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant