BlogEcommerce

Silent fillers

A 200 confirmed an incomplete payload, and three missing fields came back with invented defaults

Nicolás Biondi

A tray of empty slots being filled by drifting cyan mist, beneath a suspended brass stamp glowing warm amber.

I sent an incomplete payload and the vendor answered 200 every time. I found it by reading the printed output: one field showed a generic value I had never chosen. The vendor never mentioned it. It behaved like the clerk at the window who stamps your incomplete form after filling the blank boxes himself with whatever came to mind. You walk out happy with your stamp until you read what he wrote.

A payload is the package of data one system sends to another. Mine was missing three fields. The HTTP code doesn't tell "your data is fine" apart from "I completed what was missing as I saw fit".

I built it by trying combinations

When I built the integration I had no official example, so I put the request body together by trying combinations and kept the one that didn't fail. My success criterion was that the response didn't blow up. That criterion holds up for a long time without ever telling you it's the wrong one.

Meme UNO Draw 25 Cards: Tell me when a field is missing Tell me when a field is missing

The text didn't come from the template

Seeing that value, I assumed the vendor printed a fixed text and that I had to ask for a format change. The theory didn't last: the text came out of a field I never sent, and the vendor completed it with its own default.

Three fates for a missing field

The diff against the official example was uncomfortable: three fields were missing and none of them had ever returned an error. The vendor handles an incomplete field in three ways. It ignores it, fills it with a default, or rejects it. The first two end in a 200.

Incomplete payload

Vendor API

Ignored or defaulted

Field rejected

200 OK

Visible error

Ignoring the field or filling it with a default both end in 200; rejection is the only visible path

The one field they did reject is a free-text one with a strict format, typed by hand. Its default value carried a space, and a space doesn't pass the filter. I fixed that noisy error much earlier, because it was the only one that failed out loud.

What I write out and what I leave out on purpose

Now I write that field into the request instead of leaving it to the vendor's judgement. The catalogue identifier goes in as 0 on purpose: that's the value the official example uses for a line that doesn't come from the catalogue. The issuer identifier is read from the configuration, and when it isn't there I leave the field out instead of sending it empty. For a permissive API an empty field is worse than an absent one, because it looks like a decision. The field the person types gets cleaned before it goes out, fitted to the format the filter demands.

I didn't build my own schema validator or a client generated from the contract. The vendor publishes no formal schema, so inventing one would have meant keeping my assumption in two places instead of one.

We archived the draft instead of what we sent

We saved the draft of the document instead of the body that went out over the network, so for the whole investigation I read my intention instead of my request. Now a pure function rebuilds the real body and that's what we archive: it takes data and returns data, without touching anything else.

Along the way I found a unit test that asserted the wrong value, green since day one, defending the bug with admirable conviction.

I didn't trust the HTTP response on any of the fixes. I checked each one by reading the printed output, the only place where the error was visible to a person. I still have no formal schema for the contract, so no new payload goes out without its field by field diff against the official example. The 200 stopped counting as evidence, and if the vendor wants to convince me my package is complete, it will have to do it with something more than a stamp.

api-contract data-quality postmortem validation