Skip to content

fix(voice-bridge): match twiml-inbound route on pathname (per-profile voice 404)#263

Open
ssdavidai wants to merge 1 commit into
mainfrom
fix/voice-bridge-twiml-inbound-query
Open

fix(voice-bridge): match twiml-inbound route on pathname (per-profile voice 404)#263
ssdavidai wants to merge 1 commit into
mainfrom
fix/voice-bridge-twiml-inbound-query

Conversation

@ssdavidai

Copy link
Copy Markdown
Owner

The bug

The voice-bridge HTTP router matched the Twilio TwiML inbound route by exact req.url, which includes the query string:

if (req.url === TWIML_INBOUND_PATH) {   // TWIML_INBOUND_PATH = "/twiml/inbound"
  handleTwimlInbound(req, res)...
}

The per-profile voice feature (#120 Lane Vb) configures the Twilio number's VoiceUrl as https://voice.<domain>/twiml/inbound?profile=<slug>, and handleTwimlInbound (in twiml.ts) reads the profile via reqUrl.searchParams.get("profile"). Because the router compared the full req.url (/twiml/inbound?profile=cratchit) against the bare path /twiml/inbound, any VoiceUrl carrying ?profile= 404'd before reaching the handler. Per-profile voice routing was therefore completely broken — calls only worked with a bare /twiml/inbound, which always defaults to main.

Live evidence (verified today on tenant joe.alfred.black)

  • POST /twiml/inbound403 (served, signature-rejected — reaches the handler)
  • POST /twiml/inbound?profile=cratchit404 (rejected by the router before the handler)

After the fix, both return 403 (served) and ?profile=cratchit correctly reaches the handler.

The fix

Compare the URL pathname only, ignoring the query string:

if (new URL(req.url ?? "/", "http://localhost").pathname === TWIML_INBOUND_PATH) {

This mirrors the existing /voice/<id> WSS upgrade route, which already uses new URL(...).pathname matching.

The other exact-match routes in the same handler (/health, /metrics, /esphome/devices, /wyoming/status, /voice/recall-turn) are internal/health endpoints always called without query strings, so they are intentionally left unchanged.

Verification

npx tsc --noEmit passes clean in packages/voice-bridge/.

(Note: there may be an unrelated flaky CI check — please ignore.)

🤖 Generated with Claude Code

…q.url

The HTTP router matched the Twilio TwiML inbound route with
`req.url === TWIML_INBOUND_PATH`, which compares the full request URL
(including the query string) against the bare path "/twiml/inbound".

Per-profile voice routing configures the Twilio number's VoiceUrl as
`https://voice.<domain>/twiml/inbound?profile=<slug>`, and
handleTwimlInbound reads the profile via searchParams.get("profile").
Because the router compared the full req.url ("/twiml/inbound?profile=...")
against the bare path, any VoiceUrl carrying ?profile= 404'd before
reaching the handler — so per-profile voice was completely broken and
calls only worked with a bare /twiml/inbound, which always defaults to main.

Fix: compare the URL pathname only, ignoring the query string, mirroring
the existing /voice/<id> WSS route which already uses new URL(...).pathname.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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