TELCORE-166: add libfreeswitch hooks for mod_web_server - #599
Conversation
Adds the core-side pieces that mod_web_server needs to expose a stable C ABI to other FreeSWITCH modules. The module itself ships from its own repository (team-telnyx/mod_web_server) and is dropped into src/mod/applications/mod_web_server/ at deploy time, not committed into the freeswitch tree — same pattern as mod_telnyx and mod_dynamic_gateway. Why these pieces live in libfreeswitch -------------------------------------- APR loads modules with RTLD_LOCAL on Unix (libs/apr/dso/unix/dso.c). Symbols a module exports are not visible to any other loaded module. So for arbitrary modules to call switch_web_server_register(...), the registry, the public ABI, and the request/response opaque types have to sit in libfreeswitch — only the Beast acceptor, sessions, threads, and config loader stay inside the module itself. What this commit adds --------------------- src/include/switch_web_server.h public C ABI src/include/switch_web_server_internal.h C++ bridge to the module src/switch_web_server.cpp registry + ABI implementations Makefile.am add switch_web_server.cpp to libfreeswitch_la_SOURCES tests/unit_cpp/test_web_server_registry.cpp 43-assertion standalone test tests/unit_cpp/Makefile.am wire the test (always built; not gated on HAVE_CARES) Public ABI surface ------------------ switch_web_server_register(module, method, path, mode, fn, ud) switch_web_server_register_prefix(module, method, prefix, mode, fn, ud) switch_web_server_unregister(module, method, path) switch_web_server_unregister_module(module) switch_web_server_available() switch_web_request_method/path/query/header/body/remote_ip/param(req) switch_web_response_set_status/header/body/printf(res) Drain semantics --------------- Each registered route is tagged with its owning module name and a shared ModuleSlot (atomic in_flight + condvar). On lookup, the result carries a move-only InFlightTicket whose destructor decrements the in_flight count; while a handler is running the ticket lives in the dispatch lambda and only drops when the handler returns. The matching switch_web_server_unregister_module() removes routes from the registry and then blocks on the slot's cv until in_flight hits zero, so a module's .so cannot be invoked after teardown. Numeric input is parsed with strtoll + errno + end-pointer checks inside the registry (none of the public setters are exposed to operator input here, but the same discipline is applied in the module's config loader for consistency). Tests ----- tests/unit_cpp/test_web_server_registry.cpp is a standalone C++ program (no FST, no live core) that links against libfreeswitch and covers exact / pattern / prefix matching, 404 / 405 collection, ANY-method wildcards, conflict + idempotent re-register, in-place handler update on re-register by the same module, sweep-by-module isolation, snapshot, and single-route unregister. 43 assertions.
minhtuan1407-telnyx
left a comment
There was a problem hiding this comment.
[Hermes War Room — confidence: HIGH]
Found blockers.
- Core build portability is not guaranteed.
src/switch_web_server.cpp is added to libfreeswitch_la_SOURCES, but it uses C++17-only APIs:
src/switch_web_server.cpp:20includes<shared_mutex>src/switch_web_server.cpp:148,190,208,245,298,352,367,385usestd::shared_mutex/std::shared_lock
The only explicit C++17 flag added here is for the new unit test target:
tests/unit_cpp/Makefile.am:5setsAM_CXXFLAGS = -std=c++14tests/unit_cpp/Makefile.am:12setstest_web_server_registry_CXXFLAGS = $(AM_CXXFLAGS) -std=c++17
That does not apply to the core libfreeswitch build. I also checked configure.ac; SWITCH_AM_CXXFLAGS is initialized without a C++17 standard flag and only gets platform/PIC/visibility additions. On this host, c++ defaults to __cplusplus 201402L, and a minimal std::shared_mutex probe fails under -std=c++14.
Please either make the core library target explicitly compile this file as C++17, or avoid std::shared_mutex/std::shared_lock in the libfreeswitch code path.
SWITCH_WEB_METHOD_ANYconflicts are not enforced, so route ownership is ambiguous.
Lookup treats ANY as a wildcard:
src/switch_web_server.cpp:55-58
But registration conflict checks only compare exact method equality:
- pattern routes:
src/switch_web_server.cpp:151-157 - exact routes:
src/switch_web_server.cpp:167-175 - prefix routes:
src/switch_web_server.cpp:193-200
That allows overlapping routes like:
- module A registers
ANY /foo - module B registers
GET /foo
Both registrations can succeed, but lookup returns the first matching entry, so dispatch depends on insertion order. This can make a successful registration unreachable or let one module shadow another module’s handler. For a shared process-wide ABI, that is a blocker.
Please make wildcard/specific overlap explicit and safe, e.g. conflict when existing.method == method || existing.method == SWITCH_WEB_METHOD_ANY || method == SWITCH_WEB_METHOD_ANY, while preserving same-module re-register/update semantics. Add tests for exact, pattern, and prefix wildcard conflicts in both insertion orders.
Evidence checked:
- PR head
7e99fbc3877f391273a8de4a2b8f7e6ebade8f82 - local checkout
/Users/tuan/.hermes/agent-workspaces/ops/freeswitch-pr599-review - public/internal ABI, registry registration/lookup/unregister paths, Makefile/configure CXX flag wiring, and unit test coverage
— 🪽 Hermes War Room on behalf of @minhtuan1407-telnyx
…egment match
Two blockers from the PR review on libfreeswitch hooks:
1. C++17 in core. switch_web_server.cpp uses std::shared_mutex /
std::shared_lock, but no C++ standard flag flowed into the
libfreeswitch build path. Add AM_CXXFLAGS = -std=c++17 to the
top-level Makefile.am so libfreeswitch_la's C++ sources get the
flag. Drop the now-redundant -std=c++17 in tests/unit_cpp.
2. SWITCH_WEB_METHOD_ANY overlap was not enforced at registration,
so e.g. ANY /foo and GET /foo from different modules could both
succeed and lookup-order silently picked the winner. Add a
methods_overlap() helper and wire it into all three registration
tiers (exact, pattern, prefix) with a carveout that preserves the
same-module identical-route in-place update. Six new tests cover
both insertion orders for every tier, ANY-vs-ANY cross-module,
and the same-module ANY update.
Plus follow-on hardening surfaced during review:
- Public header documents return codes (SUCCESS / FALSE / GENERR)
including the new ANY-overlap conflict semantics; previously the
doc said "FALSE on conflict (different module)" only.
- Invalid args (empty module / bad path / null handler) now return
SWITCH_STATUS_GENERR; route conflict stays SWITCH_STATUS_FALSE.
Callers can distinguish "bad input" from "shadowed route".
- remove_module()'s drain loop now uses the predicate-variant
cv.wait_for so a notify fired between in_flight.load() and the
wait is not lost (previously could stall the full 5s for nothing).
- Prefix matching is segment-bounded: prefix "/api" matches "/api"
and "/api/v2/foo" but NOT "/apiv2"; prefix "/api/" matches "/api/"
and below. Raw substring matching had silently shadowed unrelated
routes. New test_prefix_segment_boundary covers both shapes.
- test_same_module_any_reregister_updates now loops over GET / POST
/ PUT / DELETE / PATCH instead of only GET — the whole point of
ANY is that it matches every verb.
minhtuan1407-telnyx
left a comment
There was a problem hiding this comment.
[Hermes War Room — confidence: HIGH]
Found blockers at head b1b734e2a5be93a709ee9f1c1be2a367e547f909.
- Same-tier route ambiguity is still accepted, so dispatch can depend on registration/module load order.
Current code rejects only raw-identical route conflicts:
- pattern registration compares
p.raw == path - prefix registration compares
existing.raw == prefix - lookup scans
patterns_andprefixes_in insertion order
That leaves ambiguous same-tier routes registerable:
/users/{id}vs/users/{name}have the same match set because parameter names do not affect matching./apivs/api/v2both match/api/v2/foo; current prefix lookup returns whichever matching prefix was inserted first.ANY/specific overlap is handled for identical raw strings, but not for semantic equivalents/overlaps.
For a process-wide ABI shared by independently loaded modules, successful registration followed by insertion-order shadowing is not acceptable. Pick and implement one explicit contract:
- reject semantically equivalent pattern shapes and overlapping prefixes when methods overlap, including
SWITCH_WEB_METHOD_ANY, in both insertion orders; or - implement deterministic lookup, e.g. exact > normalized/most-specific pattern > longest segment-bounded prefix, and document/test it.
Required tests should cover cross-module and same-module cases, both insertion orders, and ANY vs specific method overlap for:
/users/{id}vs/users/{name}/apibefore/api/v2/api/v2before/api- exact/pattern/prefix cross-tier precedence if coexistence is intended
switch_web_response_printf()has a source-level buffer contract bug.
The implementation sizes the string to needed, then calls:
std::vsnprintf(buf.data(), buf.size() + 1, fmt, ap2)
In C++17, std::string::data() gives writable storage for the string’s current size. Passing size() + 1 asks vsnprintf to write the trailing NUL past the string’s size contract. A local ASAN/UBSAN probe did not catch this, but “the sanitizer missed it” is not a safety proof; it is just the darkness wearing a lab coat.
Fix shape:
- allocate writable storage for
needed + 1, callvsnprintfwith that capacity, then resize/assign body toneeded; or - use a
std::vector<char>/ equivalent temporary buffer sizedneeded + 1.
Add a focused test/probe for long formatted output.
- Supported build/test evidence is still missing.
Source now wires:
Makefile.am:24withAM_CXXFLAGS = -std=c++17src/switch_web_server.cppintolibfreeswitch_la_SOURCEStests/unit_cpp/Makefile.amwith C++17
But there is no passing supported-toolchain CI/build readback proving:
- autotools regeneration/configure succeeds
- libfreeswitch compiles and links with the new C++17 source
- existing C++ objects do not regress
tests/unit_cpp/test_web_server_registryruns under the target FreeSWITCH/B2BUA environment
Available CI evidence is not sufficient: the PR Review / review check failed because the internal review action could not resolve, and the telnyx_b2bua_builder / trigger check had no successful build/test evidence in the provided closure. Local probes used Apple clang 17 and stubs; useful, but not a deploy-development build matrix.
Please fix the route ambiguity and response buffer issue, then provide supported CI/build logs for the libfreeswitch C++17 build and test_web_server_registry.
— 🪽 Hermes War Room on behalf of @minhtuan1407-telnyx
Two more blockers from the PR review:
1. Same-tier route ambiguity was still registerable, so dispatch could
depend on registration/module-load order. The previous round only
rejected raw-identical conflicts, leaving semantically-equivalent
routes through:
- "/users/{id}" vs "/users/{name}" — identical match-set, param
names do not affect matching;
- "/api" vs "/api/v2" — overlapping prefixes, lookup returned
whichever was inserted first.
Replace raw-equality conflict checks with match-set overlap:
- patterns_overlap(): same segment count and, per position, either
side is a {capture} or the literals match — true iff some path
matches both. Catches "/users/{id}" vs "/users/{name}" and also
"/a/{x}" vs "/{y}/b".
- prefixes_overlap(): one segment-bounded-contains the other —
"/api" vs "/api/v2" conflict, "/api" vs "/apiv2" do not.
The checks are symmetric, so the outcome is order-independent. The
same-module identical-route (raw + method) in-place update is
preserved. Cross-tier coexistence stays allowed and is resolved by
the documented exact > pattern > prefix lookup precedence.
Public header documents the full overlap contract. New tests cover
both insertion orders, cross- and same-module, ANY-vs-specific, the
disjoint-prefix coexistence case, and cross-tier precedence.
2. switch_web_response_printf() sized a std::string to `needed` then
let vsnprintf write its NUL into the string's terminator slot via
data()+size()+1 — relying on a subtle library-contract corner.
Format into an owned std::vector<char> of needed+1 and assign
exactly `needed` bytes into body instead. Verified live with a
5000-char formatted body: exact length, no truncation.
Registry unit tests: 113 passed, 0 failed.
minhtuan1407-telnyx
left a comment
There was a problem hiding this comment.
[Hermes War Room — confidence: HIGH]
Blocking this at head 5a74928d859b50705397d912fc21d648f179a281.
The current patch has source-level improvements over the prior review: same-tier route overlap checks appear addressed, SWITCH_WEB_METHOD_ANY overlap is handled within tiers, and switch_web_response_printf() now uses a std::vector<char> sized needed + 1 before assigning exactly needed bytes. Good. Not enough to merge.
Blockers:
- Public ABI contract and implementation/tests still disagree on cross-tier overlaps.
src/include/switch_web_server.h says registration is rejected whenever match-sets intersect under overlapping methods, so dispatch never depends on registration or module-load order.
But the implementation only rejects conflicts within the same tier:
- exact-vs-exact in
Registry::add() - pattern-vs-pattern in
Registry::add() - prefix-vs-prefix in
Registry::add_prefix()
Lookup then applies deterministic precedence:
- exact
- pattern
- prefix
The test suite matches that implementation, not the documented global-reject contract: test_cross_tier_precedence() registers prefix, then pattern, then exact and asserts exact > pattern > prefix.
Pick one ABI and make the code/docs/tests say the same thing:
- either reject every intersecting match-set globally across exact/pattern/prefix tiers; or
- explicitly document that cross-tier shadowing is allowed and intentional with precedence exact > pattern > prefix, then add coverage proving exact-vs-pattern, exact-vs-prefix, and pattern-vs-prefix behavior in both insertion orders.
Right now this is a process-wide registry for independently loaded modules, and “the comment promises no shadowing but the code allows shadowing” is not a contract. It is a future incident wearing a fake mustache.
- Supported CI/build/test evidence is still missing for this exact head.
Live GitHub readback for 5a74928d859b50705397d912fc21d648f179a281 does not show a successful supported build/test result:
- combined commit status:
pending,total_count=0 - check-run
PR Review / review: completed, conclusionfailure- failure annotation: unable to resolve
team-telnyx/reviewpr-internal
- failure annotation: unable to resolve
- check-run
Trigger telnyx_b2bua_builder / trigger: queued, conclusionnull - no visible completed builder logs proving autotools/configure, libfreeswitch C++17 compile/link, or
tests/unit_cpp/test_web_server_registryexecution
The commit message claims registry unit tests passed, but that is author-supplied text, not CI/build evidence.
Please provide passing supported-toolchain evidence tied to this exact SHA showing:
- autotools regeneration/configure succeeds
libfreeswitchcompiles and links withsrc/switch_web_server.cppunder the target C++17 buildtests/unit_cpp/test_web_server_registryruns and passes in the relevant FreeSWITCH/B2BUA/deploy-development environment
- Add focused regression coverage for the long
switch_web_response_printf()path.
The implementation shape now looks correct, but the focused long-output regression remains absent in the evidence reviewed. Add a test/probe that exercises formatted output larger than the small/common buffer path and verifies the response body length/content.
Summary: the patch is closer, but merge is still blocked on ABI consistency for cross-tier route ownership and missing supported CI/build/test proof for the exact head SHA.
— 🪽 Hermes War Room on behalf of @minhtuan1407-telnyx
Blocker 1 (ABI consistency): the header promised a global match-set reject across all tiers, but the registry only rejects WITHIN a tier and resolves cross-tier overlap by the deterministic precedence exact > pattern > prefix. That precedence is structural (by tier), not by registration/load order, which is the property the contract cares about — so document it as intentional rather than weakening the design. Rewrote the switch_web_server_register contract to state: within-tier intersections are rejected; cross-tier overlap is allowed and resolved by tier precedence. Added test_cross_tier_no_conflict_both_orders covering each tier pair (exact/pattern, exact/prefix, pattern/prefix) in BOTH insertion orders, asserting both registrations succeed and lookup resolves to the more-specific tier regardless of order. Blocker 3: added test_response_printf_long_output exercising formatted output of 10000 bytes (past any common small-buffer threshold), verifying exact byte length, content, no trailing NUL, and last-call replacement of a longer body. Registry test suite: 141 checks pass.
|
@minhtuan1407-telnyx — pushed Blocker 1 — ABI / impl / tests now agree on cross-tier overlaps. Took the second option you offered: cross-tier shadowing is documented as intentional with precedence Blocker 3 — long Blocker 2 — CI evidence. I can't resolve this from my side — the |
minhtuan1407-telnyx
left a comment
There was a problem hiding this comment.
[Hermes War Room — confidence: HIGH]
Blocking at head 9c97891c9632624527b5d655f92670348e029240.
The source-level issues from the prior review appear addressed:
- ABI/docs/tests now agree that same-tier overlaps are rejected, while cross-tier overlap is intentionally allowed and resolved by structural precedence: exact > pattern > prefix.
SWITCH_WEB_METHOD_ANYoverlap is covered within tiers.switch_web_response_printf()now has long-output regression coverage.- The registry test suite reportedly covers cross-tier ordering, pattern/prefix conflicts, module unregister sweep, and 10000-byte response formatting.
But the remaining blocker is still real: there is no supported current-head CI/build evidence proving this actually builds and runs in the target FreeSWITCH/B2BUA path.
Live check evidence for 9c97891c9632624527b5d655f92670348e029240:
-
PR Review / review- status: completed
- conclusion: failure
- started: 2026-06-04T12:15:53Z
- completed: 2026-06-04T12:15:56Z
- job: https://github.com/team-telnyx/freeswitch/actions/runs/26951101724/job/79516269947
- failure: unable to resolve
team-telnyx/reviewpr-internal
-
Trigger telnyx_b2bua_builder / trigger- status: queued
- conclusion: null
- started: 2026-06-04T12:15:50Z
- completed: null
- job: https://github.com/team-telnyx/freeswitch/actions/runs/26951101824/job/79516269815
-
Combined commit status:
- state: pending
- total legacy statuses: 0
That means no passing check currently proves:
- autotools/bootstrap/configure succeeds
libfreeswitchcompiles and links withsrc/switch_web_server.cpp- the new C++17 path is accepted by the supported builder/toolchain
tests/unit_cpp/test_web_server_registryexecutes successfully for this SHA
The author’s local real-toolchain result, “141 checks, 0 failed,” is useful supporting evidence, but it is not a replacement for the supported CI/builder signal on this deploy-development path. This PR changes core libfreeswitch build inputs and ABI surface; source review optimism is not enough here.
Required before approval:
- Re-trigger or unblock the supported builder/check path for head
9c97891c9632624527b5d655f92670348e029240. - Provide green CI/build evidence showing configure/autotools success,
libfreeswitchC++17 compile/link success, andtests/unit_cpp/test_web_server_registrypassing. - If the builder fails, attach the failing logs instead of letting the queued check sit there like a haunted vending machine.
Once that evidence is green, I do not see a remaining source-level blocker from this review round.
— 🪽 Hermes War Room on behalf of @minhtuan1407-telnyx
4668f86 to
50f1d16
Compare
dev-ryanc
left a comment
There was a problem hiding this comment.
PRBot automated review — no critical issues found. Approved based on: human approval from minhtuan1407-telnyx on current SHA + clean review from prior run (SHA unchanged).
…ent' into damir/telcore-166-build-mod_web_server-shared-http-listener-for-cross-module Resolved two conflicts: - src/switch_rtp.c: close_rtp_sockets() used raw switch_socket_close(). TELCORE-302 introduced rtp_socket_close(), which serializes closes on the RTP pool under sock_mutex; took that wrapper in all four spots. This branch's TELCORE-137 commit is the same patch that landed on deploy-development as #597, so it deduplicated cleanly. - tests/unit_cpp/Makefile.am: both sides assigned noinst_PROGRAMS, which would have dropped one test. Kept the ZMQ timeouts test as the assignment and appended test_web_server_registry.
|
resolved conflicts |
dev-ryanc
left a comment
There was a problem hiding this comment.
PRBot automated review — no critical issues found. Approved based on: human approval from tajamulTelnyx on current SHA + PRBot clean review of current SHA (same-SHA approval, SHA unchanged since 2026-08-07 review).
Adds the core-side pieces that mod_web_server needs to expose a stable C ABI to other FreeSWITCH modules. The module itself ships from its own repository (team-telnyx/mod_web_server) and is dropped into src/mod/applications/mod_web_server/ at deploy time, not committed into the freeswitch tree — same pattern as mod_telnyx and mod_dynamic_gateway.
Why these pieces live in libfreeswitch
APR loads modules with RTLD_LOCAL on Unix (libs/apr/dso/unix/dso.c). Symbols a module exports are not visible to any other loaded module. So for arbitrary modules to call switch_web_server_register(...), the registry, the public ABI, and the request/response opaque types have to sit in libfreeswitch — only the Beast acceptor, sessions, threads, and config loader stay inside the module itself.
What this commit adds
src/include/switch_web_server.h public C ABI
src/include/switch_web_server_internal.h C++ bridge to the module
src/switch_web_server.cpp registry + ABI implementations
Makefile.am add switch_web_server.cpp to libfreeswitch_la_SOURCES
tests/unit_cpp/test_web_server_registry.cpp 43-assertion standalone test
tests/unit_cpp/Makefile.am wire the test (always built; not gated on HAVE_CARES)
Public ABI surface
switch_web_server_register(module, method, path, mode, fn, ud)
switch_web_server_register_prefix(module, method, prefix, mode, fn, ud)
switch_web_server_unregister(module, method, path)
switch_web_server_unregister_module(module)
switch_web_server_available()
switch_web_request_method/path/query/header/body/remote_ip/param(req)
switch_web_response_set_status/header/body/printf(res)
Drain semantics
Each registered route is tagged with its owning module name and a shared ModuleSlot (atomic in_flight + condvar). On lookup, the result carries a move-only InFlightTicket whose destructor decrements the in_flight count; while a handler is running the ticket lives in the dispatch lambda and only drops when the handler returns. The matching switch_web_server_unregister_module() removes routes from the registry and then blocks on the slot's cv until in_flight hits zero, so a module's .so cannot be invoked after teardown.
Numeric input is parsed with strtoll + errno + end-pointer checks inside the registry (none of the public setters are exposed to operator input here, but the same discipline is applied in the module's config loader for consistency).
Tests
tests/unit_cpp/test_web_server_registry.cpp is a standalone C++ program (no FST, no live core) that links against libfreeswitch and covers exact / pattern / prefix matching, 404 / 405 collection, ANY-method wildcards, conflict + idempotent re-register, in-place handler update on re-register by the same module, sweep-by-module isolation, snapshot, and single-route unregister. 43 assertions.