Repository navigation
[HELDENPMI-132] Add HTTP error taxonomy (status + network) with overridable mapping - #216
Conversation
Add NxtHttpClient::TransientError marker + return_code-mapped subclasses under NxtHttpClient::Error: NetworkError, Timeout, ConnectionFailed, NameResolutionError, TlsError (transient) and CertificateError (cert verification, permanent). Consumers can retry_on NxtHttpClient::TransientError without per-client on(0) wiring. Code-0 (network failure) responses now raise the mapped error by default; opt out via config.raise_network_errors = false. All subclasses inherit from NxtHttpClient::Error, so rescue NxtHttpClient::Error keeps working. HELDENPMI-131
Extend the default error taxonomy from network failures to HTTP status codes, raised by default for unhandled non-success responses, so consumers stop hand-rolling the identical per-status on(...) boilerplate: - 4xx: ClientError + BadRequest/Unauthorized/Forbidden/NotFound/ UnprocessableEntity/TooManyRequests - 5xx: ServerError - code-0/network: NetworkError subclasses + CertificateError Retryable set is expressed by base classes: retry_on NxtHttpClient::Error::NetworkError, NxtHttpClient::Error::ServerError. CertificateError is a sibling of NetworkError (excluded); 429 is left to consumers. Remove the NxtHttpClient::TransientError marker — base classes replace it. Add a map_error(status, klass) DSL so a client can raise its own domain error for a status. New config flags raise_status_errors / raise_network_errors (default true) with opt-out; consumer callbacks always take precedence. HELDENPMI-132
Replace the default-on raise_status_errors / raise_network_errors flags with a single use_error_taxonomy flag defaulting to false, so a gem bump is fully backward compatible — existing behavior (and raise_response_errors) is unchanged until a consumer opts in. The taxonomy raises only via the callback_or_response fallback (never an on(:error) registration, which would collide with a consumer's own on(:error)), so consumer callbacks always take precedence. Restore raise_response_errors to its original generic-Error behavior. HELDENPMI-132
The json_response success handler parsed response_body unconditionally, so a blank/204 body raised JSON::ParserError when .body was read. Return nil for a blank body instead. Backward compatible — non-empty bodies parse as before. HELDENPMI-132
When both are set the legacy on(:error) handler raised a generic error first, shadowing the typed taxonomy. Skip registering it when use_error_taxonomy is on so the taxonomy fallback raises the typed error. Document raise_response_errors as superseded (kept for backward compatibility). HELDENPMI-132
HELDENPMI-132
…se_response_errors legacy Align the flag with the raise_ naming convention of raise_response_errors. HELDENPMI-132
There was a problem hiding this comment.
Pull request overview
Adds an opt-in, typed NxtHttpClient::Error taxonomy (HTTP status + network/code-0) to standardize retry/discard handling and error grouping, while keeping existing behavior unchanged unless explicitly enabled.
Changes:
- Introduces
NxtHttpClient::Error.from_response+ subclass taxonomy (ClientError/ServerError/NetworkErrorand specific status/return_code subclasses). - Adds
config.raise_error_taxonomyand amap_error(status, klass)DSL override, with taxonomy raising occurring only as a fallback when no consumer callback handled the response. - Adjusts JSON response parsing to return
nilfor empty/204 bodies; bumps version/changelog and adds specs for mappings + opt-in behavior.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/error_spec.rb | Adds unit coverage for response→error-class mapping and retryable hierarchy expectations. |
| spec/configuration_spec.rb | Adds coverage for 204 JSON body handling and taxonomy opt-in raising/precedence/map overrides. |
| README.md | Documents new raise_error_taxonomy behavior, retry pattern, and map_error; notes 204 JSON body behavior. |
| lib/nxt_http_client/version.rb | Bumps gem version to 2.2.0. |
| lib/nxt_http_client/error.rb | Implements error taxonomy classes + mapping logic (status_error_class / network_error_class). |
| lib/nxt_http_client/config.rb | Adds raise_error_taxonomy config default + documentation comments. |
| lib/nxt_http_client/client.rb | Implements fallback raising of mapped errors; prevents legacy raise_response_errors handler when taxonomy enabled; makes JSON parsing nil-safe for empty bodies. |
| lib/nxt_http_client/client_dsl.rb | Adds map_error + per-client error mapping inheritance via error_map. |
| Gemfile.lock | Updates lockfile version to 2.2.0. |
| CHANGELOG.md | Documents 2.2.0 changes (taxonomy + 204 JSON handling). |
Comments suppressed due to low confidence (1)
lib/nxt_http_client/client.rb:188
- Same concern as the taxonomy path:
error.to_hincludes request headers/options and body, which may contain secrets (Authorization/basic auth) and PII. Even if Sentry has server-side scrubbing, it’s safer to avoid sending these fields by default from the client library.
handler.on(:error) do |response|
error = NxtHttpClient::Error.new(response)
::Sentry.set_extras(http_error_details: error.to_h) if defined?(::Sentry)
raise error
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Validate map_error class is a NxtHttpClient::Error subclass (fail fast at registration instead of NoMethodError at raise time). - Clarify README/CHANGELOG that the taxonomy raises only on 4xx/5xx/code-0 (not 3xx). - Fix README: timeout_seconds (not timeouts). HELDENPMI-132
to_h is sent to Sentry via set_extras, so bearer tokens and basic-auth credentials were exposed. Redact them at the to_h boundary; the request_headers /request_options accessors stay raw for local debugging. HELDENPMI-132
…extras
Sentry deprecated Additional Data (set_extra) in favor of structured contexts.
Switch to set_context('http_error', error.to_h). Typhoeus isn't auto-instrumented
by Sentry, so the gem still attaches the details itself.
HELDENPMI-132
| def callback_or_response(response, response_handler) | ||
| callback = response_handler.callback_for_response(response) | ||
| callback && instance_exec(response, &callback) || response | ||
| return instance_exec(response, &callback) || response if callback |
There was a problem hiding this comment.
Since the method now returns response at the end by default do we still need the || response part here?
Are there situations where we would expect instance_exec(response, &callback) to return nil and would want to avoid returning it?
There was a problem hiding this comment.
We still need it to be able to continue having callbacks without a response returning the response.
HTTP header names are case-insensitive and HashWithIndifferentAccess does not normalize case, so a consumer-set 'authorization' (non-canonical case) leaked through Error#to_h. Match Authorization/Proxy-Authorization case-insensitively. HELDENPMI-132
Issue Link: HELDENPMI-132 (supersedes HELDENPMI-131)
Adds an opt-in typed error taxonomy under
NxtHttpClient::Error, so consumers stop hand-rolling the identicalon(400)/on(401)/on(404)/on(422)/on(5xx)/on(0)boilerplate just to get a usable taxonomy fordiscard_on/retry_onand Sentry grouping.Enabled with
config.raise_error_taxonomy = true; off by default, so a plain gem bump is fully backward compatible — existing behavior (andraise_response_errors) is unchanged until a consumer opts in (the per-consumer rollout step where its boilerplate is deleted). Supersedes the original network-only scope (HELDENPMI-131); the interimNxtHttpClient::TransientErrormarker is removed before it ships.Taxonomy (all inherit
NxtHttpClient::Error)HTTP status:
Error::BadRequestError::UnauthorizedError::ForbiddenError::NotFoundError::UnprocessableEntityError::TooManyRequestsError::ClientErrorError::ServerErrorNetwork / code 0 (
return_code-mapped):return_code:operation_timedoutError::Timeout:couldnt_connectError::ConnectionFailed:couldnt_resolve_host/_proxyError::NameResolutionError:ssl_connect_error+ non-cert:ssl_*Error::TlsErrorError::NetworkError:peer_failed_verification, …)Error::CertificateErrorRetry pattern
4xx (
ClientError) not retried.CertificateErroris a sibling ofNetworkError(not a child) → excluded.TooManyRequests/429 left to consumers.Overridable mapping
Inherited by subclasses; overrides the default for that status.
Precedence
The taxonomy raises only via the
callback_or_responsefallback — never anon(:error)registration (which would collide with a consumer's ownon(:error)). So a consumer'son(<code>)/on(:error)/on(:timed_out)always wins; you can enable the taxonomy for retries yet still handle e.g. a 404 inline.Backward compatibility
raise_error_taxonomy = false) → no behavior change on upgrade;raise_response_errorskeeps raising the genericNxtHttpClient::Error.rescue NxtHttpClient::Error/is_a?— all subclasses inherit from it.e.class ==) and error-message strings change → Sentry fingerprints regroup.Not in this PR (follow-ups)
raise_error_taxonomy = true.response_handlerboilerplate; delete the user-service localNxtHttpClient::TransientErrorinitializer (HELDENPMI-130).retry_on NetworkError, ServerErroracross admin-panel / claims / payment / insurance.Tests
Full suite green (84 examples): status→class + return_code→class mapping, opt-in default-off, raising when enabled, consumer-callback precedence, and overridable
map_error.