Skip to content

fix: report message handler failures instead of transport errors - #1669

Open
Endika wants to merge 1 commit into
pact-foundation:mainfrom
Endika:fix/message-handler-error-reporting
Open

fix: report message handler failures instead of transport errors#1669
Endika wants to merge 1 commit into
pact-foundation:mainfrom
Endika:fix/message-handler-error-reporting

Conversation

@Endika

@Endika Endika commented Aug 3, 2026

Copy link
Copy Markdown

✈️ Pre-flight checklist

📝 Summary

MessageProducerHandler.do_POST sent the 200 OK response line before calling
the handler:

self.send_response(200, "OK")

message = self.server.handler(description, data)

Any exception raised by the handler therefore left the client with a
half-written response. http.server printed a traceback to stderr and dropped
the connection, and the Pact core reported the disconnect as
Request Failed - error sending request for url (http://localhost:<port>/_pact/message).

Three unrelated failures collapsed into that one misleading message:

Cause Real exception Reported as
Handler dict value missing contents KeyError: 'contents' error sending request for url
Message description with no handler KeyError: '<description>' error sending request for url
Any exception in a user-provided handler e.g. RuntimeError error sending request for url

This PR makes three changes:

  1. MessageProducerHandler.do_POST calls the handler before responding, and
    reports any exception as a 500 whose body carries the exception type and
    message. This also makes it consistent with StateCallbackHandler.do_POST,
    which already called its handler first.

  2. StateCallbackHandler.do_POST catches handler exceptions the same way.
    Its ordering was already correct, but an exception still dropped the
    connection rather than producing a 500.

  3. Verifier.message_handler validates dictionary values eagerly. The case
    in Misleading 'error sending request for url' when message_handler dict returns raw payload (missing 'contents' key) #1665 is a configuration error that is fully knowable when the handler is
    set, so it is now raised as a TypeError on the message_handler(...) line
    in the user's own test, rather than surfacing much later as a failed
    interaction. The error names the offending message and shows the expected
    envelope shape:

    TypeError: Message handler for 'some-message' is missing the 'contents' key.
    Dictionary values must be Message envelopes, such as {'contents': b'...',
    'content_type': 'application/json'}, and not the raw payload.
    

    A request for a message with no handler now also lists the messages which
    do have one.

🚨 Breaking Changes

Verifier.message_handler now raises TypeError for a dictionary value that is
neither a callable, bytes, nor a Message dictionary. Previously such a value
was accepted and only failed during verification.

This only rejects handlers which could never have worked, and TypeError was
already the documented behaviour for invalid handler values. Anyone affected was
already getting a failed verification; they now get a clear error earlier

🔥 Motivation

Closes #1665.

The error the user sees points squarely at the network layer, so the natural
first steps are to check IPv4/IPv6 resolution, port binding, and the ordering of
add_transport — none of which are involved. The reporter of #1665 spent
roughly six hours on it, including two false-lead fixes, before spotting the
stderr traceback beneath the FFI error.

🔨 Test Plan

Added to tests/test_server.py:

  • test_message_post_handler_raises — a handler raising RuntimeError
    produces a 500 whose body contains the exception message, and the handler
    is called exactly once.
  • test_callback_post_handler_raises — the same for the state callback server.

Added to tests/test_verifier.py:

  • test_message_handler_invalid_dict_value — a dict without contents and a
    value of an unsupported type both raise TypeError at message_handler
    time.
  • test_message_handler_unknown_message — the KeyError for an unknown
    message names the messages which do have a handler.

Verified locally on Python 3.14 / Linux:

  • pytest tests/ --ignore=tests/compatibility_suite --ignore=tests/v2
    354 passed, 2 skipped.
  • pytest tests/compatibility_suite — 167 passed, 12 skipped, including
    test_v3_message_producer and test_v4_message_provider, which exercise
    the real FFI message-producer path.
  • ruff check, ruff format --check and mypy clean.

I also reproduced the original report end-to-end against a V3 message pact. The
misleading transport error is gone; the verifier now fails the interaction on a
content-type mismatch against the 500 body, and the cause is visible in the
log:

Message handler for some-message raised an exception.
...
TypeError: Message handler for 'some-message' is missing the 'contents' key. ...
127.0.0.1 - - [...] code 500, message Message handler failed

🔗 Related issues/PRs

Closes #1665.

Known limitation

The Pact core still matches the 500 response body against the expected
message body, so the verifier output reports a content-type mismatch
(expected a body of 'application/json' but the actual content type was 'text/html;charset=utf-8') rather than the producer error itself. Surfacing the error explanation in the verifier output would require a change in pact-reference, so it is out of scope here. Happy to raise that separately if you think it is worth doing — the eager validation in point 3 above means the most common case in #1665 never reaches the FFI at all.

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.

Misleading 'error sending request for url' when message_handler dict returns raw payload (missing 'contents' key)

1 participant