Skip to content

fix(pact_models): V4 body with no content attribute is not a missing body - #547

Merged
rholshausen merged 1 commit into
pact-foundation:masterfrom
Endika:fix/v4-body-without-content-attribute
Aug 5, 2026
Merged

fix(pact_models): V4 body with no content attribute is not a missing body#547
rholshausen merged 1 commit into
pact-foundation:masterfrom
Endika:fix/v4-body-without-content-attribute

Conversation

@Endika

@Endika Endika commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes pact-foundation/pact-python#1103.

A V4 message whose contents is a JSON object without a content attribute was
read as a missing body, so the interaction verified successfully whatever the
provider produced. From the issue, these two pacts differ only in how contents
is written:

"contents": { "Payload": "exists" }
"contents": {
  "content": { "Payload": "exists" },
  "contentType": "application/json",
  "encoded": false
}

Verified against a provider returning {"Payload": "does NOT exist"}, the second
fails as it should and the first reports has a matching body (OK).

Cause

body_from_json in pact_models/src/v4/http_parts.rs took the Value::Object
branch, found no content attribute, and returned OptionalBody::Missing
silently. Nothing downstream then had a body to compare.

Note the asymmetry this sat next to: when the body attribute is not a JSON
object at all, the same function already warns and loads the value as plain text.
Only the object-without-content case was silent.

Fix

Missing is kept when the object holds nothing but body attributes — {}, or
contentType/encoded with no content — since that is a genuinely absent body.
Any other key means the payload has been written where the body attributes
belong, so it now warns and loads the whole value as the body, which is the
treatment the function already gives a non-object body.

What made me comfortable doing that rather than only adding a warning is that
this shape is never produced by this library. On the write side,
OptionalBody::to_v4_json emits content for Present, {"content": ""} for
Empty, and Value::Null for Missing/Null — and MessageContents::to_json
only inserts contents when that is an object, so a missing body omits the
attribute entirely. An object without content can only come from a hand-written
or incorrectly generated pact, so there is no legitimate producer to break.

Tests

Three tests, all failing before the change:

  • body_from_json_returns_missing_if_the_body_only_has_body_attributes{}
    and an object with only contentType/encoded stay Missing.
  • body_from_json_loads_the_whole_value_if_the_body_has_no_content_attribute
    the payload is loaded rather than dropped.
  • message_contents_without_a_content_attribute_are_not_dropped in
    v4/async_message.rs — the same at the interaction level, using the JSON from
    the issue.

cargo test --package pact_models 616 passed, --package pact_matching 419 and
823 passed. cargo clippy --package pact_models reports the same 274 warnings as
master, so none are added.

End-to-end

Since the report is against pact-python, I checked the symptom actually goes away
there rather than only the parsing. I built libpact_ffi from this branch and
rebuilt pact-python's CFFI extension against it — pactffi_version() reports
0.5.6 and ldd confirms it resolves to the local build — then ran the
reporter's scenario:

Pact Released FFI 0.5.4.1 This branch
Correct envelope has a matching body (FAILED) has a matching body (FAILED)
No content attribute has a matching body (OK) has a matching body (FAILED)
Result 1 pact failure 2 pact failures

The malformed pact now fails with the same mismatch as the correct one:

$.Payload -> Expected 'does NOT exist' (String) to be equal to 'exists' (String)

Notes

  • The change is in body_from_json, which is shared with V4 HTTP request and
    response bodies, so it applies there too. That seemed right rather than
    message-specific: an HTTP body written the same way was being dropped just as
    silently.
  • I have not made this an error. Warning and reading the payload keeps the
    existing "salvage what we can" behaviour of this function, and turning
    malformed bodies into hard failures felt like a separate decision for you to
    make. Happy to change it if you would rather it failed loudly.

LLM disclosure

This PR text was written with LLM assistance.

@Endika
Endika force-pushed the fix/v4-body-without-content-attribute branch from 25f45c4 to f701ee6 Compare August 5, 2026 07:25
@rholshausen
rholshausen merged commit 3208dd7 into pact-foundation:master Aug 5, 2026
18 checks passed
@Endika
Endika deleted the fix/v4-body-without-content-attribute branch August 5, 2026 09:58
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.

(V3) Verifying a contract with a missing content field is always successful when it should error

2 participants