Skip to content

Return network failures, timeouts and bad payloads as errors - #18

Merged
milten89 merged 12 commits into
developfrom
fix/nbp-failures-in-result
Oct 1, 2026
Merged

milten89 merged 12 commits into
developfrom
fix/nbp-failures-in-result

Conversation

@milten89

@milten89 milten89 commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Fixes BACKLOG P0 item 4 (ADR-0002: "Failures that escape the result"). This is the last P0 item.

Changes

Failure Before After
HttpRequestException while sending thrown ServiceUnavailableError with the original exception (Exception, and InnerException of ToException())
HttpRequestException / IOException while reading the body thrown ServiceUnavailableError
Timeout (OperationCanceledException while the caller's token is not cancelled) thrown new RequestTimeoutError (Timeout, Exception) / RequestTimeoutException
Caller cancellation thrown still thrown (OperationCanceledException)
Empty payload (null) UnknownError SerializationError
Undecodable charset in Content-Type thrown (InvalidOperationException) SerializationError
"rates": null, [null] items ArgumentException from mappers SerializationError
  • Both catch (Exception) blocks are gone; only specific exception types are caught.
  • Mappers reject bad API data with an internal InvalidPayloadException. The new NbpPayload.Map helper (used at all 25 client call sites) returns it as a SerializationError, writes an Error log and records it on the span. Array mappers now return IReadOnlyList<T>.

Behaviour changes (pre-1.0, put them in the release notes)

  • Body reads are now limited by HttpClient.Timeout. With ResponseHeadersRead, the timeout used to stop applying once the headers arrived. A short timeout combined with a large date range can now produce a RequestTimeoutError.
  • Code that wrapped calls in try/catch (HttpRequestException / TaskCanceledException) will no longer see those exceptions. It must check the result, or use EnsureSuccess(), which throws ServiceUnavailableException / RequestTimeoutException.
  • The empty-payload error is now SerializationError instead of UnknownError.
  • ServiceUnavailableError gains an optional exception constructor parameter. Source stays compatible; binary compatibility breaks.

Tests

  • Unit:
    • network failure while sending and while reading;
    • timeout while sending and while reading the body;
    • unknown charset;
    • a timeout while reading a 400 body (gives BadRequestError without the server message);
    • caller cancellation during that read (still throws);
    • NbpPayload, and the mappers throwing InvalidPayloadException.
  • WireMock: connection failure (server stopped), timeout, malformed JSON, [null] item, and "rates": null for the currency and table clients.
  • Real API: the manual tests (integrationTest.runsettings) pass, 54/54.

Verification

  • dotnet build OpenUrzednik.slnx: 0 errors (only the existing NU1902 warning).
  • dotnet test -f net10.0: Core 84/84, Nbp 454/454, IntegrationTests 47 passed / 7 skipped (manual tests).
  • dotnet format --verify-no-changes: clean.
  • reviewer agent: no blocking findings. I fixed all three should-fix points (charset, client-level rates: null tests, behaviour changes listed above) and most of the nits.

🤖 Generated with Claude Code

milten89 and others added 11 commits October 1, 2026 17:46
The live API returns table B rates in the same shape as table A
(table, currency, code, rates) without `country` and `symbol`, so the
required properties of CountryExchangeRatesDto made every GetCountry*
call end in a SerializationError.

- Remove GetCountry* methods, CountryExchangeRates and
  CountryExchangeRatesDto.
- Add an optional `TableType table = TableType.A` parameter to the
  mid-rate methods of INbpCurrencyExchangeRateClient, validated and
  recorded as the `nbp.table` span tag.
- Document the table B publication schedule (Wednesdays).
- Replace hand-written WireMock payloads with fixtures captured from
  the real API and add manual tests against the real API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The request and response were never disposed. With
ResponseHeadersRead, a non-success response kept its connection
until GC. Both are now disposed on every path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the status mapping in ADR-0002:
- 400 -> new BadRequestError carrying the NBP plain-text message
  (e.g. "400 BadRequest - Przekroczony limit 255 wyników / ..."),
  truncated to 500 characters.
- 401/403 -> UnauthorizedError.
- 5xx -> ServiceUnavailableError (previously UnknownError).
- Every HTTP error stores the status code in Metadata under
  OpenUrzednikError.StatusCodeMetadataKey.

Adds BadRequestError/BadRequestException to Core, an optional
statusCode parameter to the HTTP-related errors, and WireMock tests
for 400/401/403/404/429/5xx using bodies captured from the real API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SerializationError.ToException() returned the raw JsonException, so
EnsureSuccess() threw outside the library's exception hierarchy. It
now returns a new SerializationException (an OpenUrzednikException)
with the original exception as InnerException. The exception is
optional, so an empty payload can be reported without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read at most 500 characters of an error body through a StreamReader
instead of buffering the whole body, and log a guarded Debug message
when it can't be read. Assert exact status codes in the Core tests,
check truncation properly, and use one fixtures glob so the csproj
matches the table B branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into fix/nbp-failures-in-result

# Conflicts:
#	docs/BACKLOG.md
Follows ADR-0002: failures that can happen during normal execution
are returned in the result, not thrown.

- HttpRequestException (and IOException while reading the body) ->
  ServiceUnavailableError, which now keeps the original exception.
- Timeouts (OperationCanceledException while the caller's token is not
  cancelled) -> new RequestTimeoutError. HttpClient.Timeout now also
  covers reading the body, which ResponseHeadersRead left unbounded.
- Empty payload -> SerializationError (was UnknownError).
- Mappers reject bad API data (null rates, null items) with an internal
  InvalidPayloadException; NbpPayload.Map returns it as a
  SerializationError. Array mappers return IReadOnlyList<T>.
- Both catch (Exception) blocks in GetNbpAsync are gone.
- Caller cancellation still throws OperationCanceledException.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er client

Also: a clearer timeout message when HttpClient.Timeout is infinite, an
error.code tag on the HTTP span for network and timeout failures, a
payload-level name for null items, and tests for timeouts and caller
cancellation while reading a 400 body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from fix/nbp-http-status-mapping to develop October 1, 2026 19:56
develop now contains #13, #14, #16 and #17 as squash commits; the
result equals develop plus this PR's two commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@milten89
milten89 merged commit f9bb611 into develop Oct 1, 2026
8 checks passed
@milten89
milten89 deleted the fix/nbp-failures-in-result branch October 1, 2026 20:01
milten89 added a commit that referenced this pull request Oct 1, 2026
- P1: drop item 11, which #22 fixed (the #23 merge put it back); add
  item 27, the unconfirmed gold publication hour.
- P2: items 12 and 13 describe what is left after #16-#18; item 26
  (WireMock error paths per client) goes with item 12.
- P3: item 20 is no longer a vulnerability after #12; item 21 notes
  what is documented; new items 24 (Microsoft.Testing.Platform, blocks
  Dependabot #11) and 25 (outdated GITHUB-SETUP.md). Item 22 points
  to the new prose skills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
milten89 added a commit that referenced this pull request Oct 1, 2026
- P1: drop item 11, which #22 fixed (the #23 merge put it back); add
  item 27, the unconfirmed gold publication hour.
- P2: items 12 and 13 describe what is left after #16-#18; item 26
  (WireMock error paths per client) goes with item 12.
- P3: item 20 is no longer a vulnerability after #12; item 21 notes
  what is documented; new items 24 (Microsoft.Testing.Platform, blocks
  Dependabot #11) and 25 (outdated GITHUB-SETUP.md). Item 22 points
  to the new prose skills.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant