Skip to content

docs(agi): document FastAGI hangup signaling#68

Merged
itg-karthicr merged 1 commit into
masterfrom
investigate/issue-5-fastagi-hangup
Jun 29, 2026
Merged

docs(agi): document FastAGI hangup signaling#68
itg-karthicr merged 1 commit into
masterfrom
investigate/issue-5-fastagi-hangup

Conversation

@itg-karthicr

Copy link
Copy Markdown
Contributor

Summary

  • Document how Asterisk sends FastAGI HANGUP notifications over the AGI command socket.
  • Explain why Asterisk can log ast_carefulwrite / Broken pipe after a handler returns during hangup-sensitive applications such as Dial.
  • Add dialplan examples for AGISIGHUP=no and AGIEXITONHANGUP=yes.

Root Cause

Asterisk 11's AGI runner sends a final FastAGI HANGUP notification when the channel hangs up and AGISIGHUP is enabled. If the FastAGI handler has already returned, the server-side socket is closed and Asterisk's final write can surface as ast_carefulwrite: write() returned error: Broken pipe. This is controlled by Asterisk dialplan variables before entering FastAGI rather than by pystrix runtime code.

Closes #5.

Validation

  • python -m pytest -q -> 72 passed
  • ruff check . -> All checks passed
  • ruff format --check . -> 26 files already formatted
  • python -m sphinx -b html doc /private/tmp/pystrix-doc-build-issue-5 -> build succeeded

Copy link
Copy Markdown
Contributor Author

Scoped review pass completed for this docs-only resolution of #5.

