发布

  • refactor(utils): extract shared HTTP-post helper for delivery modules (#952)

    frostbyte_neo 发布于 2026-04-28 01:16:23 +00:00

    • refactor(utils): extract shared HTTP-post helper for delivery modules

    Closes #864.

    app/utils/slack_delivery.py, app/utils/discord_delivery.py, and
    app/utils/telegram_delivery.py each issued httpx.post directly,
    applied a per-provider timeout, parsed the response body, and converted
    exceptions to (False, error, ...) tuples. The transport pieces were
    identical; only the success criteria, auth scheme, and error-message
    extraction differed per provider.

    Add app/utils/delivery_transport.py with post_json plus a
    DeliveryResponse dataclass that captures the shared transport
    behavior: HTTP POST, timeout, optional follow_redirects, exception
    suppression, and JSON decoding with graceful fallback. The helper
    deliberately does not decide provider-level success — callers inspect
    status_code / data per their own semantics.

    Refactored callers:

    • Slack: _call_reactions_api, _post_direct, _post_via_webapp,
      _post_via_incoming_webhook — all four code paths now go through
      post_json. send_slack_report orchestration unchanged.
    • Discord: post_discord_message, create_discord_thread — same
      Bearer/Bot header pattern factored into _discord_auth_headers.
    • Telegram: post_telegram_message — bot-token redaction in error
      messages preserved by re-running _redact_token over
      response.error.

    Provider-specific payload building, success criteria
    (data["ok"] for Slack, status codes for Discord/Telegram), and
    error extraction all stay in the calling modules. Public function
    signatures are unchanged.

    Tests:

    • tests/utils/test_delivery_transport.py (new) — 15 tests covering
      happy path, every transport-failure shape (httpx.ConnectError,
      ReadTimeout, RequestError, OSError, RuntimeError), JSON-decode
      fallbacks (non-JSON body, JSON array, empty body), header / timeout /
      follow_redirects pass-through, and DeliveryResponse frozen-dataclass
      invariants.
    • tests/utils/test_slack_delivery.py (new) — 24 tests covering all
      four Slack code paths, send_slack_report orchestration, and
      fallback chain direct → webapp.
    • tests/utils/test_discord_delivery.py (extended) — 3 new tests
      pinning the helper-delegation contract: module no longer imports
      httpx, post_discord_message and create_discord_thread
      both go through post_json.
    • tests/utils/test_telegram_delivery.py (extended) — 3 new tests
      pinning the same contract plus a regression test that the bot-token
      is redacted out of error strings even when the failure originates
      from the shared transport.
    • All pre-existing tests in test_discord_delivery.py and
      test_telegram_delivery.py updated to patch
      app.utils.delivery_transport.httpx.post (the new transport
      boundary) instead of the per-module httpx.post import that no
      longer exists.

    Verification:

    • pytest tests/: 3191 pass, 2 skipped, 1 xfailed, 0 failures.
    • pytest tests/utils/: 105 pass, 1 skipped (was 32 + 26 = 58 across
      delivery modules; now 105 with helper + Slack additions).
    • ruff check app/ tests/ and ruff format --check: clean.
    • mypy app/utils/: clean on touched modules; the 5 reported errors
      are pre-existing missing-stub warnings in
      app/integrations/{mariadb,mysql,hf_remote} unrelated to this PR.
    • ci: retrigger after anyio/mcp circular-import flake

    The previous CI run hit a known transient ImportError in
    anyio.lowlevel/mcp when opensre integrations list ran on a
    pytest-xdist worker. Reproduces zero times locally on origin/main and
    on this branch; pushing an empty commit to re-run with a fresh
    interpreter pool.

    • fix(utils): address greptile review on delivery transport refactor
    • Wrap DeliveryResponse.data in MappingProxyType so the frozen
      dataclass is fully immutable end-to-end. Mutating the parsed body
      now raises TypeError instead of silently succeeding, and caller-
      passed dicts can no longer leak mutations into the response.
    • Add error_type field on DeliveryResponse populated with
      type(exc).__name__ on transport failures. slack_delivery._post_direct
      threads it back into the exception log so on-call can distinguish
      TimeoutError from ConnectionError at a glance — restores the
      pre-refactor log shape that #864 had dropped.
    • Add TestDelegatesToSharedTransport to tests/utils/test_slack_delivery.py,
      mirroring the regression class on the discord/telegram test files.
      Pins that slack_delivery does not import httpx and that all four
      code paths (_call_reactions_api, _post_direct, _post_via_webapp,
      _post_via_incoming_webhook) route through delivery_transport.post_json.
    • Add focused tests for the immutable-data, error_type, and slack
      exc_type-log behaviours (+17 tests; 3208 pass / 2 skipped / 1 xfail).

    Tighten _discord_error_from_data signature to Mapping[str, Any] so
    mypy stays clean against the new read-only data type.

    • refactor(utils): address review nits — restore log key, rename field, drop redundant header, fix CodeQL
    • slack_delivery _post_direct exception log key reverted to type=%s
      (matching the exact pre-refactor format type=%s channel=%s thread_ts=%s detail=%s) so existing log parsers that key off
      type= keep working. Per @muddlebee's nit on PR #952.
    • DeliveryResponse field renamed error_typeexc_type. Pythonic
      abbreviation for "exception type"; can't be type because that
      shadows the builtin. Field/log-key are intentionally distinct: the
      field is the Python attribute, the log key matches the legacy format.
    • Drop Content-Type: application/json; charset=utf-8 from
      _slack_bearer_headers and _discord_auth_headers. httpx.post
      already sets Content-Type: application/json automatically when the
      request uses the json= kwarg, so the explicit header was
      redundant. No behavioural change for Slack/Discord (UTF-8 is httpx's
      default encoding and neither provider parses the charset suffix).
    • Fix CodeQL py/import-and-import-from alerts on tests #503/#504. The
      test_module_does_not_import_httpx regression test in each
      delivery test file was importing the module under test via both
      import X as mod and from X import Y styles. Switched to a
      single from app.utils import <module> style so the same module
      is no longer dual-imported.

    Test count unchanged (3208 pass / 2 skipped / 1 xfail). Lint, format,
    and mypy app/utils/ clean.

    • fix(slack): restore charset=utf-8 — Slack emits missing_charset warning without it

    Live e2e probe against real slack.com/api/chat.postMessage revealed
    that dropping the explicit Content-Type: application/json; charset=utf-8
    header (per the previous nit-fix commit) caused Slack to add a
    missing_charset entry to response_metadata.warnings on every
    request. httpx alone sets only the bare application/json for json=
    kwargs, but Slack's docs explicitly recommend the charset suffix
    (https://api.slack.com/web#posting_json) and emit the warning otherwise.

    Restored the charset=utf-8 header on _slack_bearer_headers with
    an inline comment explaining the reason. _discord_auth_headers stays
    auth-only (Discord does not emit this warning, and its docs do not
    require the charset suffix).

    Pinned via direct header assertions on the two existing slack tests that
    already captured headers (test_sends_correct_url_and_headers for the
    reactions API, test_sends_thread_reply_payload for chat.postMessage),
    so a future regression that re-drops the charset will fail loudly.

    Verified live:

    • direct chat.postMessage probe now returns warnings=[] (was
      ['missing_charset'] before this commit).
    • 38/38 live checks against real Slack / Discord / Telegram pass.
    • 3208 unit tests pass; lint / format / mypy clean.
    下载附件