Skip to content

Type the currency fetch errors and say what actually failed - #22

Merged
fardavide merged 1 commit into
mainfrom
claude/currency-update-errors-3r13k9
Aug 5, 2026
Merged

Type the currency fetch errors and say what actually failed#22
fardavide merged 1 commit into
mainfrom
claude/currency-update-errors-3r13k9

Conversation

@fardavide

Copy link
Copy Markdown
Owner

The refresh banner read "Refresh failed, showing cached data" whatever had gone wrong, because that was the only sentence it could say: everything reached it as .network(cause: .json) or .network(cause: .unknown).

Why the currencies can't update

Tracing the vague message turned up the reason there is anything to report in the first place: the shipped app has no API keys.

2996b67 "Chore: move Core -> SwiftlyCore" renamed the module directory, but both key-injection scripts still did cd Core/Sources/Currency/Data/Api, and a stale Core/.../ApiKey.swift survived the move. So the keys were written into a file nothing compiles, exit 0 swallowed the failed cd, and every build since has shipped the checked-in ApiKey.swift with three empty strings. Requests go out as ?apikey=, the services answer 401, and the app falls back to cached rates forever — which is exactly the "Updated 5 months ago" in the screenshot.

Both ci_scripts/ci_pre_xcodebuild.sh and the "Add API keys" Xcode build phase now target the compiled file and fail the build if it isn't there, rather than silently shipping empty keys. The dead Core/ copy is deleted.

Why the message was vague

Two causes, both in URLSession.resultData:

  • the HTTP status was never read, so a 401 error envelope went straight to the JSON decoder and came back as "Cannot process network response, please contact the developer";
  • anything the transport threw collapsed into .unknown.

The typed errors

Cause What the user now reads
noConnection No internet connection
timeout The currency service took too long to respond
unauthorized (401/403) The currency service rejected the app's API key
rateLimited (429) Too many requests, please try again later
notFound (404) The currency service is no longer reachable, please update the app
serverError (5xx) The currency service is unavailable (error 503)
unexpectedStatus Unexpected response from the currency service (error 418)
json unchanged

DataError.NetworkCause mirrors ApiError; StorageError gains readFailed to separate a refused read from an unclassified one. Each cause carries its own copy and SF Symbol.

The banner leads with the typed cause, demotes "Showing the rates saved on this device" to a second line, and offers Retry.

Tests

  • New ApiErrorTests: status-code and URLError classification, plus resultData driven through a stubbed URLProtocol — including the case that motivated all of this, that a 401 with an error body is reported as unauthorized rather than as bad JSON.
  • RealCurrencyRepositoryTests: the typed cause survives the fallback to cache.
  • ConverterViewModelTests: refreshError carries the typed cause, and is absent when the refresh succeeds.

ErrorView's layout and the .json copy are byte-identical, so the committed snapshot baselines still match.

Two things to flag

  • Nothing here has been compiled or run. The session had no Swift toolchain, so this rides on CI for its first build.
  • RealCurrencyRepositoryTests is dead code: none of its ~30 methods carry @Test, so swift-testing never runs them. The one added here does. I left the rest alone rather than enable 30 unverifiable assertions in the same PR, but that file asserts nothing today.

Generated by Claude Code

The refresh banner read "Refresh failed, showing cached data" whatever had
gone wrong, because that was the only sentence it could say: everything
reached it as `.network(cause: .json)` or `.network(cause: .unknown)`.

Two things caused that. `URLSession.resultData` never looked at the HTTP
status, so a 401 error envelope went straight to the decoder and was
reported as a malformed response; and anything thrown by the transport
collapsed into `.unknown`.

- ApiError gains noConnection, timeout, unauthorized, rateLimited,
  notFound, serverError and unexpectedStatus, classified from the status
  code and from URLError, with DataError.NetworkCause mirroring it.
- StorageError distinguishes a failed read from an unclassified error.
- Each cause carries its own copy and symbol in ErrorModel.
- The banner shows the typed cause as its headline, demotes "showing the
  rates saved on this device" to a second line, and offers Retry.

Fix why the currencies could not update in the first place: the API keys
were being injected into Core/Sources/..., a path that stopped existing
when the module directory was renamed to SwiftlyCore. The `cd` failed,
`exit 0` hid it, and every build shipped the checked-in ApiKey.swift with
three empty strings, so every request went out as `?apikey=` and came back
401. Both injection scripts now target the compiled file and fail loudly if
it moves again; the stale Core/ copy is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WGP73fJTZHDfydoSMf2Hw
@semanticdiff-com

semanticdiff-com Bot commented Aug 5, 2026

Copy link
Copy Markdown

@fardavide
fardavide merged commit 57a9c8d into main Aug 5, 2026
4 checks passed
@fardavide
fardavide deleted the claude/currency-update-errors-3r13k9 branch August 5, 2026 07:31
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