Skip to content

[medium] fix: return a structured error when the remote event fetch fails - #391

Merged
righel merged 2 commits into
MISP:mainfrom
elhoim:fix/push-event-fetch-error-handler
Sep 17, 2026
Merged

righel merged 2 commits into
MISP:mainfrom
elhoim:fix/push-event-fetch-error-handler

Conversation

@elhoim

@elhoim elhoim commented Sep 17, 2026

Copy link
Copy Markdown
Member

BLUF

  • Priority: medium
  • push_event_by_uuid crashed with UnboundLocalError whenever the remote MISP server was unreachable, instead of returning its error dict.
  • Cause: the except block dereferenced response, which is only bound by the very call that raised.
  • Impact: DNS/connection/TLS/timeout failures killed the Celery push task with a traceback pointing at the handler, hiding the real cause; the UI got nothing useful.
  • Fix: return a fixed 502 plus the exception text, and log with logger.exception.
  • Covered by a new fixture-free regression test in app/tests/repositories/test_servers.py.

What was wrong

In api/app/repositories/servers.py, the handler guarding the remote event fetch read response.status_code and response.json():

    except Exception as ex:
        logger.warning(
            "Failed downloading the event {} from remote server {}".format(
                event_uuid, server.id
            ),
            ex,
        )
        return {
            "status": response.status_code,
            "message": "Failed downloading the event",
            "response": response.json(),
        }

response is bound only by remote_misp._prepare_request(...) inside the try — the call that can raise. Three defects in those lines:

  1. Any transport-layer failure (DNS resolution, connection refused, bad TLS, timeout raised by requests inside _prepare_request) leaves response unbound, so the handler itself raises UnboundLocalError.
  2. Even when response is bound — _check_json_response or MISPEvent.load raising on a 200 — response.json() can raise a second time on a non-JSON body.
  3. logger.warning(msg, ex) passes ex as a printf-style argument to an already-.format()-ed message, which logging reports as a formatting error rather than logging the exception; and warning discards the traceback.

Concrete impact

Pushing an event to an unreachable, DNS-unresolvable or bad-TLS server never returns the structured error dict. The Celery task push_event_by_uuid dies with a misleading traceback anchored in the error handler, so the actual connection failure is lost from the logs, and the push endpoint returns a 500 rather than a readable message.

What this changes

One exception handler:

    except Exception as ex:
        logger.exception(
            "Failed downloading the event {} from remote server {}".format(
                event_uuid, server.id
            )
        )
        return {
            "status": 502,
            "message": "Failed downloading the event: %s" % ex,
        }

Notes on the shape chosen:

  • It mirrors the sibling handler further down the same function, which already returns a fixed status and a message with no response dereference.
  • The response key is dropped rather than reconstructed: nothing consumes it. The router returns the dict verbatim, the Celery task discards it, and the frontend reads only message (frontend/src/components/events/EventActions.vue:58).
  • "...: %s" % ex is the idiom already used in this module for remote-connection failures.
  • logger.exception keeps the traceback and removes the stray positional argument.

This also removes the second latent crash (response.json() on a non-JSON body) without a separate change.

How it was verified

  • New regression test TestPushEventByUuid::test_unreachable_server_returns_an_error in api/app/tests/repositories/test_servers.py, following the existing fixture-free class pattern in that module. It patches get_remote_misp_connection so _prepare_request raises ConnectionError, asserts the fetch was actually reached, then asserts status == 502 and that the exception text reaches message. It errors with UnboundLocalError before this change and passes after it, and needs no database, OpenSearch or network.
  • python3 -m compileall-equivalent syntax sweep over api/ is clean.
  • No schema, route or migration change, so docs/features/api/openapi.json is unaffected.

The exception handler guarding the remote event fetch in
push_event_by_uuid read response.status_code and response.json(), but
response is bound only by the call inside the try that can raise. Any
transport-layer failure (DNS, connection refused, TLS, timeout) left it
unbound, so the handler raised UnboundLocalError and the structured
error dict was never returned: the Celery push task died with a
traceback pointing at the handler and the real cause was lost. Even
when response was bound, response.json() could raise again on a
non-JSON body.

Return a fixed 502 with the exception text instead, matching the
sibling handler in the same function, and log with logger.exception so
the traceback is kept. The stray positional argument to logger.warning,
which logging reported as a formatting error rather than logging the
exception, goes away with it.

The dropped "response" key has no consumer: the router returns the dict
verbatim, the Celery task discards it and the frontend reads only
"message".

Add a fixture-free regression test that makes the remote request raise
and asserts the error dict is returned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.40%. Comparing base (adf4f0c) to head (1fdb90a).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #391      +/-   ##
==========================================
+ Coverage   84.33%   84.40%   +0.06%     
==========================================
  Files         208      208              
  Lines       19391    19402      +11     
==========================================
+ Hits        16354    16376      +22     
+ Misses       3037     3026      -11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@righel

righel commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Makes sense, thanks!

@righel
righel merged commit 2d90051 into MISP:main Sep 17, 2026
3 of 4 checks passed
@elhoim
elhoim deleted the fix/push-event-fetch-error-handler branch September 19, 2026 22:26
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.

2 participants