Skip to content

Fix kJ-as-calories, API key leak, and empty abridged foods#10

Merged
mcgarrah merged 1 commit into
mainfrom
claude/fix-calories-key-leak-abridged
Jul 13, 2026
Merged

Fix kJ-as-calories, API key leak, and empty abridged foods#10
mcgarrah merged 1 commit into
mainfrom
claude/fix-calories-key-leak-abridged

Conversation

@mcgarrah

Copy link
Copy Markdown
Owner

Three bugs found reviewing the library. Each one fails quietly and hands the caller a confident wrong answer, which for nutrition data is worse than an error. All three are verified against the live FDC API, and the 11 new tests fail on main.

1. A food's kilojoules could be reported as its calories

FDC lists energy several times per food, under the same name:

id / number name unit
1008 / 208 Energy kcal
1062 / 268 Energy kJ (the same energy)
2047 / 957 Energy (Atwater General Factors) kcal
2048 / 958 Energy (Atwater Specific Factors) kcal

_get_nutrient_id matched on the name alone, so every one of them was keyed as calories and whichever came last in the list won. Clarified butter (fdc_id 171314) lists 900 kcal and then 3766 kJ:

analyze_food(client.get_food(171314)).calories_per_serving
# before: 3766.0   <- the kJ value, 4.2x too high
# after:   900.0

analysis/cli.py:85 and visualization.py:141 then printed that with a kcal suffix. It was never only about kJ, either — the Fuji apple carries both Atwater rows (64.66 and 58.20 kcal) and the winner was decided by list order.

Energy is now matched on its nutrient id/number and unit: only kcal rows can become calories, the plain Energy row is preferred over the Atwater variants so the result cannot depend on order, and the kJ figure is kept under energy_kj rather than discarded.

2. The API key leaked into exception messages

The key travelled as a query parameter, and requests embeds the full URL in its exception text:

FdcApiError: Request failed: HTTPConnectionPool(host='...'): Max retries exceeded
with url: /fdc/v1/food/1750340?format=full&api_key=YOUR_REAL_KEY

One connection blip and the key is in the caller's traceback, log files and error tracker. It now goes in an X-Api-Key header — confirmed accepted by api.data.gov, which fronts FDC — so it never enters a URL, and it is redacted from error messages as a backstop. No API change: FdcClient(api_key=...) and FDC_API_KEY behave exactly as before.

3. format="abridged" silently returned a food with no nutrients

An abridged response inlines each nutrient's fields on the row and carries no nutrient id; format="full" nests them under a nutrient key. Only the nested shape was parsed, and every abridged nutrient was dropped without a word:

len(client.get_food(171314).nutrients)                      # 23
len(client.get_food(171314, format="abridged").nutrients)   # before: 0   after: 18

Anyone reaching for abridged to save bandwidth was computing nutrition from an empty list.

Tests

11 new regression tests, all of which fail on main:

  • tests/unit/test_analysis_energy.py — kJ never becomes calories, order does not decide the answer, kJ stays available under its own id, abridged foods (no nutrient id, KCAL uppercase) still match.
  • tests/unit/test_client.py — key sent as a header, absent from the query string, and redacted from both network errors and HTTP error bodies.
  • tests/unit/test_models.py — both nutrient shapes parse; an abridged food keeps its nutrients.

Full suite: 57 passed, 8 skipped (integration/django).

Found while reviewing the library after v0.1.10; #4 (FdcApiError has no status_code though the docs tell users to branch on it) and #5 (only DriType.RDA works) are left for a follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_014avzoF5VnrUxwoy7m1aK6K

Three bugs found reviewing the library, each of which fails quietly and
hands the caller a confident wrong answer.

1. A food's kilojoules could be reported as its calories. FDC lists energy
   several times per food under the same name: 1008 (kcal), 1062 (the same
   energy in kJ), and the Atwater variants 2047/2048. All were matched on
   the name alone and keyed as "calories", so whichever came last won. Ghee
   (fdc_id 171314) lists 900 kcal then 3766 kJ and analyzed as 3766
   "calories" — which the CLI and HTML report printed with a kcal suffix.
   Energy is now matched on its id/number and unit, the plain Energy row
   beats the Atwater variants so order cannot decide the answer, and the kJ
   figure is kept under energy_kj instead of overwriting anything.

2. The API key travelled as a query parameter, and requests puts the full
   URL in its exception text — so the first connection blip wrote the key
   into the caller's traceback, logs and error tracker. It now goes in an
   X-Api-Key header (which api.data.gov accepts) and is redacted from error
   messages as a backstop.

3. format="abridged" returned a food with zero nutrients. Abridged rows
   inline the nutrient fields and carry no nutrient id; only the nested
   full-format shape was parsed and the rest were dropped in silence. Ghee
   came back with 0 nutrients instead of 18.

All three are verified against the live FDC API. The 11 new tests fail on
the current main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014avzoF5VnrUxwoy7m1aK6K
@mcgarrah
mcgarrah merged commit a30e905 into main Jul 13, 2026
4 checks passed
@mcgarrah
mcgarrah deleted the claude/fix-calories-key-leak-abridged branch July 13, 2026 20:01
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