Skip to content

Build1 publisher2 min readPublished

A status-code guard turned Open Food Facts' normal answer into a server error

Open Food Facts replies to an unknown barcode with HTTP 404 and a product-not-found body. SnapCal's 200-to-299 guard threw before anything read that body, so the commonest outcome reached users as "Server error (404)".

The Engineer · Build desk

Illustration accompanying A status-code guard turned Open Food Facts' normal answer into a server error

What happened

  • SnapCal's HTTP layer threw on any status outside 200 to 299, and the thrown error reached the user as the message "Server error (404)".
  • Open Food Facts answers a well-formed barcode it does not hold with HTTP 404 and a body reading status 0, product not found.
  • The fix returns nil on a 404 before validation runs, and the view then shows a Product not found alert with an Add Manually action.
  • Foods typed in by hand are saved under the scanned barcode in a dictionary held apart from the API cache, so the next scan of that package resolves offline.
  • At release the iTunes lookup endpoint returned zero results for hours when queried by id, while the same query by bundleId returned the live listing at price 0.0.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Perceived reliability tracks database geography: in countries where Open Food Facts coverage is thin, the error path is the path most users meet, and the app looks broken to them and fine to its developer.
  • decision Bug reports of this shape arrive as complaints about the server, so triage has to start at the error taxonomy before anyone opens a network trace.
  • precedent An upstream that uses 404 as a record-level no cannot be detected from the status line, so the not-found semantics of each API have to be read out of its docs before the HTTP layer is written.

By the time `APIError.httpError` is thrown, the answer the user asked for is already in memory. The session call hands back `data` and `response` together, and `validateResponse(response)` runs after it [8]. Open Food Facts puts its verdict inside that body: `{"status":0,"status_verbose":"product not found"}` [4]. The guard never opens it.

The guard rejects every status outside 200 to 299, and the error case interpolates the code it received into the string the user reads, so a 503 during an outage and a 404 meaning the database holds no such product arrive as the same message [15]. Both times the app blames the server.

Coverage is what makes the second case the usual one. The author writes that Open Food Facts coverage is heavily skewed by country, and that "no data" is the expected outcome for a random package [5]. A tester reported that "some barcodes work, some say something went wrong" and concluded "your server logic is broken" [6]. The developer's own diagnosis was shorter: "the API was fine, my error taxonomy was wrong" [7].

The app was built solo in about two weeks, SwiftUI, iOS 17 and up, no third-party dependencies, 85 unit tests [1]. The conditional that fixes this is cheap. The caller pays for it: the view now has to tell an absent record from a broken call, and the post does that by checking `store.errorMessage == nil` before it shows the not-found alert [9]. In my view that is the soft spot in an otherwise clean patch, because three outcomes are being carried in two variables and nothing stops a nil food arriving next to a non-nil error message. An enum with found, notFound and failed cases would make that combination impossible to write.

For the fix to transfer, every 404 that endpoint can emit has to mean absence. The check keys on the status code alone, not on `status:0` in the payload [8][4]. A mistyped path or a retired route returns 404 as well, and the early return would dress both as a missing product with an offer to type it in by hand. A guard on 200 to 299 is correct about HTTP and wrong about this API; a guard that swallows every 404 is wrong about the broken URL.

The same confusion between a signal and a failure showed up again on release day. The author says he cannot explain which index or cache lifetime caused the split, and wrote: "when two ways of asking the same question disagree, don't trust the one that's telling you what you fear" [13].

What to watch

  • Whether Open Food Facts keeps 404 as its answer for unknown barcodes; a 200 with status 0 would make the early return dead code.
  • Country-level coverage figures for Open Food Facts, which set how often the not-found path is the only path a scan can take.
  • Whether new App Store listings keep resolving under bundleId before they resolve under id in the iTunes lookup API.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories