Prompt
When the API returns an error, the Slack integration swallows it and says
"Uh oh! There was an unexpected error starting the job :(". An agent
debugging this gets nothing. Preserve the HTTP status, the error detail
and the error id, and render them so a machine can parse them and a human
can act on them.
Files changed
5
Integration fixed
Slack
Tests added
2
Runtime verified
CI only
01
Milestones
Four slices. Slice 1 is shared and independent; slices 2–3 are the Slack wiring; slice 4 is the test that keeps it honest.
Slice 1
Shared exception class
New APIConversationError carrying status_code, error_detail and error_id. to_slack_message() renders YAML front matter plus a human-readable line.
integrations/exceptions.pynew file
Slice 2
Preserve the API error in the view
send_message_to_v1_conversation() stops catching Exception. It catches httpx.HTTPStatusError, reads detail and error_id out of the response body, and raises APIConversationError.
integrations/slack/slack_view.pyline 422
Slice 3
Structured handler in the manager
An except APIConversationError branch sits ahead of the catch-all in start_job() and calls to_slack_message() instead of the "Uh oh!" string.
integrations/slack/slack_manager.pyline 849
Slice 4
Tests
One test asserts HTTP 500 becomes APIConversationError with the right fields. One asserts the front-matter output shape. Both run in the existing suite.
tests/unit/integrations/slackpytest
02
Data flow
The error already travels the whole path — the top row is what we discard today, the bottom row is what we keep. Nothing new crosses a boundary; we just stop throwing it away.
The error is identical in both rows. The only difference is whether we read the response body before we re-raise.
03
Key code
Two pieces. The exception carries the facts; the view is where the facts were being dropped.
# before — the real error was discarded here# except Exception as e:# raise Exception(# 'Failed to send message to v1 conversation') from eexcept httpx.HTTPStatusError as e:
error_detail = str(e)
error_id = Nonetry:
body = e.response.json()
error_detail = body.get('detail', str(e))
error_id = body.get('error_id')
except Exception:
passraiseAPIConversationError(
status_code=e.response.status_code,
error_detail=error_detail,
error_id=error_id,
) from e
04
Risks & mitigations
Risk
Sev
Mitigation
Upstream repo does not accept external pull requests, so the patch cannot land as a PR.
HIGH
Publish the branch on the fork and file a public issue on OpenHands/OpenHands carrying the diff and the reproduction. A maintainer applies it.
The same swallow exists in five other integrations (github, gitlab, bitbucket, bitbucket_dc, azure_devops).
MED
Scope this change to Slack; note the other five in the issue body as a mechanical follow-up — same three lines each.
Full test suite needs the enterprise dependency tree (litellm, openhands-sdk), which is not installable locally.
MED
Syntax verified locally; the two new tests are written to run in the existing suite on CI. Say "syntax verified, runtime CI" in the issue — never call it verified.
Front matter is agent-friendly but unfamiliar to a human reading raw Slack.
LOW
The detail line sits below the block and reads plainly on its own; the front matter is additive, not a replacement for prose.
05
Open questions
Should the other five integrations be fixed in the same change?
The call is Slack-only for the first change. The other five share the exact pattern, so a single follow-up can sweep them — but each needs its own view-layer edit and its own test.
Decide with · maintainer, before follow-up
Which issue should this link to?
The motivating bug is a plugin schema crash, not #14480 (Slack resolver, missing critic key) or enterprise #296 (stale org key). Neither is this. File a new issue and reference the two as prior art.