Repository navigation
Survive json 3.0.0, which broke every API response for users too - #39
Merged
Merged
Conversation
`json 3.0.0` dropped the second *positional* argument to `JSON.parse`;
options are keyword-only now. `Faraday::Response::Json#parse` still
calls `decoder.public_send(method_name, body, @parser_options || {})`,
so every JSON response from KSeF raised
Faraday::ParsingError: wrong number of arguments (given 2, expected 1)
**This reached users, not just CI.** The gemspec requires
`faraday "~> 2.0"`, faraday declares `json >= 0`, and faraday 2.14.3 is
the latest release — so `gem install ksef_client` produced a client that
could not read any response.
It arrived with no commit here. `Gemfile.lock` is gitignored by library
convention, so CI resolves fresh, and **rubocop 1.90.0 relaxed its own
`json ~> 2.3` pin to `>= 2.3`**, which is what let json 3.0.0 in. A
transitive development pin had been shielding the build by accident.
Measured in a clone bundled against json 3.0.0, before and after:
1580 examples / **163 failures** -> 1581 examples / **0 failures** with
the coverage gates enforced. Every one of the 163 was this one cause;
`lib/` and `tasks/` make no direct JSON calls at all, so faraday's two
middlewares are the gem's entire JSON surface, and the encode side is
unaffected.
Faraday's response middleware takes a caller-supplied decoder, read from
*inside* `parser_options`, so only the failing call changes.
`Ksef::HTTP::JsonDecoder.call(body, _options = nil)` provides it and
everything else the middleware does is retained — the content-type match
with its `;` split, so `application/problem+json; charset=utf-8` still
parses; the `to_str` guard, so a 204 does not raise; blank body to nil;
and `Faraday::ParsingError` wrapping, which published users may rescue.
It also leaves `connection_spec.rb`'s ordering pin — which names
`Faraday::Response::Json` by class — working untouched. Replacing the
middleware would have meant re-implementing six behaviours and rewriting
the spec that guards the ordering invariant.
Three traps, all measured, all recorded on the code:
- `parser_options` in a **frozen** constant raises `FrozenError` on the
first request, because the read is a destructive `delete`.
- One hash **shared** by two connections is emptied by the first, and the
second silently reverts to `::JSON.parse` and fails again.
- `decoder: JSON` takes faraday's `respond_to?(:load)` branch, and
`JSON.load(body, {})` under json 3 returns **nil** for a valid body
with no exception.
So the hash stays a fresh literal in `build` and the decoder is given in
array form. CLAUDE.md says not to tidy either.
`spec/ksef/http/retry_spec.rb` builds its own stack with a bare
`f.response :json` and needs the same wiring — without it, seven of the
163 survive a fix to `connection.rb` alone.
The guard example in `connection_spec.rb` asserts our decoder is the one
called. It is the only thing that can see the wiring go missing: under
json 2 the default decoder works, so removing it leaves every other
example green — which is precisely how this arrived unannounced.
Confirmed by removing the wiring under json 2.21.2: exactly that one
example fails.
`docs/REFERENCE.md` §4.9 carries the removal trigger. Faraday merged json
3 support in PR #1687 on 2026-08-12 and has not released it;
`JsonDecoder`'s optional second parameter satisfies both that call shape
and today's, so the shim needs no version check and can be deleted when
the floor moves.
No new dependency and no version pin: pinning `json` would fight
faraday's own `json >= 0` and rubocop's `json >= 2.3`, making this gem
uninstallable beside them.
Verified: `rake exit=0`, 1581 examples, line 100 / branch 98.76 /
method 100, RuboCop clean over 187 files, release gates 1597, green on
the 3.2.11 floor, and green under json 3.0.0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mainis currently broken, and so is every freshgem install. This is the fix.What happened
json 3.0.0dropped the second positional argument toJSON.parse.Faraday::Response::Json#parsestill passes one:So every JSON response from KSeF raised
Faraday::ParsingError: wrong number of arguments (given 2, expected 1).This reached users, not just CI. The gemspec requires
faraday "~> 2.0", faraday declaresjson >= 0, and faraday 2.14.3 is the latest release — sogem install ksef_clientproduced a client that could not read any API response.It arrived with no commit here.
Gemfile.lockis gitignored by library convention, so CI resolves fresh — and rubocop 1.90.0 relaxed its ownjson ~> 2.3pin to>= 2.3, which is what let json 3.0.0 in. A transitive development pin had been shielding the build by accident.Measured in a clone bundled against json 3.0.0, before and after in the same clone:
All 163 were this one cause.
lib/andtasks/make no direct JSON calls at all — faraday's two middlewares are the gem's entire JSON surface — and the encode side is unaffected.The fix
Faraday's response middleware takes a caller-supplied decoder, read from inside
parser_options, so only the failing call changes.Ksef::HTTP::JsonDecoderprovides it, and everything else the middleware does is retained: the content-type match with its;split (soapplication/problem+json; charset=utf-8still parses), theto_strguard (so a 204 doesn't raise onnil.strip), blank body →nil, andFaraday::ParsingErrorwrapping — which published users may already rescue.It also leaves
connection_spec.rb's ordering pin working untouched, since that namesFaraday::Response::Jsonby class. Replacing the middleware wholesale would have meant re-implementing six behaviours and rewriting the spec that guards the ordering invariant — I'd originally recommended that; it was the worse plan.Three traps, all measured
parser_optionsin a frozen constantFrozenErroron the first request — the read is a destructivedelete{}; second silently reverts to::JSON.parseand fails againdecoder: JSON(faraday'srespond_to?(:load)branch)JSON.load(body, {})returns nil for a valid body, no exceptionSo the hash stays a fresh literal in
buildand the decoder is given in array form. CLAUDE.md says not to tidy either, because "hoist it into a frozen constant" is exactly what this codebase's style would otherwise invite.Two things easy to miss
spec/ksef/http/retry_spec.rbbuilds its own stack with a baref.response :json— without the same wiring, 7 of the 163 survive a fix toconnection.rbalone.Nothing in
lib/requiresjsontoday; it has always arrived via faraday's middleware file, which masks the gap.JsonDecoderrequires it itself.The guard
connection_spec.rbnow asserts our decoder is the one actually called. It is the only thing that can see the wiring go missing: under json 2 the default decoder works, so removing it leaves every other example green — which is precisely how this arrived unannounced. Confirmed by removing the wiring under json 2.21.2: exactly that one example fails.A message expectation rather than a peek at faraday's
@kwargs— what matters is that the parse goes through our module, not how faraday stores the option.Removal trigger
docs/REFERENCE.md§4.9 records it. Faraday merged json 3 support in PR #1687 on 2026-08-12 and has not released it (2.14.3 shipped 2026-06-16).JsonDecoder's optional second parameter satisfies both that call shape (kwargs) and today's (two positionals), so the shim needs no version check and can be deleted when the gemspec floor moves.Why not a pin
Pinning
json < 3— as a runtime dep or in the Gemfile — would fight faraday's ownjson >= 0and rubocop ≥1.90'sjson >= 2.3. A Rails app on json 3 would become uninstallable alongside ksef_client. And a dev-only pin would leave users broken while CI went green, which is the pattern behind the skipped coverage gate.Verified
rake exit=0· 1581 examples, 0 failures · line 100 / branch 98.76 / method 100 · RuboCop clean over 187 files · release gates 1597 · green on the 3.2.11 floor · and green under json 3.0.0.Note on #38
PR #38 (workflow guards) is red for this reason alone and will go green once this lands. It needs no change of its own.