Skip to content

Fix range patterns and Integer comparisons against Response::Status - #847

Open
lineoffligbot wants to merge 1 commit into
httprb:mainfrom
hakanensari:fix/status-integer-comparison
Open

Fix range patterns and Integer comparisons against Response::Status#847
lineoffligbot wants to merge 1 commit into
httprb:mainfrom
hakanensari:fix/status-integer-comparison

Conversation

@lineoffligbot

Copy link
Copy Markdown
Contributor

Problem

Pattern matching a response status against a range silently never matches:

case response
in status: 500..599  # never matches, even for a 503
  retry
end

Equality patterns (in status: 429) work, which makes the range failure easy to miss: the pattern doesn't error, it just falls through. This is the exact usage recommended in the discussion that closed #839 (in status: 500..599 then retry), and it would apply equally to the StatusError deconstruction proposed there, since that delegates to the same Response#deconstruct_keys.

Root cause

deconstruct_keys exposes status: as a Response::Status. A range pattern evaluates (500..599) === status, which calls 500 <=> status. Integer#<=> resolves unfamiliar operands through the numeric coercion protocol (instance #coerce), not through #to_int. Status defines #<=>, #to_i, and #to_int, but no instance #coerce (the class-level Status.coerce is an unrelated factory), so 500 <=> status returns nil and the range never matches.

Fix

Implement the protocol:

def coerce(other)
  [other, code]
end

Now 500 <=> status returns -1 and (500..599) === status returns true, in case/in, case/when, and mixed sorting alike. I chose this over making deconstruct_keys emit status.to_i, which would change the deconstruction shape and break in status: {code: ...} matching.

Notes

  • Integer arithmetic also routes through coerce, so 1 + status now returns 504 instead of raising TypeError (status + 1 still raises, since Status defines no +). This seems consistent with the class already delegating to_i, to_int, and hash to the code and including Comparable.
  • Strings are unaffected. String doesn't use the numeric protocol, and in status: "400" still (correctly) never matches.

Tests pin all three layers: 500 <=> status, Range#===, and the user-facing in status: 500..599 on Response, plus the coerce pair itself. RBS signature included. Mutant kills 100% of mutations on the diff.

Integer#<=> requires the numeric coercion protocol (#coerce) to
compare with custom numeric types. Status lacked #coerce, so
`500 <=> status` returned nil and prevented Range#=== from matching,
causing pattern matching like `case response in status: 500..599` to
fail silently. This adds #coerce returning [other, code].

Co-authored-by: Hakan Ensari <hakanensari@gmail.com>
@hakanensari
hakanensari force-pushed the fix/status-integer-comparison branch from 0d67de4 to 4488a49 Compare August 16, 2026 14:14
@lineoffligbot

Copy link
Copy Markdown
Contributor Author

A note on the failing steep job: it is environment drift, not this patch. The Gemfile does not lock rbs (gem "rbs", ">= 4"), so CI resolves it fresh on every run. The last green run on main (July 14) resolved rbs 4.0.3; runs today resolve rbs 4.1.3, which introduces 8 new warnings in files this PR does not touch, and bundle exec rake steep fails identically on an untouched checkout of main. Happy to send a small separate PR fixing those warnings so CI goes green again.

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