fix(telephony): handle Cloudonix CDR payloads missing session/disposition

The /cloudonix/cdr webhook is a public, unauthenticated endpoint that parses
arbitrary external JSON. It dereferenced cdr_data.get("session").get("token")
unconditionally, so a partial or malformed CDR payload that omits "session"
(or sends "session": null) raised AttributeError -> HTTP 500. The existing
"Missing call_id field" guard right below it was unreachable because the crash
happened first.

StatusCallbackRequest.from_cloudonix_cdr had the same fragility plus a second
one: data.get("disposition", "") returns None when the key is present-but-null,
and None.upper() then crashed.

Navigate both fields defensively so missing/null values fall through to the
intended graceful error path instead of crashing. Adds regression tests
covering missing session, null session, null disposition, and the well-formed
mapping path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mubashir Rahim 2026-06-03 14:22:01 +05:00
parent acc2ef9e96
commit ace4903c0e
3 changed files with 98 additions and 3 deletions

View file

@ -103,7 +103,7 @@ async def handle_cloudonix_cdr(request: Request):
return {"status": "error", "message": "Missing domain field"}
# Extract call_id to find workflow run
call_id = cdr_data.get("session").get("token")
call_id = (cdr_data.get("session") or {}).get("token")
logger.info(f"Cloudonix CDR data for call id {call_id} - {cdr_data}")
if not call_id:
logger.warning("Cloudonix CDR missing call_id field")

View file

@ -114,11 +114,11 @@ class StatusCallbackRequest(BaseModel):
"NOANSWER": "no-answer",
}
disposition = data.get("disposition", "")
disposition = data.get("disposition") or ""
status = disposition_map.get(disposition.upper(), disposition.lower())
return cls(
call_id=data.get("session").get("token"),
call_id=(data.get("session") or {}).get("token") or "",
status=status,
from_number=data.get("from"),
to_number=data.get("to"),