Review coverage:

  • Protocol accuracy: no blockers. The added note matches Asterisk 11 FastAGI behavior: HANGUP is sent inline over the FastAGI connection, AGISIGHUP=no suppresses those hangup notifications when set before entering AGI(agi://...), and AGIEXITONHANGUP=yes makes Asterisk exit AGI handling on hangup detection.
  • Documentation clarity/RST: no blockers. The section is placed naturally in the FastAGI example page, the literal dialplan blocks render correctly, and the Sphinx build is clean.
  • Issue disposition: no blockers. Closing Asterisk error on hangup during dial in FastAGI application #5 via docs is intentional because the reported ast_carefulwrite: Broken pipe is an Asterisk-side late write after the FastAGI handler/socket lifecycle has ended. pystrix only receives the AGI environment after the session starts, while Asterisk reads AGISIGHUP before entering the AGI exchange, so pystrix cannot set that flag early enough in runtime code.

Validation remains green:

  • python -m pytest -q -> 72 passed
  • ruff check . -> passed
  • ruff format --check . -> passed
  • Sphinx HTML build -> succeeded
  • GitHub CI -> passed across docs, lint, package build, and Python 3.9-3.13

@itg-karthicr

Copy link
Copy Markdown
Contributor Author

Review Panel — Round 1

Base: master | Diff range: master...origin/investigate/issue-5-fastagi-hangup | Reviewers: Codex, general-purpose, code-reviewer (3/3)


Warning

1. AGISIGHUP=no example is a no-op and its description is wrongdoc/examples/fastagi.rst (new section)

AGISIGHUP=no is pystrix's default. agi_core.py:231:

if "no" == self._environment.get("AGISIGHUP", "no") and "HANGUP\n" == line:
    line = self._read_line(should_strip=False)

When AGISIGHUP is unset or no, pystrix already intercepts the inline HANGUP line mid-execution and reads the next line — the broken pipe does not surface in that path. Setting Set(AGISIGHUP=no) in the dialplan changes nothing for users who have not explicitly set AGISIGHUP=yes. The description "suppress those hangup notifications" is also wrong: AGISIGHUP controls Asterisk's POSIX SIGHUP signal delivery to the AGI process, not the inline TCP HANGUP line. A user who adds this dialplan variable will see no change.

Suggested rewrite: explain that when AGISIGHUP=no (the default), pystrix consumes the inline HANGUP line and continues waiting for the real command response. The broken-pipe race applies specifically when Asterisk writes HANGUP after the handler exits — a timing artifact, not something AGISIGHUP controls.

2. Timing description conflates two distinct casesdoc/examples/fastagi.rst (new section, sentence 2)

"Asterisk may send HANGUP after the FastAGI handler has already returned and the socket has been closed."

This describes only the post-exit race. When HANGUP arrives while the handler is still running (the default AGISIGHUP=no path), _read_line at agi_core.py:231 skips it transparently and the broken pipe never occurs. The broken-pipe error is specific to the race where Asterisk writes to a socket pystrix already closed. These two paths should be described separately so the workarounds make sense in context.

3. Missing pystrix exception cross-referencesdoc/examples/fastagi.rst (new section)

The section explains Asterisk-side variables but does not tell the developer what pystrix raises. AGISIGPIPEHangup surfaces on a closed pipe (agi_core.py:236); AGISIGHUPHangup surfaces when a POSIX SIGHUP is received (agi.py:81). A developer diagnosing one of these errors needs to know which exception to catch.


Info

  • Spelling: "signalling" (double-L) has no prior prose precedent in the docs or AGI source. The codebase defaults to US English in prose — use "signaling".
  • AGIEXITONHANGUP is handled entirely by Asterisk, not pystrix. A brief note ("Asterisk itself closes the connection") would prevent readers from looking for pystrix behavior that does not exist.
  • same => n, dialplan syntax requires a prior extension definition to be valid. An optional one-line comment would help readers unfamiliar with Asterisk dialplan.

@itg-karthicr itg-karthicr force-pushed the investigate/issue-5-fastagi-hangup branch from 92c4bdb to 0929fcb Compare June 28, 2026 02:58

Copy link
Copy Markdown
Contributor Author

Addressed the Round 1 review panel feedback in 0929fcb.

What changed:

  • Split the docs into the two distinct hangup paths:
    • inline HANGUP received while pystrix is waiting for a command response, which pystrix consumes before reading the actual response;
    • final HANGUP sent after the FastAGI handler/socket has already closed, which can show up in Asterisk as ast_carefulwrite / Broken pipe.
  • Kept AGISIGHUP=no as an Asterisk-side dialplan setting, but clarified that it prevents Asterisk from sending those hangup notifications rather than implying pystrix controls it.
  • Clarified that AGIEXITONHANGUP=yes is handled by Asterisk and closes AGI processing from the Asterisk side.
  • Switched signalling to signaling.
  • Made the dialplan examples self-contained with an initial exten => ... line.
  • Added the pystrix exception cross-reference for closed FastAGI connections: AGISIGPIPEHangup.

Validation after the update:

  • python -m pytest -q -> 72 passed
  • ruff check . -> passed
  • ruff format --check . -> passed
  • python -m sphinx -b html doc /private/tmp/pystrix-doc-build-issue-5 -> build succeeded

@itg-karthicr

Copy link
Copy Markdown
Contributor Author

Review Panel — Round 2

Base: master | Diff range: master...origin/investigate/issue-5-fastagi-hangup | Reviewers: Codex, general-purpose, code-reviewer (3/3)


Warning — still open

AGISIGHUP=no example is still a no-op (Codex + general-purpose; verified independently against agi_core.py:231)

The updated text reads: "Set AGISIGHUP before entering FastAGI if Asterisk should not send those hangup notifications" with Set(AGISIGHUP=no). This still implies the variable prevents Asterisk from sending HANGUP, which is incorrect on two counts:

  1. AGISIGHUP=no is the default in pystrix. agi_core.py:231 uses self._environment.get("AGISIGHUP", "no"). Setting it explicitly in the dialplan changes nothing in pystrix's behavior.
  2. It does not prevent the post-exit broken-pipe race. That race happens after the handler returns and the socket closes — pystrix is not reading at that point, so the consume-and-reread path at line 231 is irrelevant.

Suggested fix: Remove the AGISIGHUP=no block. Replace it with a note that mid-execution HANGUP consumption is the default behavior — no dialplan change is needed. The only lever to suppress the post-exit broken-pipe log message is AGIEXITONHANGUP=yes, which is already documented in the section. Optionally note that the broken pipe itself is cosmetic — the FastAGI handler completed normally.


Resolved

  • Timing — two-paragraph structure correctly separates mid-execution consumption from the post-exit race. ✓
  • Exception cross-reference:class:agi.AGISIGPIPEHangup`` matches the re-exported symbol and existing Sphinx convention. ✓
  • Spelling — "signaling" (single-L). ✓
  • RST heading — correct character and length. ✓
  • Dialplan examples — self-contained with exten => anchor line. ✓

@itg-karthicr

Copy link
Copy Markdown
Contributor Author

Correction — Round 2 AGISIGHUP=no finding is wrong

Round 2 synthesis flagged the docs' AGISIGHUP=no example as a no-op and recommended removing it. That was an error. This comment retracts it.


Root cause of the error

The panel conflated two things that share the name AGISIGHUP:

  1. pystrix client sidepystrix/agi/agi_core.py:231 reads AGISIGHUP from the AGI environment dict to decide whether to consume a HANGUP line mid-dialog, defaulting to "no". This is almost always "no" on the client side.
  2. Asterisk sideAGISIGHUP is a channel variable that Asterisk reads to decide whether to send HANGUP at all. This is what the docs refer to.

The panel reasoned about (1) and applied that reasoning to (2). They are independent.


Asterisk 11 res/res_agi.c evidence

// Line 3547–3548 — gate computed at call start
sighup_str = pbx_builtin_getvar_helper(chan, "AGISIGHUP");
send_sighup = !ast_false(sighup_str);

ast_false() returns true for "no", "false", "off", and "0", and for NULL. So:

  • AGISIGHUP unsetast_false(NULL) = 0 → send_sighup = 1 → Asterisk sends (default behavior).
  • AGISIGHUP=noast_false("no") = 1 → send_sighup = 0 → Asterisk sends nothing.

Both write sites are gated on that flag:

  • Line 3567 — mid-execution HANGUP, sent when hangup is detected during the command dialog: if (send_sighup) { ... ast_agi_send(agi->fd, chan, "HANGUP\n"); }
  • Line 3680 — post-loop "Notify process" HANGUP, sent after the command loop exits: if (send_sighup) { ... ast_agi_send(agi->fd, chan, "HANGUP\n"); }

Line 3680 is the exact write that races the closed socket and causes the broken pipe in ast_carefulwrite. Setting AGISIGHUP=no zeroes send_sighup, so Asterisk skips both writes entirely.

The Asterisk 11 documentation block (line 824) confirms this:

A fast AGI server will correspondingly receive a HANGUP inline with the command dialog. Both of these signals may be disabled by setting the AGISIGHUP channel variable to no before executing the AGI application.


On AGIEXITONHANGUP

This controls whether Asterisk breaks out of the command loop early on hangup (around line 3573). It does not gate the HANGUP write. The two knobs do different things and are not interchangeable.


Conclusion

The AGISIGHUP=no guidance in the docs is correct and should stay. No change to doc/examples/fastagi.rst is needed. All other Round 2 findings stand.

@itg-karthicr itg-karthicr marked this pull request as ready for review June 29, 2026 20:10
@itg-karthicr itg-karthicr merged commit aaed730 into master Jun 29, 2026
8 checks passed
@itg-karthicr itg-karthicr deleted the investigate/issue-5-fastagi-hangup branch June 29, 2026 20:10
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.

Asterisk error on hangup during dial in FastAGI application

1 participant