Skip to content

fix(icons): cache one source per icon, retry rate limits, stop panicking - #390

Merged
LeadcodeDev merged 1 commit into
mainfrom
fix/icon-cache-and-panic
Sep 27, 2026
Merged

LeadcodeDev merged 1 commit into
mainfrom
fix/icon-cache-and-panic

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes #365. Refs #388.

Six of the ten studies in the issue lost icons while rendering in parallel; most ended up inlining the Lucide sources by hand.

One download per colour and per size

The disk entry was {slug}-{color}-{w}x{h}.svg, because the Iconify URL baked colour and size into the SVG. A cache could hold 21 copies of lucide:sparkles, and changing only a colour sent an icon that rendered fine offline back to the network.

The source is now fetched once per prefix:name and recoloured and resized locally. Lucide draws with stroke="currentColor", so the recolour is a substitution; the resize rewrites the root width/height and leaves the viewBox alone, which is the glyph's own coordinate space.

No regex crate was added for this — the attribute rewrite is a short scan of the root tag.

No retry

A single GET. Four attempts with exponential backoff now, and deliberately only for 429 and 5xx:

matches!(error, ureq::Error::StatusCode(code) if *code == 429 || *code >= 500)

A 404 is not going to resolve on the fourth try, and retrying it would make a typo take four times as long to report.

The panic was deliberate, so the severity stays and the mechanism changes

I first downgraded it to a warning to match still and sheet. That broke an existing test:

an_unresolvable_icon_must_fail_the_preload_not_be_swallowed
  "prefetch_icons must panic (or otherwise hard-fail) when an icon cannot be
   resolved via disk cache or network, instead of silently continuing"

That is explicit evidence of intent, and it settles which of the issue's two options to take — it offers "either all fail with a named error, or all warn", and the codebase had already chosen. "or otherwise hard-fail" is the part that leaves the mechanism open, and a panic is the wrong one: it prints a backtrace-style message for what is user input, and cannot be handled by a caller.

So prefetch_icons returns a named IconsUnresolved error:

before after
render panic exit 1, named error
still exit 0, warns exit 1, named error
sheet exit 0, warns exit 1, named error
validate exit 0, offline unchanged
studio — warns, preview stays alive
$ rustmotion still -f missing-icon.json --time 0 -o out.png
Error: 1 icon(s) could not be loaded — checked the disk cache at
~/.cache/rustmotion/icons and the network, both failed:
  - 'lucide:no-such-icon-xyz' (color #2A2F6B, target 84x84px):
    Failed to fetch icon: http status: 404
[exit 1]

The studio is the one caller that must not die on a bad icon, so it warns and continues — which is also why returning an error beats panicking: the caller decides.

A test that asserted the bug

disk_cache_is_keyed_by_icon_color_and_size — "different colors must not share a cache file" — encoded the first fault as a guarantee. It is replaced by its opposite (disk_cache_is_keyed_by_the_icon_alone) rather than worked around, and the two other tests that built paths with the old helper were migrated.

Verification

Eight tests over the cache and rewrite, plus the rewritten preload test. cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace (1590) all clean.

Not done

The issue also suggests validate --offline and a rustmotion icons prefetch subcommand. Neither is here: the first needs a policy decision about what validate is allowed to touch (#320 is about it not reaching the network), and the second is a new command rather than a fix. Worth their own issues if wanted.

Written comment-free, per the codebase-wide rule from #345.

Three faults, one cause each.

**One download per colour and per size.** The disk entry was named
`{slug}-{color}-{w}x{h}.svg` because the Iconify URL baked colour and size into
the SVG. A cache could hold 21 copies of `lucide:sparkles`, and changing only a
colour sent an icon that rendered fine offline back to the network. The source
is now fetched once per `prefix:name`, cached under `{slug}.svg`, and recoloured
and resized locally — Lucide draws with `stroke="currentColor"`, so recolouring
is a substitution, and resizing rewrites the root `width`/`height` while leaving
the `viewBox` alone. Done without a regex crate: the attribute rewrite is a
short scan of the root tag.

**No retry.** A single `GET`, so a `429` from `api.iconify.design` under
parallel renders lost the icon. Four attempts with exponential backoff, and only
for `429` and `5xx` — a `404` is not going to resolve on the fourth try, and
retrying it would make a typo take four times as long to report.

**`render` panicked where `still` and `sheet` warned.** The panic was not an
oversight: `an_unresolvable_icon_must_fail_the_preload_not_be_swallowed` exists
to enforce it, and says "must panic (or otherwise hard-fail)". So the severity
stays and the mechanism changes. `prefetch_icons` returns a named
`IconsUnresolved` error, the encode paths propagate it, and `still` and `sheet`
now preload too, so all three fail the same way with the same message and a
clean exit code instead of a backtrace. The studio warns and keeps its preview
alive, which is the one caller that must not die. `validate` stays offline.

    $ rustmotion still -f missing-icon.json --time 0 -o out.png
    Error: 1 icon(s) could not be loaded — checked the disk cache at
    ~/.cache/rustmotion/icons and the network, both failed:
      - 'lucide:no-such-icon-xyz' (color #2A2F6B, target 84x84px):
        Failed to fetch icon: http status: 404
    [exit 1]

`disk_cache_is_keyed_by_icon_color_and_size` asserted the first fault as a
guarantee — "different colors must not share a cache file". It is replaced by
its opposite rather than worked around.

Closes #365
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 27, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 27, 2026
@LeadcodeDev
LeadcodeDev merged commit 6cee0f6 into main Sep 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Icons: cache keyed by colour and size, no retry on HTTP 429, and render panics where still and sheet only warn

1 participant