diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 66014d7d..1922dbbd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,48 +8,15 @@ on:
workflow_dispatch:
jobs:
- # Runs test:deep (the complete suite, not just the surface subset) inside
- # the exact same official Docker Hub `ruby` image used for local
- # validation of each version -- confirmed clean for every version here
- # across all 5 parsers (see .github/docker/ and the "Known Test Suite
- # Exceptions" section in the README for the few narrow, understood
- # exceptions on 3.0/3.1/3.2).
- #
- # This replaces an earlier ruby/setup-ruby-based matrix that ran directly
- # on the GitHub-hosted runner VM (Ubuntu). That base kept drifting out from
- # under local validation in ways that cost a lot of debugging time: Ubuntu
- # rolling ubuntu-latest forward from under a pinned Ruby version,
- # ruby-builder not always publishing a binary for every Ruby x Ubuntu
- # combination, and Ubuntu 22.04's ICU-enabled system libxml2 colliding with
- # libxml-ruby's C extension in a way that never reproduced locally against
- # Debian. Running inside the identical container we test against locally
- # makes "works locally" and "works in CI" the same claim by construction.
- #
- # This deliberately does NOT use the job-level `container:` key. GitHub
- # Actions runs container jobs by injecting its own Node.js runtime into the
- # container to execute JS-based steps like actions/checkout -- and most of
- # these official `ruby` images are old enough (Debian jessie/stretch-era
- # glibc) that the injected Node 24 binary can't even execute
- # ("version `GLIBC_2.27' not found"), failing checkout before a single test
- # runs. Confirmed empirically: every job up through Ruby 2.7 failed this
- # way when first tried with `container:`. Instead, checkout runs on the
- # (modern) runner VM as normal, and a plain `docker run` does the actual
- # bundle install / test run inside the target image -- exactly the pattern
- # already used below for 1.8.7/1.9.3/2.0.0, which never had this problem
- # because they never used `container:` in the first place.
- #
- # Exact patch-level tags are pinned (not the bare X.Y floating tag) to
- # match precisely what was validated -- these are all EOL/frozen lines on
- # Docker Hub so the floating tag would resolve to the same image anyway,
- # but pinning removes any ambiguity.
+ # Runs test:deep inside the exact same official Docker Hub `ruby` image
+ # used for local validation, via plain `docker run` (not the job-level
+ # `container:` key -- that injects a Node runtime these old images can't
+ # run). See CHANGELOG.md for the full architecture rationale.
test:
name: Ruby ${{ matrix.ruby }}
runs-on: ubuntu-latest
- # continue-on-error is set for 3.0/3.1/3.2 specifically: all three have
- # one confirmed, understood, non-soap4r-bug failure (test_exception in
- # marshaltestlib.rb compares a live Test::Unit::TestCase#inspect dump
- # that happens to render differently on this narrow range of Ruby
- # patch versions) -- see "Known Test Suite Exceptions" in the README.
+ # 3.0.7/3.1.7/3.2.11 have one known, non-soap4r-bug failure each -- see
+ # "Known Test Suite Exceptions" in the README.
continue-on-error: ${{ contains(fromJSON('["3.0.7", "3.1.7", "3.2.11"]'), matrix.ruby) }}
strategy:
fail-fast: false
@@ -81,29 +48,14 @@ jobs:
|| gem install bundler -v 1.17.3 --no-document 2>&1 \
|| gem install bundler -v 1.17.3 --no-ri --no-rdoc
bundle install
- # set -e above covers setup (nothing to test if that fails), but
- # must NOT apply to the loop itself: rake test:deep exits non-zero
- # on any test failure, and 3.0.7/3.1.7/3.2.11 have exactly one
- # guaranteed failure on every single parser (see README) -- under
- # set -e that killed the script after oxparser alone, silently
- # skipping nokogiri/libxml/oga/rexml every run without anyone
- # noticing (confirmed: CI logs showed only one parser ever ran
- # for those versions). Track failures instead and exit at the end
- # so every parser actually runs regardless of earlier ones.
+ # set -e must not wrap the loop itself: rake test:deep exits
+ # non-zero per failure, and some versions have exactly one
+ # guaranteed failure on every parser (see README) -- track
+ # failures instead so every parser still runs. See CHANGELOG.md.
set +e
FAILED=0
for parser in oxparser nokogiriparser libxmlparser ogaparser rexmlparser; do
- # All 5 parsers run as separate `rake test:deep` invocations
- # sequentially in this one long-lived container, sharing a
- # hardcoded port (17171) used throughout the test suite --
- # confirmed via CI logs (Ruby 2.7.8, run 28854082576) that
- # something can be left
- # bound to that port across that process boundary for minutes,
- # well past the widened retry budget in test/testutil.rb (60
- # tries/1s). Never reproduced locally, and not root-caused
- # (suspected a WEBrick/CGI-handler subprocess not fully
- # released), so force-clear the port before every parser
- # regardless of cause rather than keep guessing at timing.
+ # Force-clear the shared test port between runs -- see CHANGELOG.md.
fuser -k -n tcp 17171 >/dev/null 2>&1 || true
echo "--- SOAP4R_PARSERS=$parser ---"
SOAP4R_PARSERS=$parser bundle exec rake test:deep
@@ -113,17 +65,10 @@ jobs:
exit $FAILED
'
- # HTTP client backend is independently selectable too (see
- # lib/soap/httpbackend.rb, same mechanism as SOAP4R_PARSERS above).
- # httpclient is already exercised by every run of the loop above (it's
- # the default), so this only adds the net_http fallback -- the one
- # backend that, prior to this, no test in the suite could ever
- # actually reach (it's only selected when both httpclient and
- # http-access2 fail to load, and this project's Gemfile always
- # installs httpclient). http-access2 itself is left out: it's no
- # longer published on RubyGems.org at all, so there's nothing
- # installable to sweep. A single parser (oxparser) is enough here --
- # this is about exercising the HTTP backend, not the XML parser.
+ # HTTP client backend is independently selectable (lib/soap/httpbackend.rb).
+ # httpclient is already exercised above (the default); this adds the
+ # net_http fallback. One parser (oxparser) is enough -- this tests the
+ # HTTP backend, not the XML parser.
- name: Run net_http HTTP client backend (oxparser)
run: |
docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c '
@@ -138,20 +83,72 @@ jobs:
SOAP4R_HTTP_CLIENTS=net_http SOAP4R_PARSERS=oxparser bundle exec rake test:deep
'
- # 1.9.3 and 2.0.0 have official Docker Hub images, but every tag in both
- # lines was published in the legacy Docker manifest v1 schema and is no
- # longer pullable at all -- confirmed empirically ("not implemented: media
- # type ... manifest.v1+prettyjws ... no longer supported since containerd
- # v2.1") against every 1.9.x/2.0.x tag tried (bare, -slim, every specific
- # patch level). GitHub-hosted runners are on similarly current
- # Docker/containerd and hit the same wall, so `container: image: ruby:1.9.3`
- # is a dead end here, not just a local quirk. Built from source instead via
- # rbenv/ruby-build on Debian bullseye (Dockerfile.legacy) -- confirmed
- # clean across all 5 parsers for both versions, including libxmlparser
- # (Debian bullseye's libxml2-dev doesn't have the ICU/`bool`-collision
- # problem ubuntu-22.04's does) and ogaparser (oga's ruby-ll dependency
- # installs and works fine on both, contrary to earlier assumptions that it
- # needed Ruby >= 2.1 -- that constraint turned out to only apply to 1.8).
+ # curb is opt-in (Gemfile group :http_curb, optional: true) and gated
+ # to Ruby >= 2.4; below that floor this step skips explicitly rather
+ # than hard-failing the job. Uses BUNDLE_WITH (env var), not `bundle
+ # config set with "..."` -- the latter is Bundler 2.x-only syntax and
+ # silently no-ops on the Bundler 1.17.3 every Ruby below 3.2 falls
+ # back to. See CHANGELOG.md for both bugs in full.
+ #
+ # continue-on-error scoped to just this step (not the whole job) for
+ # 2.4.10/2.5.9 specifically: their Docker images ship a libcurl/OpenSSL
+ # pair that fails CA chain validation on test_ca_verification/
+ # test_ciphers -- a known, environment-specific CANTFIX (see "Known
+ # Test Suite Exceptions" in README.md), not a real regression. Every
+ # other step for these two versions (main parser sweep, net_http,
+ # faraday) is left fully enforced.
+ - name: Run curb HTTP client backend (oxparser)
+ continue-on-error: ${{ contains(fromJSON('["2.4.10", "2.5.9"]'), matrix.ruby) }}
+ run: |
+ docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c '
+ set -e
+ if ! ruby -e "exit(RUBY_VERSION.to_f >= 2.4 ? 0 : 1)"; then
+ echo "Skipping: curb is gated to Ruby >= 2.4 in the Gemfile."
+ exit 0
+ fi
+ export BUNDLE_WITH="http_curb"
+ apt-get update -qq && apt-get install -y -qq psmisc libcurl4-openssl-dev pkg-config build-essential
+ mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
+ gem install bundler --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-ri --no-rdoc
+ bundle install
+ fuser -k -n tcp 17171 >/dev/null 2>&1 || true
+ SOAP4R_HTTP_CLIENTS=curb SOAP4R_PARSERS=oxparser bundle exec rake test:deep
+ '
+
+ # Faraday's default adapter (:net_http) doesn't get its own step --
+ # net_http above already covers that path end-to-end. :typhoeus
+ # (libcurl-based, via ethon/FFI) is the adapter this project tests
+ # against instead, and gated to Ruby >= 2.6 same as Faraday itself
+ # (see Gemfile/CHANGELOG.md); below that floor this step skips
+ # explicitly rather than hard-failing the job.
+ - name: Run faraday HTTP client backend, typhoeus adapter (oxparser)
+ run: |
+ docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c '
+ set -e
+ if ! ruby -e "exit((RUBY_VERSION.to_f >= 2.6 && RUBY_PLATFORM !~ /java/) ? 0 : 1)"; then
+ echo "Skipping: Faraday is gated to Ruby >= 2.6 in the Gemfile."
+ exit 0
+ fi
+ export BUNDLE_WITH="http_faraday"
+ apt-get update -qq && apt-get install -y -qq psmisc libcurl4-openssl-dev
+ mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
+ gem install bundler --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-document 2>&1 \
+ || gem install bundler -v 1.17.3 --no-ri --no-rdoc
+ bundle install
+ fuser -k -n tcp 17171 >/dev/null 2>&1 || true
+ SOAP4R_HTTP_CLIENTS=faraday SOAP4R_FARADAY_ADAPTER=typhoeus SOAP4R_PARSERS=oxparser bundle exec rake test:deep
+ '
+
+ # :patron was tried as a third spot-check adapter and dropped from CI:
+ # a real upstream patron bug, not ours to fix -- see "Known Test Suite
+ # Exceptions" in README.md.
+
+ # 1.9.3/2.0.0's official Docker Hub images use the legacy manifest v1
+ # schema and are no longer pullable at all. Built from source instead via
+ # rbenv/ruby-build on Debian bullseye (Dockerfile.legacy). See CHANGELOG.md.
test-legacy-source-build:
name: Ruby ${{ matrix.ruby }} (built from source via rbenv)
runs-on: ubuntu-latest
@@ -180,9 +177,7 @@ jobs:
|| gem install bundler -v 1.17.3 --no-ri --no-rdoc
rbenv rehash 2>&1 || true
bundle install
- # See the `test` job above for why set -e must not wrap this loop,
- # and why each iteration force-clears port 17171 first (psmisc is
- # baked into this Dockerfile already).
+ # See the `test` job above re: set -e and port-clearing.
set +e
FAILED=0
for parser in oxparser nokogiriparser libxmlparser ogaparser rexmlparser; do
@@ -195,30 +190,11 @@ jobs:
exit $FAILED
'
- # Ruby 1.8.7 has no official Docker Hub image at all (the "ruby" library
- # starts at 1.9) and no ruby-build definition with OpenSSL handling, so
- # rbenv/ruby-build can't get this one for free the way 1.9.3/2.0.0 above
- # do. Dockerfile.legacy187 builds it from source against a vendored
- # OpenSSL 1.0.2u instead (confirmed working: openssl.so builds, and
- # OpenSSL::SSL::SSLContext.new instantiates correctly at runtime).
- #
- # oga is intentionally excluded from the parser loop: it was never
- # installed for Ruby <= 1.8 in the first place (its ruby-ll dependency
- # needs Ruby >= 2.1 -- confirmed this constraint is real for 1.8, unlike
- # 1.9.3 above where ruby-ll installs and works fine), so xsd/xmlparser.rb's
- # cascade correctly reports it unavailable -- that's an expected,
- # non-zero-exit "failure", not a real one, so it's left out here rather
- # than treated as a test result.
- #
- # continue-on-error is set: this job has two confirmed, understood,
- # non-soap4r-bug failure categories -- Kernel#singleton_class not
- # existing until Ruby 1.9.2 (see README), and a deeper interaction
- # between Logger::Application and the CGI test suite's stripped-ENV
- # subprocess spawn that wasn't fully root-caused (narrowed to
- # lib/soap/rpc/cgistub.rb#run's `logger` call raising NameError only in
- # that specific spawned-subprocess context; not reproducible in an
- # isolated minimal case) -- both confined to CGI-based tests
- # (test_calc_cgi, test_authheader_cgi) on this one Ruby version.
+ # Ruby 1.8.7 has no official Docker Hub image; Dockerfile.legacy187 builds
+ # it from source against a vendored OpenSSL 1.0.2u. oga is excluded from
+ # the parser loop (its ruby-ll dependency genuinely needs Ruby >= 2.1).
+ # continue-on-error: known, understood failures -- see "Known Test Suite
+ # Exceptions" in README.md / CHANGELOG.md.
test-ruby-1_8_7:
name: Ruby 1.8.7 (built from source, vendored OpenSSL)
runs-on: ubuntu-latest
@@ -238,12 +214,7 @@ jobs:
|| gem install bundler -v 1.17.3 --no-document 2>&1 \
|| gem install bundler -v 1.17.3 --no-ri --no-rdoc
bundle install
- # See the comment on the `test` job above for why set -e must not wrap this
- # loop -- 1.8.7 has guaranteed failures on every parser
- # (singleton_class), so this job in particular was only ever
- # actually testing oxparser before this fix. Also force-clears
- # port 17171 before each iteration (psmisc is baked into this
- # Dockerfile already) -- see the comment on the `test` job above for why.
+ # See the `test` job above re: set -e and port-clearing.
set +e
FAILED=0
for parser in oxparser nokogiriparser libxmlparser rexmlparser; do
@@ -256,23 +227,10 @@ jobs:
exit $FAILED
'
- # JRuby: nokogiri, oga, and rexml all work correctly. ox and libxml-ruby
- # have no viable JRuby-compatible gem at all (ox has never had a JRuby
- # port; libxml-jruby's one and only release is from 2010 and calls a
- # since-removed JRuby API) -- both are excluded from the parser loop for
- # the same reason oga is excluded from the 1.8.7 job above.
- #
- # Runs inside the official jruby Docker Hub image (same as local
- # validation) via plain `docker run` rather than the job-level `container:`
- # key -- see the long comment on the `test` job above for why: GitHub
- # Actions injects its own Node.js runtime into `container:` jobs to run
- # actions/checkout, and that crashed here too (exit 127) against this
- # image's libc.
- #
- # continue-on-error is set because the remaining JRuby-specific behavioral
- # differences are known and confined to environment/dependency quirks
- # rather than soap4r-ng bugs -- see "Known Test Suite Exceptions" in the
- # README for the root-cause breakdown.
+ # JRuby: ox and libxml-ruby have no viable JRuby gem, so both are excluded
+ # from the parser loop. continue-on-error: remaining differences are known
+ # environment/dependency quirks, not soap4r-ng bugs -- see "Known Test
+ # Suite Exceptions" in README.md.
test-jruby:
name: JRuby ${{ matrix.ruby }}
runs-on: ubuntu-latest
@@ -290,22 +248,12 @@ jobs:
run: |
docker run --rm -v "${{ github.workspace }}":/repo-ro:ro jruby:${{ matrix.ruby }} bash -c '
set -e
- # The official jruby image does not ship git, but the Gemfile
- # pulls rubyjedi-testunitxml straight from its GitHub repo, so
- # bundle install needs it -- confirmed missing locally too
- # (apt-get install was already required there, just got dropped
- # when this job was rewritten to a plain docker run). psmisc
- # (fuser) is for the port-clearing step below.
+ # git: Gemfile pulls rubyjedi-testunitxml from GitHub. psmisc: port-clearing below.
apt-get update -qq && apt-get install -y -qq git psmisc
mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app
gem install bundler --no-document
bundle install
- # See the comment on the `test` job above for why set -e must not wrap this
- # loop -- JRuby has the same guaranteed-every-parser failure
- # count (see README), so this job was also only ever actually
- # testing nokogiriparser before this fix. Also force-clears port
- # 17171 before each iteration for the same reason as the `test`
- # job.
+ # See the `test` job above re: set -e and port-clearing.
set +e
FAILED=0
for parser in nokogiriparser ogaparser rexmlparser; do
@@ -351,15 +299,9 @@ jobs:
}
};
- // core.summary.addTable() emits an actual
element rather
- // than hand-rolled markdown pipe syntax. That matters here: a
- // markdown table placed immediately after addHeading()'s
- // HTML with no blank line in between doesn't get parsed as a
- // table at all (GFM requires a fresh block boundary before table
- // syntax) -- confirmed on a live run, where the whole thing
- // rendered as one literal-pipe-character paragraph instead of a
- // table. An HTML table sidesteps that block-adjacency rule
- // entirely.
+ // addTable() emits a real
, not markdown pipe syntax --
+ // a markdown table right after addHeading()'s doesn't
+ // render as a table at all (GFM block-adjacency rule). See CHANGELOG.md.
const rows = [
[
{ data: 'Job', header: true },
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..930f9702
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,406 @@
+# Changelog
+
+Detailed rationale and investigation notes for non-obvious dependency and
+compatibility decisions. Kept separate from README.md/code comments so
+those stay skimmable; this is the place to look for the "why" in full.
+
+## Unreleased
+
+### Faraday gated to Ruby >= 2.6
+
+`gem 'faraday'` was previously unconstrained, which resolved *some* old
+release for every Ruby down to 1.9.3 -- but that old-Faraday territory
+turned out to be broken, not just untested:
+
+- Below Faraday 1.10 (resolved as far up as Ruby 2.3.8), net_http support
+ is bundled directly into the main `faraday` gem with no separate
+ `faraday-net_http` package. `lib/soap/faradayClient.rb`'s
+ `require "faraday/#{adapter}"` convention assumes the modern
+ split-into-per-adapter-gems architecture, so it can't load any adapter
+ at all there -- an architecture mismatch, not a version-selection bug.
+- Ruby 2.4.10/2.5.9 resolve Faraday 1.10.6, which does carry a real
+ `faraday-net_http` dependency and gets further, but hits a distinct bug:
+ `TypeError: wrong argument (String)! (Expected kind of
+ OpenSSL::X509::Certificate)` deep inside `Net::HTTP#connect`, from
+ `faradayClient.rb` passing a raw file-path string where curb's own
+ client already correctly converts to a parsed certificate object.
+
+Decision: Faraday is a modern-transport enhancement, not a legacy
+capability worth carrying forward two Ruby versions for. Gated to
+`>= 2.6` outright, matching `faraday-typhoeus`'s own real floor
+(confirmed: its oldest RubyGems release needs Ruby >= 2.5's `logger`, and
+the next release requires Ruby >= 2.6 directly). `faraday-patron` bumped
+to the same floor since an adapter gem is inert without its host.
+
+`curb` has the equivalent floor for an unrelated reason: its C extension
+references `CURL_SSLVERSION_MAX_*` constants absent before libcurl 7.54.0.
+The official `ruby:2.2.10`/`ruby:2.3.8` Docker Hub images' system
+`libcurl-dev` predates that (confirmed hard compile failure, not a
+graceful skip); `ruby:2.4.10`'s image is new enough.
+
+CI had a latent bug matching both of these: `bundle config set with "..."`
+is Bundler 2.x-only syntax. Bundler 1.17.3 (what every Ruby < 3.2 falls
+back to) has no `set` verb and silently creates a bogus config key
+literally named `"set"` -- meaning curb/faraday were never actually
+exercised in CI below Ruby 3.2. Fixed with the `BUNDLE_WITH="group1:group2"`
+env var instead (confirmed identical behavior on both Bundler
+generations), exported (not just prefixed on the `bundle install` line)
+since `bundle exec`'s own `Bundler.setup` re-checks group inclusion
+independently for that invocation too. Both `ci.yml` steps also needed
+explicit skip-and-exit-0 logic below their respective floors --
+previously a Ruby version below the floor would hard-fail the whole job
+(`RuntimeError: HTTP client backend not found`) rather than skip cleanly.
+
+### Ox pin split three ways (dropping htmlentities where possible)
+
+The Gemfile pins Ox differently depending on Ruby version because
+compatibility and entity-decoding behavior don't move together, or even
+monotonically, across releases -- confirmed by installing every Ox
+release from 2.4.5 through 2.14.28 across Ruby 1.8.7, 1.9.3, 2.0.0, and
+2.1.10 and testing both (a) whether it loads and (b) whether it decodes
+named HTML entities (`…`, `—`) natively via
+`:convert_special => true`:
+
+- Entity decoding never improves anywhere in 2.4.5-2.8.0 (identical,
+ undecoded output throughout every version tested). It first improves at
+ **2.13.4** (2020-09-12), which decodes the same named-entity set
+ htmlentities does, byte-identical on direct comparison.
+- **2.14.7** is the first release to call `rb_utf8_str_new`, a symbol not
+ exported by Ruby's shared lib before 2.2 -- `undefined symbol:
+ rb_utf8_str_new` at native-ext load time on Ruby 2.0.0/2.1.10. Oddly,
+ this does *not* reproduce on Ruby 1.9.3 even with the same Ox version --
+ Ox's extconf must feature-detect the symbol differently depending on
+ which Ruby's headers are present at compile time, so the boundary isn't
+ a simple "older Ruby is safer" gradient; each version needs testing
+ directly rather than assumed from a neighbor.
+- **2.14.6** is therefore the newest release confirmed to work, with
+ correct entity decoding, on 1.9.3, 2.0.0, *and* 2.1.10 uniformly. Pinned
+ exactly (`= 2.14.6`), since the very next patch release breaks it.
+- **Ruby 1.8.7** can't use 2.14.6 at all: its `extconf.rb` fails outright
+ with `undefined method 'slice!' for nil:NilClass`, an mkmf/extconf
+ incompatibility with 1.8.7's build tooling, unrelated to the
+ `rb_utf8_str_new` issue above. Falls back to the older `= 2.4.5` pin,
+ which does compile and load there but only decodes the basic 5 XML
+ entities -- htmlentities is still required, and only there.
+ - Pinned exactly (`= 2.4.5`, not `~>`): later 2.4.x patches (confirmed
+ with 2.4.13) hit a *second*, different unresolvable-symbol crash
+ (`RSTRUCT_GET`) on Ruby 2.0.0/2.1.10 -- though notably *not* on
+ 1.8.7/1.9.3, the same non-monotonic pattern as the 2.14.x boundary
+ above. Moot now that 1.8.7 is the only Ruby left in this branch, but a
+ floating patch-level constraint would silently regress this the
+ moment that changes.
+ - `htmlentities 4.3.1` pinned exactly there too: 4.3.3+ hard-fails on
+ 1.8.7 with `NameError: uninitialized constant
+ HTMLEntities::Decoder::Encoding` (references Ruby's `Encoding` class,
+ introduced in 1.9, absent on 1.8.7).
+
+A CRLF regression was found and fixed along the way, incidental to this
+investigation: `lib/xsd/xmlparser/oxparser.rb`'s no-decoder branch used
+`:skip => :skip_return`, which discards `\r` -- unrelated to entity
+decoding, but broke CRLF round-tripping (`test_string_crlf`) for anyone
+running without htmlentities installed. Changed to `:skip_none`
+unconditionally.
+
+Regression matrix re-run after these changes: 1.9.3, 2.0.0, and 2.1.10 all
+green; 1.8.7 shows only its pre-existing, already-documented exceptions
+(see "Known Test Suite Exceptions" below) -- no new failures introduced.
+**This re-run did not include Ruby >= 2.2** (its `gem 'ox'` branch wasn't
+touched by this change, so it seemed out of scope at the time) -- see the
+correction directly below, found days later while reordering these same
+Gemfile branches for an unrelated reason.
+
+### Correction: htmlentities still required on Ruby 2.2.x - 2.6.x
+
+The "htmlentities is dead weight above 1.9.3" conclusion above was wrong
+for part of that range. `gem 'ox'` unconstrained (the `>= 2.2` branch)
+resolves whatever the newest Ox release is for the running Ruby -- and Ox
+added a hard `Ruby >= 2.7.0` requirement to its own gemspec starting at
+2.14.15. That means Ruby 2.2.x-2.6.x are stuck resolving **2.14.14** (the
+last release before that floor), while Ruby >= 2.7 gets 2.14.15+.
+
+2.14.14 segfaults inside `Ox.sax_parse`'s `:convert_special => true` path
+on complex documents -- confirmed via a real crash in `test_mapping.rb`
+(`[BUG] Segmentation fault` at `oxparser.rb:33`), reproduced identically on
+Ruby 2.2.10, 2.3.8, 2.4.10, and 2.6.10, while a bare, simple XML string
+through the same code path does *not* crash (so it's data/complexity
+dependent, not a trivial always-fails bug -- a quick smoke test wouldn't
+have caught it). Ruby 2.7.8 (resolving Ox 2.14.28) and Ruby 3.0.7/3.4.10/
+4.0.5 all run the same test cleanly.
+
+`htmlentities` avoids the bug entirely by design, not by luck:
+`oxparser.rb` only passes `:convert_special => true` when no decoder is
+present; with htmlentities installed, Ox is called without that option and
+never enters the buggy path. So for Ruby 2.2.x-2.6.x, htmlentities is a
+required workaround, not an optional speed boost -- `gem 'htmlentities'`
+was restored there (Gemfile), unconstrained Ox unconstrained-and-safe only
+from Ruby >= 2.7.
+
+This was only caught because a later, unrelated task (reordering the
+Gemfile's version-gate branches newest-first) prompted a fresh full-matrix
+re-run that happened to include Ruby 2.2.10 for the first time since the
+original htmlentities change. The lesson: a version-gate boundary that
+touches one branch's *dependency count* (adding/removing a gem) can change
+behavior in a sibling branch that looks untouched, if both branches share
+downstream code (`oxparser.rb`) that behaves differently based on which
+gems happen to be present -- worth re-running the full matrix after any
+Gemfile change that touches a shared code path, not just the Ruby
+versions whose own branch changed.
+
+### CI: Docker-per-version architecture, not ruby/setup-ruby
+
+`ci.yml`'s `test` job runs each Ruby version via a plain `docker run` of
+the exact same official `ruby:X.Y.Z` image used for local validation,
+rather than a `ruby/setup-ruby`-based matrix on the GitHub-hosted runner
+VM directly, and rather than the job-level `container:` key.
+
+- The earlier `ruby/setup-ruby` approach kept drifting from local
+ validation in ways that cost real debugging time: `ubuntu-latest`
+ rolling forward under a pinned Ruby version, `ruby-builder` not always
+ publishing a binary for every Ruby x Ubuntu combination, and Ubuntu
+ 22.04's ICU-enabled system libxml2 colliding with `libxml-ruby`'s C
+ extension in a way that never reproduced locally against Debian. Running
+ the identical container makes "works locally" and "works in CI" the same
+ claim by construction.
+- `container:` was tried and rejected: GitHub Actions injects its own
+ Node.js runtime into container jobs to run JS-based steps like
+ `actions/checkout`, and most of these official `ruby` images are old
+ enough (Debian jessie/stretch-era glibc) that the injected Node 24
+ binary can't execute (`version 'GLIBC_2.27' not found`), failing
+ checkout before a single test runs. Confirmed empirically: every job up
+ through Ruby 2.7 failed this way. Checkout instead runs on the runner VM
+ as normal, and a plain `docker run` does the actual `bundle install`/test
+ run inside the target image.
+- Exact patch-level tags (e.g. `2.4.10`, not `2.4`) are pinned to match
+ precisely what was locally validated, even though these are all
+ EOL/frozen lines where the floating tag would resolve identically.
+
+### CI: BUNDLE_WITH bug (curb/faraday silently never tested below Ruby 3.2)
+
+`bundle config set with "http_curb"` is Bundler 2.x-only syntax. Every
+Ruby below 3.2 in this matrix falls back to Bundler 1.17.3 (the last
+release installable there), whose `config` CLI has no `set` verb at all --
+under 1.17.3 that command silently creates a bogus config key literally
+named `"set"` (value `"with http_curb"`) instead of configuring the `with`
+group list. Confirmed empirically: `bundle config` afterward showed the
+bogus key, and `SOAP4R_HTTP_CLIENTS=curb` then failed with "HTTP client
+backend not found" against a gem that was never installed. Meaning: every
+curb/faraday CI step below Ruby 3.2 had been silently testing nothing.
+
+Fixed with the `BUNDLE_WITH="group1:group2"` env var instead -- the same
+underlying mechanism a persisted `bundle config` writes to, confirmed
+working identically on both Bundler generations. It must be `export`ed
+(not just prefixed on the `bundle install` line): `bundle install` alone
+happily installs an optional group's gems to disk even without
+`BUNDLE_WITH` set for a later command, but `bundle exec`'s own
+`Bundler.setup` excludes `:optional => true` groups from the load path by
+default unless told to include them for that invocation too -- confirmed
+empirically that a `bundle exec` immediately after a successful,
+curb-installing `bundle install` still raised `LoadError` on `require
+'curb'` without `BUNDLE_WITH` set again.
+
+Both the curb and faraday CI steps also needed explicit skip-and-exit-0
+logic below their respective Gemfile floors (Ruby >= 2.4 / >= 2.6): without
+it, a matrix entry below the floor would hard-fail the whole job
+(`RuntimeError: HTTP client backend not found`) under `set -e` with no
+`continue-on-error` at that granularity, rather than skip cleanly.
+
+### CI: port 17171 clearing between parser runs
+
+All parsers/backends run as separate `rake test:deep` invocations
+sequentially in one long-lived container, sharing a hardcoded port (17171)
+used throughout the test suite. Confirmed via CI logs (Ruby 2.7.8, run
+28854082576) that something can stay bound to that port across a process
+boundary for minutes, well past the widened retry budget in
+`test/testutil.rb` (60 tries/1s). Never reproduced locally, and not fully
+root-caused (suspected a WEBrick/CGI-handler subprocess not fully
+released) -- `fuser -k -n tcp 17171` runs before every iteration
+regardless of cause rather than keep guessing at timing.
+
+Relatedly, `set -e` must not wrap the parser loop itself (only the setup
+steps before it): `rake test:deep` exits non-zero on any test failure, and
+several Ruby versions have exactly one guaranteed failure on every parser
+(see "Known Test Suite Exceptions" below) -- under `set -e` that killed the
+script after the first parser alone, silently skipping the rest without
+anyone noticing (confirmed: CI logs showed only one parser ever ran for
+those versions). Failures are tracked in a variable and the loop always
+runs to completion instead.
+
+### CI: summary job's GFM table-adjacency gotcha
+
+The `summary` job uses `core.summary.addTable()` (an actual HTML
+`
`) rather than hand-rolled markdown pipe syntax, because a
+markdown table placed immediately after `addHeading()`'s `` HTML with
+no blank line in between doesn't get parsed as a table at all (GitHub
+Flavored Markdown requires a fresh block boundary before table syntax) --
+confirmed on a live run, where the whole thing rendered as one
+literal-pipe-character paragraph instead of a table.
+
+### CI: legacy Ruby build strategy (1.9.3, 2.0.0, 1.8.7)
+
+1.9.3 and 2.0.0 have official Docker Hub images, but every tag in both
+lines was published in the legacy Docker manifest v1 schema and is no
+longer pullable at all (confirmed empirically: `not implemented: media
+type ... manifest.v1+prettyjws ... no longer supported since containerd
+v2.1`, against every tag tried). GitHub-hosted runners hit the same wall.
+Built from source instead via rbenv/ruby-build on Debian bullseye
+(`Dockerfile.legacy`) -- confirmed clean across all 5 parsers for both
+versions, including libxmlparser (bullseye's libxml2-dev doesn't have
+Ubuntu 22.04's ICU/`bool`-collision problem) and ogaparser (oga's ruby-ll
+dependency installs and works fine on both, unlike 1.8.7 below).
+
+1.8.7 has no official Docker Hub image at all (the `ruby` library starts
+at 1.9) and no ruby-build definition with OpenSSL handling, so
+rbenv/ruby-build can't get it for free. `Dockerfile.legacy187` builds it
+from source against a vendored OpenSSL 1.0.2u instead (confirmed working:
+`openssl.so` builds, `OpenSSL::SSL::SSLContext.new` instantiates
+correctly). `oga` is intentionally excluded from its parser loop: its
+`ruby-ll` dependency genuinely needs Ruby >= 2.1 on this version (unlike
+1.9.3 above), so it's correctly never installed there.
+
+### HTTP client backend rollout (curb/faraday, wiredump parity, TLS trust)
+
+`http-access2` (httpclient's old name, same author) is no longer published
+on RubyGems.org at all; retired from the backend cascade for the same
+reason. See git history for the adapter that used to sit here.
+
+Before `SOAP4R_HTTP_CLIENTS` existed as an independently selectable env
+var, nothing in the test suite could ever actually reach the `net_http`
+fallback -- this project's Gemfile always installs `httpclient`, so the
+cascade never had a reason to fall through to it. Making backends
+independently selectable surfaced (and fixed) a real bug in the process:
+`SOAP::NetHttpClient`'s wiredump output duplicated request/response bodies
+and was missing the raw request-line/header block entirely, corrupting
+any test or tool parsing `wiredump_dev` output.
+
+Several tests (WSDL-driven codegen round-trips, request-envelope
+assertions, ASP.NET-handler interop) used to be gated to `httpclient` only
+because they parsed `wiredump_dev` output assuming its specific block
+layout. That layout turned out to already be backend-neutral by design
+once `NetHttpClient`/`CurbClient`/`FaradayClient` were all built to mirror
+it -- confirmed by simply removing the gates and running the suite
+unmodified under every backend. The duplicated parsing logic across those
+test files is now consolidated into
+`TestUtil.parse_wiredump_request_body`/`parse_wiredump_response_body`
+(`test/testutil.rb`).
+
+`test/soap/ssl/test_ssl.rb` runs its SSL config-loading coverage against
+every SSL-capable backend now, not just httpclient: `test_ca_verification`
+and `test_ciphers` (rewritten to expect each backend's own real exception
+class -- `OpenSSL::SSL::SSLError` for httpclient, `Curl::Err::CurlError`
+for curb, `Faraday::Error` for Faraday; confirmed empirically, since
+test-unit's `assert_raise` wants an exact class match).
+`test_options`/`test_verification`/`test_property` stay httpclient-only:
+they're built around `ssl_config.verify_callback`, and neither
+libcurl-based backend (curb, or Faraday on typhoeus/patron) exposes a
+per-certificate Ruby callback hook at all -- confirmed against both
+libraries' public APIs. Faraday's own adapter list is deliberately not
+swept exhaustively in CI -- `lib/soap/faradayClient.rb` is what's under
+test, and sweeping every adapter Faraday supports would mostly re-test
+Faraday's own correctness. `:typhoeus` was chosen as the one CI adapter
+because it's also the one people actually reach for in practice (28
+reverse dependencies on RubyGems vs. `faraday-patron`'s 3, per [Ruby
+Toolbox](https://www.ruby-toolbox.com/projects/faraday-typhoeus)); Faraday's
+own `faraday-typhoeus` adapter was confirmed to silently drop the
+`ciphers` option (never forwards it to `ethon`'s `ssl_cipher_list`) -- a
+real gap in that third-party adapter, not this bridge.
+
+`httpclient`'s `SSLConfig` doesn't trust the system CA bundle unless told
+to -- left unconfigured, it lazily falls back to its own gem-vendored
+`cacert.pem` snapshot, which can go stale relative to a real server's
+certificate chain as CAs rotate intermediates (confirmed directly: a real
+Let's Encrypt-signed endpoint failed verification against an older bundled
+snapshot while verifying fine against the host's own CA bundle).
+`lib/soap/httpbackend/httpclient.rb` now calls `set_default_paths` on
+every connection's `ssl_config` by default, deferring to whatever CA store
+OpenSSL was built to trust on the platform, layered before any
+`ssl_config.ca_file`/`ca_path`/`cert_store` set explicitly. `NetHttpClient`
+was never affected -- it has no SSL config surface of its own and defers
+to stdlib `Net::HTTP`/OpenSSL defaults directly.
+
+## Known Test Suite Exceptions (full detail)
+
+Short summary lives in README.md; full root-cause narrative here.
+
+- **Ruby 1.8.7**:
+ - 3 errors (`test_time`, `test_time_ivar`, `test_time_subclass` in
+ `marshaltestlib.rb`): `Kernel#singleton_class` doesn't exist until Ruby
+ 1.9.2. CANTFIX -- the method is absent on that Ruby, full stop.
+ - CGI-based tests (`test_calc_cgi`, `test_authheader_cgi`) fail with
+ `SOAP::ResponseFormatError: ... Internal Server Error`. Root-caused
+ partway: WEBrick's `CGIHandler` wipes the spawned CGI subprocess's
+ entire environment before exec'ing it, so `logger-application` and
+ `webrick` need forwarding via a raw `-I` load-path flag (already fixed,
+ in both test files). That forwarding alone wasn't enough on 1.8.7
+ specifically: the spawned subprocess still hits `NameError: undefined
+ local variable or method 'logger'` inside
+ `lib/soap/rpc/cgistub.rb#run`, even though `Logger::Application#logger`
+ is a real public method and a minimal reproduction of the same class
+ hierarchy works fine in isolation. Not fully root-caused -- something
+ specific to the real `CGIStub`/`AuthHeaderPortServer`/`CalcServer`
+ class graph, only reproducible via the actual spawned-CGI-subprocess
+ path. Narrow enough (2 of ~330 tests, both CGI smoke tests) that it
+ wasn't worth further chasing.
+ - Collateral damage from the CGI issue above: `test_nil_attribute` and
+ `test_wsdl_with_map` (`test/wsdl/document/test_rpc.rb`) intermittently
+ receive a garbage `dateTime` value (`XSD::ValueSpaceError`) when run as
+ part of the full suite, but pass cleanly in isolation
+ (`SCOPE=wsdl/document`). Confirmed state leaking from a still-lingering
+ CGI subprocess/WEBrick thread earlier in the same run, not a genuine
+ 1.8.7 `Date`/`DateTime` bug -- `XSDDateTimeImpl#screen_data`'s `Time`
+ branch produces a correct result in isolation. Not independently
+ fixable -- same underlying CGI/WEBrick fragility, different test.
+- **Ruby 2.4.10, 2.5.9, `SOAP4R_HTTP_CLIENTS=curb`**: `test_ca_verification`
+ and `test_ciphers` (`test/soap/ssl/test_ssl.rb`) fail with
+ `Curl::Err::SSLPeerCertificateError: ... unable to get issuer
+ certificate`, even with a correct, complete CA chain supplied. CANTFIX --
+ confirmed environment-specific: the official `ruby:2.4.10`/`ruby:2.5.9`
+ images ship libcurl 7.64.0/OpenSSL 1.1.1d, while `ruby:2.6.10`+ ship
+ libcurl 7.74.0/OpenSSL 1.1.1n; the same chain validates cleanly under the
+ newer pair. httpclient/net_http (unaffected by libcurl version) pass
+ this same test cleanly everywhere, confirming this is that older libcurl
+ build, not this bridge's CA-file wiring.
+- **Ruby 3.0.7, 3.1.7, 3.2.11**: 1 failure in `test_exception`
+ (`marshaltestlib.rb`), which marshals an exception whose `.message`
+ embeds a live `#inspect` dump of the entire running
+ `Test::Unit::TestCase` (memory addresses, internal framework state and
+ all). WONTFIX -- confirmed via minimal reproduction that soap4r-ng's own
+ exception-marshaling round-trips correctly; the test itself compares
+ volatile process/framework internals that render differently across this
+ narrow patch-version range.
+- **JRuby** (9.4.15.0 and 10.1.0.0, identical on both) -- 13 confirmed
+ environment-specific items:
+ - 7 `XSD::ValueSpaceError` tests (`test_SOAPInteger`, `test_XSDInteger`,
+ etc.): JRuby's `Kernel#Integer()` silently stops validating trailing
+ garbage once the digit count gets long enough, where MRI always raises
+ `ArgumentError`. CANTFIX -- JRuby core-method behavior difference.
+ - `test_singleton` (`marshaltestlib.rb`): expects marshaling `ENV` to
+ raise `TypeError`. JRuby's `ENV` is backed by a `Hash`-flavored object
+ rather than MRI's anonymous-object singleton, so it never trips the
+ check. CANTFIX -- JRuby object-representation difference.
+ - `test_ciphers`, `test_property`, `test_verification`
+ (`test/soap/ssl/test_ssl.rb`): `TypeError: failed to coerce
+ java.lang.String to [Ljava.lang.String;`, confirmed entirely inside
+ `httpclient`'s own JRuby SSL socket bridge
+ (`httpclient/jruby_ssl_socket.rb`). CANTFIX (upstream dependency).
+ - `test_nestedexception` (both variants): JRuby's backtrace formatting
+ differs from every MRI format already branched on. CANTFIX -- the
+ engine differs, not the Ruby version.
+ - A handful of other JRuby-only failures (`test_calc`, `test_calc2`,
+ `test_calc_cgi`, `test_authfailure` x2, `test_mu`) turned out to be a
+ real, fixable bug: `SOAP::Mapping.fault2exception` assumed a
+ reconstructed fault exception always has a non-nil `.backtrace`, false
+ on JRuby for a programmatically-constructed exception. Fixed with a
+ nil-guard.
+- **`SOAP4R_HTTP_CLIENTS=faraday SOAP4R_FARADAY_ADAPTER=patron`** -- not run
+ in CI, but documented for anyone who reaches for it: CGI-based tests
+ fail with `Patron::Aborted: Callback aborted`. Root-caused to `patron`
+ itself: reproduced with a bare `Patron::Session#post` against the exact
+ same WEBrick CGI-subprocess server, no soap4r-ng or Faraday code in the
+ path. A raw TCP-level dump ruled out a missing `Content-Length`. Most
+ likely explanation, unconfirmed without reading patron's C extension
+ directly: patron implements its own request timeouts via a libcurl
+ progress callback, and the CGI handler's per-request subprocess spawn (a
+ few hundred ms before the first byte, unlike every other test's
+ in-process handler) is the one thing distinguishing failing requests
+ from passing ones, including under `curb` (also libcurl-based) and both
+ adapters CI actually runs. CANTFIX without a patron-side fix.
diff --git a/Gemfile b/Gemfile
index c76f2fc0..a470ad1e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,11 +1,42 @@
source 'http://rubygems.org'
-if RUBY_VERSION.to_f <= 1.8
- gem 'htmlentities', '4.3.1' # Require this if OxParser's built-in "Special Character" conversion isn't sufficient for your needs.
- gem 'httpclient', '~> 2.7.0.1'
-else
+if RUBY_VERSION.to_f > 1.8
gem 'httpclient' # 2.1.5.2
- gem 'htmlentities', '~> 4.3.3' # Require this if OxParser's built-in "Special Character" conversion isn't sufficient for your needs.
+else
+ gem 'httpclient', '~> 2.7.0.1'
+end
+
+# ---------------------------------------------------------------------------
+# Additional (opt-in) HTTP client backends -- see "HTTP Client Backends" in
+# README.md and lib/soap/httpbackend.rb. Not installed by default; requires
+# curb needs a system libcurl-dev at compile time. Install with:
+# bundle install --with http_curb http_faraday
+# Neither has a JRuby port (no native-extension support there).
+group :http_curb, :optional => true do
+ # >= 2.4: curb.c needs CURL_SSLVERSION_MAX_* (libcurl >= 7.54.0), which
+ # predates the system libcurl-dev on ruby:2.2.10/2.3.8's Docker images.
+ # See CHANGELOG.md.
+ gem 'curb' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.4
+end
+group :http_faraday, :optional => true do
+ # >= 2.6: an enhancement for modern Rubies, not a legacy capability --
+ # older Faraday releases don't match lib/soap/faradayClient.rb's adapter
+ # architecture. See CHANGELOG.md.
+ gem 'faraday' if RUBY_VERSION.to_f >= 2.6
+ # faraday-typhoeus is the real second adapter this project tests against
+ # (see SOAP4R_FARADAY_ADAPTER in lib/soap/faradayClient.rb).
+ gem 'faraday-typhoeus' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6
+ gem 'typhoeus' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6
+ # ethon (typhoeus's dep) pulls in an unconstrained ffi needing Ruby >= 3.0;
+ # pin the newest release that still installs below that.
+ gem 'ffi', '~> 1.12.2' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6 && RUBY_VERSION.to_f < 3.0
+ # faraday-patron: third, optional spot-check adapter (see
+ # SOAP4R_FARADAY_ADAPTER=patron).
+ gem 'faraday-patron' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6
+ gem 'patron' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6
+ # faradayClient.rb's manual Basic-auth header encoding needs this from
+ # Ruby >= 3.4 (see logger/getoptlong below for the same demotion pattern).
+ gem 'base64' if RUBY_VERSION.to_f >= 3.4
end
# ---------------------------------------------------------------------------
@@ -17,115 +48,102 @@ end
if RUBY_PLATFORM =~ /java/
# ox has no JRuby port at all -- nothing to require here; oxparser will
# gracefully report unavailable via xsd/xmlparser.rb's parser cascade.
-elsif RUBY_VERSION.to_f >= 2.2
+elsif RUBY_VERSION.to_f >= 2.7
gem 'ox' # oxparser ; Uses its own custom C-library
+elsif RUBY_VERSION.to_f >= 2.2
+ # Ruby 2.2.x - 2.6.x: unconstrained resolves Ox 2.14.14, the newest release
+ # before Ox's own gemspec required Ruby >= 2.7. 2.14.14 segfaults inside
+ # Ox.sax_parse's :convert_special => true path on complex documents
+ # (confirmed via test_mapping.rb on 2.2.10/2.3.8/2.4.10/2.6.10) --
+ # htmlentities avoids that path entirely (see oxparser.rb), so it's a
+ # required workaround here, not an optional speed boost.
+ gem 'ox'
+ gem 'htmlentities', '~> 4.3.3'
+elsif RUBY_VERSION.to_f > 1.8
+ # Ruby 1.9.3 - 2.1.x: newest Ox that loads without the rb_utf8_str_new
+ # crash here and decodes named entities natively (htmlentities not
+ # needed). Confirmed empirically -- 2.14.7 introduces the crash. Pin
+ # exact: patch releases matter here.
+ gem 'ox', '= 2.14.6'
else
- # current ox needs rb_utf8_str_new (Ruby >= 2.2); older Rubies fail with
- # an unresolvable-symbol crash at native-ext load time (not a catchable
- # LoadError). 2.4.5 predates that and is already the known-good pin used
- # for Ruby <= 1.8 above; it also covers 1.9.x - 2.1.x.
- #
- # Must be pinned exactly ("= 2.4.5", not "~> 2.4.5"): later 2.4.x patches
- # (confirmed with 2.4.13) hit a second, different unresolvable-symbol
- # crash of their own (RSTRUCT_GET) on these old Rubies, so a floating
- # patch-level constraint silently re-breaks this the same way.
+ # Ruby 1.8.7: 2.14.6's extconf.rb fails to build here (unrelated build
+ # tooling issue), so fall back to 2.4.5. Pin exact: 2.4.13 hits a
+ # different crash (RSTRUCT_GET) elsewhere in this range.
gem 'ox', '= 2.4.5'
+ # Only needed here: 2.4.5 decodes just the 5 basic XML entities, not the
+ # full named set. 4.3.1 pinned -- 4.3.3+ needs Encoding, absent on 1.8.7.
+ gem 'htmlentities', '4.3.1'
end
-if RUBY_VERSION.to_f <= 1.8
- gem 'nokogiri', '~> 1.5.11' # nokogiriparser ; Uses libxml2, libxslt, and zlib
-elsif RUBY_VERSION.to_f <= 2.2
+if RUBY_VERSION.to_f > 2.2
+ gem 'nokogiri' # let Bundler pick a version compatible with the running Ruby
+elsif RUBY_VERSION.to_f > 1.8
gem 'nokogiri', '~> 1.6.6' # nokogiriparser ; Uses libxml2, libxslt, and zlib
else
- gem 'nokogiri' # let Bundler pick a version compatible with the running Ruby
+ gem 'nokogiri', '~> 1.5.11' # nokogiriparser ; Uses libxml2, libxslt, and zlib
end
if RUBY_PLATFORM =~ /java/
- # libxml-jruby's one and only release is from 2010 and calls
- # `include_class`, a JRuby API removed long ago -- it's a NoMethodError at
- # require time (not a catchable LoadError) on any current JRuby, and
- # there's no newer version or replacement gem to pin instead. Leaving
- # libxmlparser unavailable here is consistent with ox above, which simply
- # has no JRuby port either.
-elsif RUBY_VERSION.to_f <= 1.9
- gem 'libxml-ruby', '~> 2.8.0'
-else
+ # libxml-jruby's only release (2010) calls a since-removed JRuby API
+ # (NoMethodError at require time) -- no replacement exists, so libxmlparser
+ # is simply unavailable here, same as ox above.
+elsif RUBY_VERSION.to_f > 1.9
gem 'libxml-ruby', '~> 3.1.0'
+else
+ gem 'libxml-ruby', '~> 2.8.0'
end
if RUBY_VERSION.to_f > 1.8
- # oga itself supports Ruby >= 1.9.3, but its own `ruby-ll` dependency
- # constraint (~> 2.1) permits drifting all the way up to ruby-ll 2.2.0,
- # whose C ext needs RUBY_TYPED_FREE_IMMEDIATELY (Ruby >= 2.1) -- Bundler
- # has no way to know that from the declared constraint alone. Pin ruby-ll
- # directly to the last release that still declares Ruby >= 1.9.3 support.
+ # oga's own ruby-ll dependency (~> 2.1) can drift to 2.2.0, whose C ext
+ # needs RUBY_TYPED_FREE_IMMEDIATELY (Ruby >= 2.1). Pin the last ruby-ll
+ # release that still supports Ruby >= 1.9.3.
gem 'ruby-ll', '~> 2.1.2' if RUBY_VERSION.to_f < 2.1
gem 'oga' # ogaparser ; Pure-Ruby Alternative
end
if RUBY_VERSION.to_f > 1.8
- # rexml/webrick/logger were all implicit default gems once, pulled in
- # automatically without a Gemfile entry -- but only up to the Ruby version
- # where each was demoted to a bundled gem (rexml/webrick: 3.0, logger: 4.0).
- # Below that floor they're still implicitly available and don't need (or
- # want) a Gemfile entry: rubygems.org only hosts *current* rexml/webrick/
- # logger releases, built with syntax (e.g. `**opts`, `&.`) that predates-Ruby
- # can't even parse, so pulling them in unconditionally shadows a perfectly
- # working built-in with a gem that hard-crashes at require time.
- gem 'rexml' if RUBY_VERSION.to_f >= 3.0 # no longer an implicit default gem under Bundler as of Ruby 3.0
- gem 'webrick' if RUBY_VERSION.to_f >= 3.0 # same; needed by lib/soap/rpc/{httpserver,cgistub,soaplet}.rb
- # dropped in Ruby 4.0, but the "will no longer be part of the default
- # gems" notice itself already starts firing a full version earlier, on a
- # plain `require 'logger'` with no Bundler pinning involved at all --
- # confirmed silent on 3.1.7/3.2.11/3.3.11, present starting 3.4.10. Gated
- # here from 3.4 (not 4.0) to actually silence it, matching getoptlong
- # below which has the same early-warning behavior.
+ # rexml/webrick/logger were implicit default gems until demoted to bundled
+ # gems (rexml/webrick: Ruby 3.0, logger: 4.0). Below that floor they're
+ # still built in and must NOT get a Gemfile entry: current releases on
+ # rubygems.org use syntax predates-Ruby can't parse.
+ gem 'rexml' if RUBY_VERSION.to_f >= 3.0
+ gem 'webrick' if RUBY_VERSION.to_f >= 3.0 # needed by lib/soap/rpc/{httpserver,cgistub,soaplet}.rb
+ # logger's deprecation warning starts firing a version early (3.4, not
+ # 4.0) on a plain `require` with no pinning at all; gated here to silence
+ # it, matching getoptlong below.
gem 'logger' if RUBY_VERSION.to_f >= 3.4
- gem 'getoptlong' if RUBY_VERSION.to_f >= 3.4 # same; bin/{xsd2ruby,wsdl2ruby}.rb both need it unconditionally
+ gem 'getoptlong' if RUBY_VERSION.to_f >= 3.4 # bin/{xsd2ruby,wsdl2ruby}.rb need it unconditionally
gem 'logger-application', :require=>'logger-application'
end
## # Testing Support ###
group :test do
- if RUBY_VERSION.to_f <= 1.8
+ if RUBY_VERSION.to_f > 1.9
+ gem 'test-unit'
+ gem 'rake'
+ elsif RUBY_VERSION.to_f > 1.8
+ # current test-unit uses Ruby >= 2.0 keyword-argument syntax (SyntaxError
+ # on 1.9.3); pin an old-enough release.
+ gem 'test-unit', '~> 3.0.5'
+ gem 'rake'
+ else
gem 'test-unit', '~> 1.2.3'
gem 'rake', '~> 10.4.2'
- # test-unit 1.2.3 lists hoe as a *runtime* dependency (a common
- # old-ecosystem quirk from that era); hoe's current release needs
- # Ruby >= 2.7, so pin the oldest version that still satisfies
- # test-unit's own floor (>= 1.5.1). hoe in turn pulls in rubyforge,
- # which pulls in json_pure -- same story, pin that too.
+ # test-unit 1.2.3 lists hoe as a runtime dependency; current hoe needs
+ # Ruby >= 2.7, so pin the oldest release satisfying test-unit's floor
+ # (>= 1.5.1). hoe pulls in rubyforge -> json_pure; pin that too.
gem 'hoe', '1.5.1'
gem 'json_pure', '~> 1.7.6'
- elsif RUBY_VERSION.to_f <= 1.9
- # current test-unit (like rexml/webrick/logger above) is built with
- # Ruby >=2.0 keyword-argument syntax and is a SyntaxError on 1.9.3; pin an
- # old-enough release instead of relying on Bundler to steer around it.
- gem 'test-unit', '~> 3.0.5'
- gem 'rake'
- else
- gem 'test-unit'
- gem 'rake'
end
- # test-unit pulls in power_assert, which pulls in ansi -- ansi 1.6.0 needs
- # Ruby >= 3.1, and Bundler's resolver picks it for some Ruby versions in
- # our supported range (2.3.8 confirmed) but not others (2.2.10, 1.9.3
- # both land on the older, compatible 1.5.0) depending on how the rest of
- # the bundle resolves. Pin it directly rather than rely on that.
+ # test-unit -> power_assert -> ansi; ansi 1.6.0 needs Ruby >= 3.1 and
+ # Bundler's resolver doesn't always avoid it below that. Pin directly.
gem 'ansi', '~> 1.5.0' if RUBY_VERSION.to_f < 3.1
- # Not used directly anywhere in this codebase -- it's here only because
- # simplecov (below) depends on 'json' unconstrained, and current json
- # releases use **opts keyword-splat syntax that's a SyntaxError on Ruby
- # <= 1.9. Without this pin Bundler resolves json 2.3.0 even on 1.9.3 and
- # every test run fails at require time before a single test executes.
+ # Not used directly -- simplecov (below) depends on unconstrained 'json',
+ # whose current releases use **opts syntax (SyntaxError on Ruby <= 1.9).
gem 'json', '~> 1.8' if RUBY_VERSION.to_f <= 1.9
gem 'rubyjedi-testunitxml', :git=>'https://github.com/rubyjedi/testunitxml.git', :branch=>'master'
- # test/helper.rb requires this directly (RUBY_VERSION.to_f >= 1.9); it used
- # to arrive only transitively via codeclimate-test-reporter, so removing
- # that gem means it needs its own declaration now. Pinned to the exact
- # version that dependency always resolved to, since that's what every
- # Ruby version in this project's test matrix has actually been verified
- # against.
+ # test/helper.rb requires this directly; pinned to the version this
+ # project's test matrix has actually been verified against.
gem 'simplecov', '0.13.0' if RUBY_VERSION.to_f >= 1.9
### Misc Debugging Aids ###
diff --git a/README.md b/README.md
index 576457b7..c6837e2a 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,9 @@ gem 'webrick'
gem 'logger'
gem 'httpclient' # Strongly recommended. See "HTTP Client Backends" below for the Net::HTTP fallback's limitations.
+## curb and faraday are additional, opt-in HTTP client backends -- not
+## needed unless you specifically want one of them; see "HTTP Client
+## Backends" below.
gem 'soap4r-ng', :git=>'https://github.com/rubyjedi/soap4r.git', :branch=>"master"
```
@@ -89,140 +92,78 @@ is a drop-in file rather than a change to library code.
* **[httpclient](https://github.com/nahi/httpclient)** -- the default and
strongly recommended backend. Fully featured: proxying, basic/digest auth,
- cookies, SSL/TLS configuration, request/response filters (used for things
- like cookie handling -- see `test/soap/test_cookie.rb`).
-* **[http-access2](https://rubygems.org/gems/http-access2)** -- httpclient's
- predecessor, by the same author. Historically the second fallback, but it's
- no longer published on RubyGems.org at all, so in practice this backend is
- unreachable today unless you vendor the gem yourself.
+ cookies, SSL/TLS configuration, request/response filters. (Formerly named
+ `http-access2`; see CHANGELOG.md if you need that old adapter.)
+* **[curb](https://github.com/taf2/curb)** -- libcurl bindings. Opt-in (see
+ below), needs system `libcurl-dev` at compile time. Supports proxying,
+ SSL/TLS config (ca_file, client cert/key, cipher-list restriction), and
+ basic/digest WWW-Authenticate auth. No cookies or request/response
+ filters, and no `ssl_config.verify_callback` (libcurl's C API exposes no
+ per-certificate Ruby callback hook). No JRuby port; gated to Ruby >= 2.4.
+* **[faraday](https://github.com/lostisland/faraday)** -- gated to Ruby
+ >= 2.6 (see CHANGELOG.md for why). Has its own adapter system underneath
+ our backend selection, defaulting to `:net_http` and overridable with
+ `SOAP4R_FARADAY_ADAPTER` (e.g. `SOAP4R_FARADAY_ADAPTER=typhoeus`) -- a
+ second, independent knob layered under `SOAP4R_HTTP_CLIENTS`. Opt-in (see
+ below). Supports proxying, basic auth (sent proactively via a
+ manually-built header), and SSL/TLS config (ca_file, client cert/key as
+ file paths). No challenge-response auth, cookies, request/response
+ filters, or `verify_callback`. Cipher-list restriction passes through to
+ the active adapter, but `:typhoeus` silently ignores it (an upstream gap,
+ not this bridge). `SOAP4R_FARADAY_ADAPTER=patron` also works for ordinary
+ requests but isn't part of the automated test matrix (see "Known Test
+ Suite Exceptions" below).
* **`SOAP::NetHttpClient`** -- this project's own wrapper around stdlib
- `Net::HTTP`, used only when neither gem above is installed. It does **not**
- support basic/digest auth, cookies, or request/response filters (all raise
- `NotImplementedError`, or are silently inert for filters) -- these were
- never wired up for this backend. It's a reasonable fallback for simple
- unauthenticated SOAP calls when you can't add a gem dependency, but
- `httpclient` is what most of this library's real-world testing and feature
- support assumes.
+ `Net::HTTP`, used only when none of the above are installed. No
+ basic/digest auth, cookies, or request/response filters. A reasonable
+ fallback for simple unauthenticated SOAP calls when you can't add a gem
+ dependency, but `httpclient` is what most of this library's real-world
+ testing assumes.
-Force a specific backend (e.g. to debug something backend-specific, or to
-run the test suite against a backend other than the default) with:
+Force a specific backend with:
```
SOAP4R_HTTP_CLIENTS=net_http bundle exec rake test:deep
```
-Valid names are `httpclient`, `http_access2`, and `net_http`, matching the
-files under `lib/soap/httpbackend/`. CI runs the full suite against
-`net_http` too (single-parser, since this is about the HTTP layer, not XML
-parsing) precisely because, before this option existed, nothing in the test
-suite could ever actually reach that fallback -- this project's own Gemfile
-always installs `httpclient`, so the fallback cascade never had a reason to
-fall through. Making it independently selectable surfaced (and fixed) a real
-bug in the process: `SOAP::NetHttpClient`'s wiredump output duplicated
-request/response bodies and was missing the raw request-line/header block
-entirely, corrupting any test or tool that parsed `wiredump_dev` output.
-
-**A note on TLS trust for the `httpclient` backend**: `httpclient`'s
-`SSLConfig` doesn't trust your system's CA bundle unless told to -- left
-unconfigured, it lazily falls back to its own gem-vendored `cacert.pem`
-snapshot, which can go stale relative to a real server's certificate chain as
-CAs rotate their intermediates (confirmed directly: a real Let's
-Encrypt-signed endpoint failed verification against an older bundled
-snapshot while verifying fine against the host's own, actively-maintained CA
-bundle). `lib/soap/httpbackend/httpclient.rb` calls `set_default_paths` on
-every `httpclient` connection's `ssl_config` by default now, which defers to
-whatever CA store OpenSSL was actually built to trust on your platform --
-portable across Debian/RHEL/Alpine/etc, and layered *before* any
-`ssl_config.ca_file`/`ca_path`/`cert_store` you set yourself, so explicit
-configuration still works exactly as before. `SOAP::NetHttpClient` was never
-affected by this -- it has no SSL configuration surface of its own and
-simply defers to Ruby's own stdlib `Net::HTTP`/OpenSSL defaults, which
-already trust the system store without any help.
+Valid names are `httpclient`, `curb`, `faraday`, and `net_http`, matching
+the files under `lib/soap/httpbackend/`. `curb` and `faraday` are opt-in
+Bundler groups: `BUNDLE_WITH="http_curb:http_faraday" bundle install`
+(an env var, not `bundle config set with "..."` -- see CHANGELOG.md).
+
+CI runs the full suite against `net_http`, `curb`, and `faraday` too
+(single-parser each -- this is about the HTTP layer, not XML parsing).
+Faraday runs with the `:typhoeus` adapter specifically (real correctness
+check plus the most-used adapter in practice); `:patron` was tried and
+dropped from CI (real upstream bug, not ours -- see "Known Test Suite
+Exceptions" below). Full backstory on the backend rollout (wiredump-parity
+work, TLS-trust defaults, per-backend test coverage decisions) is in
+CHANGELOG.md.
#### Known Test Suite Exceptions
-Running `rake test:deep` (the complete suite) across the full version matrix
-surfaces a small, understood set of failures that aren't soap4r-ng bugs --
-they're either something the target Ruby genuinely can't do, or a test that's
-checking something too environment-specific to be a fair test. Documented
-here so they don't get mistaken for regressions:
-
-CI runs every version inside the same Docker image used for local
-validation (official `ruby:X.Y.Z` / `jruby:X.Y.Z.W` images where one exists;
-a from-source `rbenv` build on Debian bullseye for 1.9.3 and 2.0.0, whose
-official Docker Hub images are permanently unpullable -- every tag in both
-lines was published in the long-deprecated Docker manifest v1 schema; a
-from-source build against a vendored OpenSSL 1.0.2u for 1.8.7, which has no
-official image at all). Matching the exact environment means a clean local
-run and a clean CI run are the same claim -- there's no separate "works on
-my machine" environment for version-specific gotchas to hide in.
-
-* **Ruby 1.8.7** -- two separate issues, neither a soap4r-ng regression:
- * 3 errors (`test_time`, `test_time_ivar`, `test_time_subclass` in
- `marshaltestlib.rb`) from `Kernel#singleton_class`, which doesn't exist
- until Ruby 1.9.2. **CANTFIX**: the method is absent on that Ruby, full
- stop.
- * The CGI-based tests (`test_calc_cgi`, `test_authheader_cgi`) fail with
- `SOAP::ResponseFormatError: ... Internal Server Error`. Root-caused
- partway: WEBrick's `CGIHandler` wipes the spawned CGI subprocess's
- entire environment before exec'ing it, so `logger-application` and
- `webrick` need forwarding via a raw `-I` load-path flag (already fixed,
- in `test/soap/calc/test_calc_cgi.rb` and
- `test/soap/header/test_authheader_cgi.rb`). That forwarding alone
- wasn't enough on 1.8.7 specifically: the spawned
- subprocess still hits `NameError: undefined local variable or method
- 'logger'` inside `lib/soap/rpc/cgistub.rb#run`, even though
- `Logger::Application#logger` is a real public method and a minimal
- reproduction of the exact same class hierarchy (`Logger::Application`
- combined with `include SOAP` and `include WEBrick`) works fine in
- isolation.
- **Not fully root-caused** -- something specific to the real
- `CGIStub`/`AuthHeaderPortServer`/`CalcServer` class graph, only
- reproducible via the actual spawned-CGI-subprocess path, not a
- simplified one. Narrow enough in scope (2 of ~330 tests, both CGI
- smoke tests rather than core library behavior) that it wasn't worth
- further chasing this round; flagged here rather than silently
- swallowed by `continue-on-error`.
-* **Ruby 3.0.7, 3.1.7, 3.2.11** -- 1 failure in `test_exception`
- (`test/soap/marshal/marshaltestlib.rb`), which marshals an exception whose
- `.message` embeds a live `#inspect` dump of the entire running
- `Test::Unit::TestCase` (memory addresses, internal test-framework state and
- all). **WONTFIX**: confirmed via a minimal reproduction that soap4r-ng's own
- exception-marshaling code round-trips correctly; the test itself is
- fragile because it's comparing volatile process/framework internals that
- happen to render differently across this narrow range of Ruby patch
- versions, not anything under this library's control.
-* **JRuby** (9.4.15.0 and 10.1.0.0, identical on both) -- 13 confirmed
- environment-specific items, none of them soap4r-ng bugs:
- * 7 `XSD::ValueSpaceError` tests (`test_SOAPInteger`, `test_XSDInteger`,
- and friends) -- JRuby's `Kernel#Integer()` silently stops validating
- trailing garbage characters (e.g. a trailing `.`) once the digit count
- gets long enough, where MRI correctly raises `ArgumentError` regardless
- of length. **CANTFIX**: a JRuby core-method behavior difference.
- * `test_singleton` (`marshaltestlib.rb`) -- expects marshaling `ENV` to
- raise `TypeError` (Ruby singleton objects can't be dumped). JRuby's
- `ENV` is backed by a `Hash`-flavored object rather than MRI's
- anonymous-object singleton, so it never trips the singleton-detection
- check at all. **CANTFIX**: a JRuby object-representation difference,
- and not a realistic thing to special-case for one object.
- * `test_ciphers`, `test_property`, `test_verification`
- (`test/soap/ssl/test_ssl.rb`) -- fail with
- `TypeError: failed to coerce java.lang.String to [Ljava.lang.String;`.
- Confirmed via backtrace inspection that this is entirely inside the
- `httpclient` gem's own JRuby-specific SSL socket bridge
- (`httpclient/jruby_ssl_socket.rb`), not soap4r-ng's code.
- **CANTFIX** (upstream, in a dependency).
- * `test_nestedexception` (both `SOAP::TestMapping` and
- `SOAP::TestNestedException`) -- JRuby's backtrace formatting differs
- from every MRI format the test already branches on by Ruby version.
- **CANTFIX**: nothing to version-branch against, since it's the Ruby
- engine that differs, not the Ruby version.
-
- A handful of other JRuby-only failures (`test_calc`, `test_calc2`,
- `test_calc_cgi`, `test_authfailure` x2, `test_mu`) turned out to be a real,
- fixable bug rather than a JRuby limitation: `SOAP::Mapping.fault2exception`
- (`lib/soap/mapping/mapping.rb`) assumed a reconstructed fault exception
- always has a non-nil `.backtrace`, which doesn't hold on JRuby for a
- programmatically-constructed (never actually `raise`d-and-caught)
- exception object. Fixed with a nil-guard; confirmed clean on both JRuby and
- MRI afterward.
+Running `rake test:deep` across the full version matrix surfaces a small,
+understood set of failures that aren't soap4r-ng bugs -- either something
+the target Ruby/engine genuinely can't do, or a test checking something too
+environment-specific to be fair. Documented here so they don't get mistaken
+for regressions; full root-cause writeups are in CHANGELOG.md.
+
+* **Ruby 1.8.7** -- 3 errors from `Kernel#singleton_class` not existing
+ until Ruby 1.9.2 (CANTFIX), plus the CGI-based tests
+ (`test_calc_cgi`, `test_authheader_cgi`) and two collateral WSDL tests
+ failing from a not-fully-root-caused WEBrick/CGI-subprocess environment
+ issue.
+* **Ruby 2.4.10, 2.5.9, `SOAP4R_HTTP_CLIENTS=curb`** -- `test_ca_verification`/
+ `test_ciphers` fail from those Docker images' older libcurl/OpenSSL
+ (CANTFIX, environment-specific, not a soap4r-ng/curb bug).
+* **Ruby 3.0.7, 3.1.7, 3.2.11** -- `test_exception` fails because it compares
+ a live `Test::Unit::TestCase#inspect` dump that renders differently across
+ this patch-version range (WONTFIX, not under this library's control).
+* **JRuby** (9.4.15.0, 10.1.0.0) -- 13 confirmed environment/engine
+ differences (integer parsing, `ENV` object model, SSL socket bridge,
+ backtrace formatting), all CANTFIX/upstream. A handful of other JRuby-only
+ failures were a real, fixable bug (`SOAP::Mapping.fault2exception` assumed
+ a non-nil `.backtrace`) -- fixed with a nil-guard.
+* **`SOAP4R_HTTP_CLIENTS=faraday SOAP4R_FARADAY_ADAPTER=patron`** -- not run
+ in CI; the CGI-based tests fail with `Patron::Aborted: Callback aborted`,
+ root-caused to patron itself, not this project's code (CANTFIX).
#### How to Use
* [NaHi's Original documentation](https://web.archive.org/web/20101212040735/http://dev.ctor.org/soap4r/wiki/) -- the authoritative reference material is still available through the Wayback Machine, thankfully!
@@ -231,7 +172,7 @@ my machine" environment for version-specific gotchas to hide in.
#### How to Get a Speed Boost : Use Nokogiri or Ox, not REXML
Be sure to have Nokogiri or Ox available in your Gemset. Soap4R-ng will find and use what's available (Ox has highest precedence, then Nokogiri, then LibXML, then Oga, falling back to REXML as the last resort if needed).
-I personally recommend **Nokogiri** as the best performing, most flexible parser at this time, as it handles "special characters" like HTML ampersand-escaped characters internally. Ox doesn't handle such an extensive set of special-characters natively, so to get things up to par, I added **htmlentities** support if it's available when using the Ox parser. Using **htmlentities** with **Ox** in this manner adds a bit of a performance penalty, however.
+I personally recommend **Nokogiri** as the best performing, most flexible parser at this time, as it handles "special characters" like HTML ampersand-escaped characters internally. Ox needs **htmlentities** as a fallback on Ruby 1.8.7 (stuck on the old Ox 2.4.5, which only decodes the 5 basic XML entities) and on Ruby 2.2.x-2.6.x (stuck on Ox 2.14.14, which segfaults on complex documents without it -- see CHANGELOG.md). Ruby 1.9.3-2.1.x and Ruby >= 2.7 both resolve an Ox release that decodes entities natively and safely, so htmlentities is dead weight there.
If you know your incoming XML is "clean", Ox is a really great alternative.
diff --git a/lib/soap/curbClient.rb b/lib/soap/curbClient.rb
new file mode 100644
index 00000000..2824484f
--- /dev/null
+++ b/lib/soap/curbClient.rb
@@ -0,0 +1,286 @@
+# encoding: UTF-8
+# SOAP4R - curb (libcurl) wrapper.
+
+require 'curb'
+require 'tempfile'
+require 'soap/filter/filterchain'
+
+
+module SOAP
+
+
+class CurbClient
+ attr_reader :proxy
+ attr_accessor :no_proxy
+ attr_accessor :debug_dev
+ attr_reader :ssl_config
+ attr_accessor :protocol_version # ignored -- libcurl negotiates this itself.
+ attr_accessor :connect_timeout
+ attr_accessor :send_timeout # ignored -- libcurl has one overall #timeout, not separate send/receive phases.
+ attr_accessor :receive_timeout
+ attr_reader :test_loopback_response
+ attr_reader :request_filter # ignored for now, same as SOAP::NetHttpClient.
+
+ def initialize(proxy = nil, agent = nil)
+ @proxy = proxy
+ @no_proxy = nil
+ @agent = agent
+ @debug_dev = nil
+ @ssl_config = SSLConfig.new
+ @connect_timeout = @receive_timeout = @send_timeout = nil
+ @basic_auth = nil # [user, pass], set via #set_basic_auth
+ @challenge_auth = nil # [user, pass], set via #set_auth
+ @cookie_store = nil
+ @test_loopback_response = []
+ @request_filter = Filter::FilterChain.new
+ end
+
+ def proxy=(value)
+ if value
+ uri = value.is_a?(URI) ? value : URI.parse(value)
+ if uri.scheme.nil? or uri.scheme.downcase != 'http' or uri.host.nil? or uri.port.nil?
+ raise ArgumentError.new("unsupported proxy `#{value}'")
+ end
+ end
+ @proxy = value
+ end
+
+ def set_basic_auth(uri, user_id, passwd)
+ @basic_auth = [user_id, passwd]
+ end
+
+ # soap4r's "auth" (as opposed to "basic_auth") is fed by a server
+ # challenge (WWW-Authenticate: Basic or Digest) rather than sent
+ # proactively -- :any lets libcurl negotiate whichever the server
+ # actually asks for, covering both test/soap/auth/test_basic.rb and
+ # test/soap/auth/test_digest.rb with the same code path.
+ def set_auth(uri, user_id, passwd)
+ @challenge_auth = [user_id, passwd]
+ end
+
+ def set_cookie_store(filename)
+ @cookie_store = filename
+ end
+
+ def save_cookie_store
+ # curb's cookiejar is written incrementally as requests complete;
+ # nothing to flush here.
+ end
+
+ def reset(url)
+ # no persistent connection state kept between requests; ignored.
+ end
+
+ def reset_all
+ # ditto.
+ end
+
+ def post(url, req_body, header = {})
+ if str = @test_loopback_response.shift
+ if @debug_dev
+ @debug_dev << "= Request\n\n"
+ @debug_dev << req_body
+ @debug_dev << "\n\n= Response\n\n"
+ @debug_dev << str
+ end
+ return Response.new(200, nil, 'text/xml', Hash.new { |h, k| h[k] = [] }, str)
+ end
+ curl = build_curl(url, header)
+ curl.http_post(req_body)
+ dump_wiredump(curl, req_body) if @debug_dev
+ Response.from_curl(curl)
+ end
+
+private
+
+ def build_curl(url, header)
+ # Curl::Easy.new needs a String -- given a URI object instead (the
+ # driver's endpoint can be constructed from either, see test_uri in
+ # test/soap/test_streamhandler.rb), curb's C extension doesn't coerce
+ # it and silently stores nil for the URL internally, surfacing much
+ # later as "TypeError: no implicit conversion of nil into String"
+ # inside curl/multi.rb's own perform machinery rather than failing
+ # here where the actual mistake is.
+ curl = Curl::Easy.new(url.to_s)
+ curl.headers = header.dup
+ curl.headers['User-Agent'] = @agent if @agent
+ curl.follow_location = false # streamHandler.rb's own send_post loop handles redirects.
+ curl.connect_timeout = @connect_timeout if @connect_timeout
+ curl.timeout = @receive_timeout if @receive_timeout
+ unless no_proxy?(url.is_a?(URI) ? url : URI.parse(url))
+ curl.proxy_url = @proxy
+ end
+ if @challenge_auth
+ curl.http_auth_types = :any
+ curl.username, curl.password = @challenge_auth
+ elsif @basic_auth
+ curl.http_auth_types = :basic
+ curl.username, curl.password = @basic_auth
+ end
+ curl.cookiejar = @cookie_store if @cookie_store
+ apply_ssl_config(curl)
+ curl
+ end
+
+ def apply_ssl_config(curl)
+ cfg = @ssl_config
+ return unless cfg
+ curl.ssl_verify_peer = (cfg.verify_mode != OpenSSL::SSL::VERIFY_NONE)
+ curl.ssl_verify_host = (cfg.verify_mode == OpenSSL::SSL::VERIFY_NONE) ? 0 : 2
+ curl.cacert = cfg.ca_file if cfg.ca_file
+ # Curl::Easy has no dedicated ciphers=/cipher_list= method at all
+ # (confirmed empirically -- NoMethodError) despite libcurl itself
+ # supporting CURLOPT_SSL_CIPHER_LIST; #setopt with the raw constant is
+ # curb's only way to reach it.
+ curl.setopt(Curl::CURLOPT_SSL_CIPHER_LIST, cfg.ciphers) if cfg.ciphers
+ # Curl::Easy#cert=/#cert_key= want file paths, but
+ # HTTPConfigLoader#cert_from_file/#key_from_file (lib/soap/httpconfigloader.rb)
+ # already parsed the configured files into OpenSSL::X509::Certificate/
+ # OpenSSL::PKey::RSA objects (that's what httpclient's SSLConfig wants) --
+ # round-trip them back out to PEM tempfiles for curb's sake.
+ curl.cert = write_pem_tempfile(cfg.client_cert, 'cert') if cfg.client_cert
+ curl.cert_key = write_pem_tempfile(cfg.client_key, 'key') if cfg.client_key
+ end
+
+ def write_pem_tempfile(openssl_obj, basename)
+ f = Tempfile.new(["soap4r-curb-#{basename}-", '.pem'])
+ f.write(openssl_obj.to_pem)
+ f.close
+ # The finalizer proc must not be created in an instance-method context
+ # closing over self -- that makes self itself unreachable-but-not-quite
+ # (reachable only via its own finalizer), so it never actually becomes
+ # eligible for GC and the finalizer never runs (confirmed: Ruby warns
+ # "finalizer references object to be finalized" and the tempfile never
+ # got cleaned up). Building the proc in a class method keeps the
+ # closure's only capture to f, not self.
+ ObjectSpace.define_finalizer(self, self.class.tempfile_unlinker(f))
+ f.path
+ end
+
+ def self.tempfile_unlinker(file)
+ proc { file.unlink rescue nil }
+ end
+
+ def dump_wiredump(curl, req_body)
+ # Mirrors httpclient's wiredump block layout (marker line, blank line,
+ # raw request-line + headers, blank line, body) -- callers that parse
+ # wiredump_dev output by block position or by scanning for a "POST ..."
+ # line (e.g. test/soap/test_streamhandler.rb's parse_req_header) depend
+ # on that exact shape regardless of which backend produced it.
+ uri = URI.parse(curl.url)
+ request_line = (curl.proxy_url ? curl.url : uri.request_uri)
+ @debug_dev << "= Request\n\n"
+ @debug_dev << "POST #{request_line} HTTP/1.1\n"
+ curl.headers.each { |k, v| @debug_dev << "#{k}: #{v}\n" }
+ @debug_dev << "\n"
+ @debug_dev << req_body
+ @debug_dev << "\n\n= Response\n\n"
+ @debug_dev << "HTTP/1.1 #{curl.response_code} #{Response.reason_from_header_str(curl.header_str)}\n"
+ @debug_dev << "Content-Type: #{curl.content_type}\n" if curl.content_type
+ @debug_dev << "\n"
+ @debug_dev << curl.body
+ end
+
+ NO_PROXY_HOSTS = ['localhost']
+
+ def no_proxy?(uri)
+ if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
+ return true
+ end
+ unless @no_proxy
+ return false
+ end
+ @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
+ if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
+ (!port || uri.port == port.to_i)
+ return true
+ end
+ end
+ false
+ end
+
+ class SSLConfig
+ attr_accessor :client_cert
+ attr_accessor :client_key
+ attr_accessor :client_ca
+ attr_accessor :verify_mode
+ attr_accessor :verify_depth # not directly settable via libcurl's simpler peer/host verify model; stored only.
+ attr_accessor :options # ditto -- no libcurl equivalent to OpenSSL::SSL::SSLContext#options.
+ attr_accessor :ciphers
+ attr_accessor :verify_callback # not supported -- libcurl has no per-certificate Ruby callback hook.
+ attr_accessor :cert_store # not supported -- libcurl manages its own trust store internally.
+ attr_reader :ca_file
+
+ def set_trust_ca(value)
+ @ca_file = value
+ end
+
+ def set_crl(value)
+ @crl = value # not applied -- libcurl has no direct CRL-file option exposed via curb.
+ end
+
+ # curb/libcurl never bundles its own CA snapshot the way httpclient
+ # does (see lib/soap/httpconfigloader.rb) -- unless #set_trust_ca
+ # overrides it, it always defers to whatever CA bundle libcurl itself
+ # was built against (the system's, on every mainstream Linux
+ # distribution), so there's nothing to override here.
+ def set_default_paths
+ end
+ end
+
+ class Response
+ attr_reader :status
+ attr_reader :reason
+ attr_reader :contenttype
+ attr_reader :header
+ attr_reader :content
+
+ def initialize(status, reason, contenttype, header, content)
+ @status = status
+ @reason = reason
+ @contenttype = contenttype
+ @header = header
+ @content = content
+ end
+
+ def self.from_curl(curl)
+ header = parse_headers(curl.header_str)
+ reason = reason_from_header_str(curl.header_str)
+ new(curl.response_code, reason, curl.content_type, header, curl.body)
+ end
+
+ # curb has no accessor for the HTTP reason phrase (e.g. "OK", "Not
+ # Found") separate from the numeric status -- pull it off the first
+ # line of the raw header block ourselves ("HTTP/1.1 200 OK" -> "OK").
+ # With redirects (multiple status lines in header_str, one per hop)
+ # this reads the LAST one, matching #parse_headers below which also
+ # only reflects the final response's headers.
+ def self.reason_from_header_str(header_str)
+ return nil unless header_str
+ line = header_str.each_line.select { |l| l.start_with?('HTTP/') }.last
+ return nil unless line
+ line.strip.split(' ', 3)[2]
+ end
+
+ # curb only exposes the raw response header block (#header_str), not a
+ # parsed hash -- build one ourselves, keyed lowercase like
+ # streamHandler.rb expects (it reads header['content-encoding'][0] and
+ # header['location'][0] directly), with an Array of values per key to
+ # match the other backends' Hash-of-Arrays shape.
+ def self.parse_headers(header_str)
+ result = Hash.new { |h, k| h[k] = [] }
+ return result unless header_str
+ header_str.each_line do |line|
+ line = line.strip
+ next if line.empty? || line.start_with?('HTTP/')
+ key, value = line.split(':', 2)
+ next unless key && value
+ result[key.strip.downcase] << value.strip
+ end
+ result
+ end
+ end
+end
+
+
+end
diff --git a/lib/soap/faradayClient.rb b/lib/soap/faradayClient.rb
new file mode 100644
index 00000000..f10e3a5e
--- /dev/null
+++ b/lib/soap/faradayClient.rb
@@ -0,0 +1,290 @@
+# encoding: UTF-8
+# SOAP4R - Faraday wrapper.
+#
+# Faraday is itself pluggable (it has its own adapter selection, on top of
+# ours) -- default to its own default adapter (:net_http, bundled via the
+# faraday-net_http gem that ships as a runtime dependency of faraday itself)
+# and let SOAP4R_FARADAY_ADAPTER override it for anyone who wants Faraday to
+# ride on a different underlying transport (e.g. :patron for a libcurl-based
+# one). This is a second, independent config knob layered under our own
+# SOAP4R_HTTP_CLIENTS -- see lib/soap/httpbackend.rb -- not a replacement
+# for it.
+
+require 'faraday'
+require 'base64'
+require 'tempfile'
+require 'soap/filter/filterchain'
+
+
+module SOAP
+
+
+class FaradayClient
+ attr_reader :proxy
+ attr_accessor :no_proxy
+ attr_accessor :debug_dev
+ attr_reader :ssl_config
+ attr_accessor :protocol_version # ignored -- the underlying Faraday adapter negotiates this itself.
+ attr_accessor :connect_timeout
+ attr_accessor :send_timeout # ignored -- mapped onto Faraday's single #timeout option instead (see #receive_timeout).
+ attr_accessor :receive_timeout
+ attr_reader :test_loopback_response
+ attr_reader :request_filter # ignored for now, same as SOAP::NetHttpClient.
+
+ ADAPTER = (ENV['SOAP4R_FARADAY_ADAPTER'] || 'net_http').to_sym
+
+ def initialize(proxy = nil, agent = nil)
+ @proxy = proxy
+ @no_proxy = nil
+ @agent = agent
+ @debug_dev = nil
+ @ssl_config = SSLConfig.new
+ @connect_timeout = @receive_timeout = @send_timeout = nil
+ @basic_auth = nil # [user, pass]
+ @cookie_store = nil
+ @test_loopback_response = []
+ @request_filter = Filter::FilterChain.new
+ require_adapter!
+ end
+
+ def proxy=(value)
+ if value
+ uri = value.is_a?(URI) ? value : URI.parse(value)
+ if uri.scheme.nil? or uri.scheme.downcase != 'http' or uri.host.nil? or uri.port.nil?
+ raise ArgumentError.new("unsupported proxy `#{value}'")
+ end
+ end
+ @proxy = value
+ end
+
+ def set_basic_auth(uri, user_id, passwd)
+ @basic_auth = [user_id, passwd]
+ end
+
+ def set_auth(uri, user_id, passwd)
+ # Faraday's core has no bundled challenge-response (WWW-Authenticate)
+ # negotiation -- that lives in third-party middleware, not something
+ # this bridge can assume is installed. Digest auth in particular has no
+ # core equivalent at all.
+ raise NotImplementedError.new("challenge-response auth is not supported under soap4r + faraday.")
+ end
+
+ def set_cookie_store(filename)
+ raise NotImplementedError.new("cookie persistence is not supported under soap4r + faraday.")
+ end
+
+ def save_cookie_store
+ raise NotImplementedError.new("cookie persistence is not supported under soap4r + faraday.")
+ end
+
+ def reset(url)
+ # no persistent connection state kept between requests; ignored.
+ end
+
+ def reset_all
+ # ditto.
+ end
+
+ def post(url, req_body, header = {})
+ if str = @test_loopback_response.shift
+ if @debug_dev
+ @debug_dev << "= Request\n\n"
+ @debug_dev << req_body
+ @debug_dev << "\n\n= Response\n\n"
+ @debug_dev << str
+ end
+ return Response.new(200, nil, 'text/xml', Hash.new { |h, k| h[k] = [] }, str)
+ end
+ response = build_connection(url).post do |req|
+ header.each { |k, v| req.headers[k] = v }
+ req.headers['User-Agent'] = @agent if @agent
+ if @basic_auth
+ req.headers['Authorization'] =
+ "Basic #{Base64.strict_encode64(@basic_auth.join(':'))}"
+ end
+ req.body = req_body
+ end
+ dump_wiredump(url, header, req_body, response) if @debug_dev
+ Response.from_faraday(response)
+ end
+
+private
+
+ def require_adapter!
+ require "faraday/#{ADAPTER}"
+ rescue LoadError
+ raise LoadError.new(
+ "Faraday adapter #{ADAPTER.inspect} could not be loaded -- is its " \
+ "gem (e.g. \"faraday-#{ADAPTER}\" and whatever it wraps) installed?")
+ end
+
+ def build_connection(url)
+ cfg = @ssl_config
+ ssl_opts = {
+ :ca_file => cfg && cfg.ca_file,
+ :verify => !(cfg && cfg.verify_mode == OpenSSL::SSL::VERIFY_NONE),
+ # Faraday::SSLOptions documents client_cert/client_key as accepting
+ # OpenSSL objects directly, but confirmed empirically that the
+ # :typhoeus adapter rejects them ("Problem with the local SSL
+ # certificate") and only accepts file paths -- same requirement as
+ # curb's C binding (see curbClient.rb), so round-trip the same way
+ # here for whichever adapter is actually active.
+ :client_cert => cfg && cfg.client_cert && write_pem_tempfile(cfg.client_cert, 'cert'),
+ :client_key => cfg && cfg.client_key && write_pem_tempfile(cfg.client_key, 'key'),
+ :verify_depth => cfg && cfg.verify_depth,
+ :cert_store => cfg && cfg.cert_store,
+ }
+ # Faraday::SSLOptions is a Struct, and Faraday's own connection setup
+ # merges this hash into it via []= -- which raises NameError for any
+ # key that isn't a struct member, rather than just ignoring it the way
+ # a Hash would. :ciphers was dropped from that struct for a stretch of
+ # faraday 2.x releases and later restored (confirmed: present on
+ # 2.14.3, absent on 2.8.1 -- exactly what Ruby 3.3+ vs. 2.6/2.7
+ # resolve to respectively) -- confirmed crashing every single request
+ # on the versions missing it. Only include the key at all when this
+ # faraday actually has it; it's passed through for adapters that honor
+ # SSLOptions#ciphers, but confirmed empirically that :typhoeus silently
+ # ignores it either way (Faraday's own typhoeus adapter doesn't
+ # forward it to ethon's ssl_cipher_list at all) -- a real, external gap
+ # in that adapter, not something this bridge can paper over.
+ # verify_depth/cert_store have the same no-guarantee-every-adapter-
+ # honors-them caveat.
+ ssl_opts[:ciphers] = cfg && cfg.ciphers if Faraday::SSLOptions.members.include?(:ciphers)
+ Faraday.new(
+ :url => url,
+ :proxy => no_proxy?(url.is_a?(URI) ? url : URI.parse(url)) ? nil : @proxy,
+ :ssl => ssl_opts
+ ) do |f|
+ f.adapter ADAPTER
+ end.tap do |conn|
+ conn.options.open_timeout = @connect_timeout if @connect_timeout
+ conn.options.timeout = @receive_timeout if @receive_timeout
+ end
+ end
+
+ def write_pem_tempfile(openssl_obj, basename)
+ f = Tempfile.new(["soap4r-faraday-#{basename}-", '.pem'])
+ f.write(openssl_obj.to_pem)
+ f.close
+ # The finalizer proc must not be created in an instance-method context
+ # closing over self -- see the identical comment in curbClient.rb's own
+ # write_pem_tempfile for why (confirmed: Ruby warns "finalizer
+ # references object to be finalized" and the tempfile never actually
+ # got cleaned up otherwise).
+ ObjectSpace.define_finalizer(self, self.class.tempfile_unlinker(f))
+ f.path
+ end
+
+ def self.tempfile_unlinker(file)
+ proc { file.unlink rescue nil }
+ end
+
+ def dump_wiredump(url, header, req_body, response)
+ # Mirrors httpclient's wiredump block layout (marker line, blank line,
+ # raw request-line + headers, blank line, body) -- callers that parse
+ # wiredump_dev output by block position or by scanning for a "POST ..."
+ # line (e.g. test/soap/test_streamhandler.rb's parse_req_header) depend
+ # on that exact shape regardless of which backend produced it.
+ uri = url.is_a?(URI) ? url : URI.parse(url)
+ request_line = (@proxy && !no_proxy?(uri)) ? url : uri.request_uri
+ @debug_dev << "= Request\n\n"
+ @debug_dev << "POST #{request_line} HTTP/1.1\n"
+ header.each { |k, v| @debug_dev << "#{k}: #{v}\n" }
+ @debug_dev << "\n"
+ @debug_dev << req_body
+ @debug_dev << "\n\n= Response\n\n"
+ @debug_dev << "HTTP/1.1 #{response.status} #{response.reason_phrase}\n"
+ contenttype = response.headers['content-type']
+ @debug_dev << "Content-Type: #{contenttype}\n" if contenttype
+ @debug_dev << "\n"
+ @debug_dev << response.body
+ end
+
+ NO_PROXY_HOSTS = ['localhost']
+
+ def no_proxy?(uri)
+ if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
+ return true
+ end
+ unless @no_proxy
+ return false
+ end
+ @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
+ if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
+ (!port || uri.port == port.to_i)
+ return true
+ end
+ end
+ false
+ end
+
+ class SSLConfig
+ # client_cert/client_key/ciphers/verify_depth/cert_store are all applied
+ # via Faraday::SSLOptions in build_connection -- Faraday's options
+ # struct accepts the same OpenSSL::X509::Certificate/OpenSSL::PKey::RSA
+ # objects HTTPConfigLoader already builds for httpclient's sake. Whether
+ # each one is actually *honored* still depends on which concrete adapter
+ # Faraday is riding on (SOAP4R_FARADAY_ADAPTER) -- confirmed working for
+ # :net_http and :typhoeus, not verified against every adapter Faraday
+ # supports.
+ attr_accessor :client_cert
+ attr_accessor :client_key
+ attr_accessor :client_ca
+ attr_accessor :verify_mode
+ attr_accessor :verify_depth
+ attr_accessor :options # no unified Faraday equivalent across adapters (OpenSSL::SSL::SSLContext#options bitmask); stored only.
+ attr_accessor :ciphers
+ attr_accessor :verify_callback # NOT supported -- no adapter Faraday supports exposes a per-certificate Ruby callback hook (same underlying limitation as libcurl-based backends generally).
+ attr_accessor :cert_store
+ attr_reader :ca_file
+
+ def set_trust_ca(value)
+ @ca_file = value
+ end
+
+ def set_crl(value)
+ @crl = value # not applied -- no unified Faraday equivalent.
+ end
+
+ # Whichever concrete adapter Faraday is riding on (net_http, patron,
+ # etc.) determines the real default CA trust behavior; none of them
+ # bundle their own snapshot the way httpclient does, so there's nothing
+ # to override here.
+ def set_default_paths
+ end
+ end
+
+ class Response
+ attr_reader :status
+ attr_reader :reason
+ attr_reader :contenttype
+ attr_reader :header
+ attr_reader :content
+
+ def initialize(status, reason, contenttype, header, content)
+ @status = status
+ @reason = reason
+ @contenttype = contenttype
+ @header = header
+ @content = content
+ end
+
+ def self.from_faraday(response)
+ # Faraday::Utils::Headers keeps one value per key (last write wins for
+ # anything the adapter received more than once, e.g. repeated
+ # Set-Cookie lines) -- wrap each in a single-element Array so callers
+ # get the same Hash-of-Arrays shape the other backends provide
+ # (streamHandler.rb reads header['content-encoding'][0] and
+ # header['location'][0]). A known, accepted limitation: multi-valued
+ # response headers only reflect the last occurrence under this
+ # backend.
+ header = Hash.new { |h, k| h[k] = [] }
+ response.headers.each { |k, v| header[k.downcase] = [v] }
+ new(response.status, response.reason_phrase, response.headers['content-type'],
+ header, response.body)
+ end
+ end
+end
+
+
+end
diff --git a/lib/soap/httpbackend.rb b/lib/soap/httpbackend.rb
index 9f2b0400..58b39ed3 100644
--- a/lib/soap/httpbackend.rb
+++ b/lib/soap/httpbackend.rb
@@ -11,12 +11,21 @@
# anyone debugging a backend-specific issue) actually exercise
# SOAP::NetHttpClient end-to-end instead of it only ever being reachable
# when every other backend happens to fail to load.
+# http-access2 (httpclient's own predecessor, by the same author) used to
+# sit in this cascade too. Removed: the gem was renamed to httpclient years
+# ago and is no longer published on RubyGems.org at all, so that entry
+# could never actually load -- confirmed empirically (`gem install
+# http-access2` fails outright). Anyone still vendoring the old gem
+# directly (e.g. via a git ref) can select it by placing a matching
+# lib/soap/httpbackend/http_access2.rb adapter back on their own
+# $LOAD_PATH; see git history for the version that shipped here.
if ENV.has_key?('SOAP4R_HTTP_CLIENTS')
backend_list = ENV['SOAP4R_HTTP_CLIENTS'].to_s.split(',')
else
backend_list = [
'httpclient', ## Uses the httpclient gem
- 'http_access2', ## Uses the http-access2 gem ; no longer published on RubyGems.org
+ 'curb', ## Uses the curb gem (libcurl bindings) ; not installed by default, opt-in
+ 'faraday', ## Uses the faraday gem (itself pluggable -- see soap/faradayClient.rb) ; not installed by default, opt-in
'net_http', ## Falls back to this project's own wrapper around stdlib Net::HTTP
]
end
diff --git a/lib/soap/httpbackend/curb.rb b/lib/soap/httpbackend/curb.rb
new file mode 100644
index 00000000..0ade1dfa
--- /dev/null
+++ b/lib/soap/httpbackend/curb.rb
@@ -0,0 +1,7 @@
+# encoding: UTF-8
+# SOAP4R - HTTP client backend: the curb gem (libcurl bindings).
+
+require 'soap/httpbackend/registry'
+require 'soap/curbClient'
+
+SOAP::HTTPBackend.register(SOAP::CurbClient, true)
diff --git a/lib/soap/httpbackend/faraday.rb b/lib/soap/httpbackend/faraday.rb
new file mode 100644
index 00000000..d0a1644a
--- /dev/null
+++ b/lib/soap/httpbackend/faraday.rb
@@ -0,0 +1,7 @@
+# encoding: UTF-8
+# SOAP4R - HTTP client backend: the faraday gem.
+
+require 'soap/httpbackend/registry'
+require 'soap/faradayClient'
+
+SOAP::HTTPBackend.register(SOAP::FaradayClient, true)
diff --git a/lib/soap/httpbackend/http_access2.rb b/lib/soap/httpbackend/http_access2.rb
deleted file mode 100644
index 274fc5df..00000000
--- a/lib/soap/httpbackend/http_access2.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# encoding: UTF-8
-# SOAP4R - HTTP client backend: the http-access2 gem (httpclient's
-# predecessor, by the same author; no longer published on RubyGems.org, so
-# this adapter is effectively unreachable in practice today, but kept for
-# anyone still vendoring the gem directly).
-
-require 'soap/httpbackend/registry'
-require 'http-access2'
-
-if HTTPAccess2::VERSION < "2.0"
- raise LoadError.new("http-access2/2.0 or later is required.")
-end
-
-SOAP::HTTPBackend.register(HTTPAccess2::Client, true)
diff --git a/lib/wsdl/xmlSchema/importer.rb b/lib/wsdl/xmlSchema/importer.rb
index 2f474395..34a6214c 100644
--- a/lib/wsdl/xmlSchema/importer.rb
+++ b/lib/wsdl/xmlSchema/importer.rb
@@ -82,17 +82,9 @@ def web_client
require 'httpclient'
@web_client = HTTPClient
rescue LoadError
- begin
- require 'http-access2'
- if HTTPAccess2::VERSION < "2.0"
- raise LoadError.new("http-access/2.0 or later is required.")
- end
- @web_client = HTTPAccess2::Client
- rescue LoadError
- warn("Loading http-access2 failed. Net/http is used.") if $DEBUG
- require 'soap/netHttpClient'
- @web_client = ::SOAP::NetHttpClient
- end
+ warn("Loading httpclient failed. Net/http is used.") if $DEBUG
+ require 'soap/netHttpClient'
+ @web_client = ::SOAP::NetHttpClient
end
@web_client
end
diff --git a/lib/xsd/datatypes.rb b/lib/xsd/datatypes.rb
index f1bd246a..85c487dc 100644
--- a/lib/xsd/datatypes.rb
+++ b/lib/xsd/datatypes.rb
@@ -584,10 +584,18 @@ def screen_data(t)
t <<= 12 if t.year < 0
t
elsif t.is_a?(Time)
- jd = DateTime.send(:civil_to_jd, t.year, t.mon, t.mday, DateTime::ITALY)
- fr = DateTime.send(:time_to_day_fraction, t.hour, t.min, [t.sec, 59].min) + t.usec.to_r / DayInMicro
+ # Reached only on Rubies without Time#to_datetime (in practice, 1.8.7
+ # only, since the to_datetime branch above wins on every later
+ # version). The civil_to_jd/jd_to_ajd route below built a Rational
+ # day-fraction and hit the exact same Ruby 1.8.7 DateTime/Rational
+ # arithmetic bug documented in screen_data_str's secfrac handling
+ # below (confirmed: produced a bogus ~4714 BC date). Build via
+ # DateTime.civil directly with the sub-second fraction baked into
+ # the seconds argument instead, avoiding the buggy code path
+ # entirely, same fix as that other call site.
of = t.utc_offset.to_r / DayInSec
- DateTime.new!(DateTime.send(:jd_to_ajd, jd, fr, of), of, DateTime::ITALY)
+ secfrac = t.usec.to_r / SecInMicro
+ DateTime.civil(t.year, t.mon, t.mday, t.hour, t.min, t.sec + secfrac, of)
else
screen_data_str(t)
end
diff --git a/lib/xsd/xmlparser/oxparser.rb b/lib/xsd/xmlparser/oxparser.rb
index 52bf6068..ad03d390 100644
--- a/lib/xsd/xmlparser/oxparser.rb
+++ b/lib/xsd/xmlparser/oxparser.rb
@@ -23,11 +23,17 @@ def do_parse(string_or_readable)
handler = OxDocHandler.new(self, @decoder)
string = string_or_readable.respond_to?(:read) ? string_or_readable.read : StringIO.new(string_or_readable)
+ # :skip_none regardless of decoder: the old no-decoder default
+ # (:skip_return) discarded \r entirely, breaking CRLF round-tripping
+ # (see test_string_crlf) whenever htmlentities wasn't installed.
if @decoder.nil?
- # Use the built-in conversion with Ox.
- ::Ox.sax_parse(handler, string, {:symbolize=> false, :convert_special=> true, :skip=> :skip_return} )
+ # Ox's own :convert_special decodes the full named-entity set, but this
+ # path segfaults on complex documents under Ox 2.14.14 (the version
+ # Ruby 2.2.x-2.6.x are stuck on) -- htmlentities is required there
+ # specifically to avoid this branch entirely (see Gemfile).
+ ::Ox.sax_parse(handler, string, {:symbolize=> false, :convert_special=> true, :skip=> :skip_none} )
else
- # Use HTMLEntities Decoder. Leave the special-character conversion alone and let HTMLEntities decode it for us.
+ # Let HTMLEntities decode instead; leave Ox's own conversion off.
::Ox.sax_parse(handler, string, {:skip=> :skip_none})
end
end
diff --git a/test/soap/asp.net/test_aspdotnet.rb b/test/soap/asp.net/test_aspdotnet.rb
index 776ddb77..5451b25e 100644
--- a/test/soap/asp.net/test_aspdotnet.rb
+++ b/test/soap/asp.net/test_aspdotnet.rb
@@ -94,15 +94,8 @@ def test_aspdotnethandler
assert_equal("Hello Mike", @client.sayHello("Mike"))
end
- # Fixture/assertions below assume httpclient's wiredump format, so this
- # must check the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a
- # different one -- see lib/soap/httpbackend.rb), not merely whether the
- # gem happens to be loaded in this process (e.g. test/soap/ssl/test_ssl.rb
- # requires it unconditionally regardless of the active backend).
- if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient
-
- # qualified!
- REQUEST_ASPDOTNETHANDLER =
+ # qualified!
+ REQUEST_ASPDOTNETHANDLER =
%q[
]
- def test_aspdotnethandler_envelope
- @client = SOAP::RPC::Driver.new(Endpoint, Server::Namespace)
- @client.wiredump_dev = str = String.new
- @client.add_method_with_soapaction('sayHello', Server::Namespace + 'SayHello', 'name')
- @client.default_encodingstyle = SOAP::EncodingStyle::ASPDotNetHandler::Namespace
- assert_equal("Hello Mike", @client.sayHello("Mike"))
- assert_xml_equal(REQUEST_ASPDOTNETHANDLER, parse_requestxml(str),
- [REQUEST_ASPDOTNETHANDLER, parse_requestxml(str)].join("\n\n"))
- end
-
- def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
- end
+ def test_aspdotnethandler_envelope
+ @client = SOAP::RPC::Driver.new(Endpoint, Server::Namespace)
+ @client.wiredump_dev = str = String.new
+ @client.add_method_with_soapaction('sayHello', Server::Namespace + 'SayHello', 'name')
+ @client.default_encodingstyle = SOAP::EncodingStyle::ASPDotNetHandler::Namespace
+ assert_equal("Hello Mike", @client.sayHello("Mike"))
+ assert_xml_equal(REQUEST_ASPDOTNETHANDLER, parse_requestxml(str),
+ [REQUEST_ASPDOTNETHANDLER, parse_requestxml(str)].join("\n\n"))
+ end
+ def parse_requestxml(str)
+ TestUtil.parse_wiredump_request_body(str)
end
end
diff --git a/test/soap/auth/test_basic.rb b/test/soap/auth/test_basic.rb
index 912b39ad..a61ebc87 100644
--- a/test/soap/auth/test_basic.rb
+++ b/test/soap/auth/test_basic.rb
@@ -130,14 +130,19 @@ def test_proxy
private
- # SOAP::NetHttpClient#set_auth explicitly raises NotImplementedError --
- # soap4r + net/http never supported WWW-Authenticate-style auth. This is
- # a real, permanent backend limitation, not a bug, so skip cleanly under
- # SOAP4R_HTTP_CLIENTS=net_http instead of failing (see
- # lib/soap/httpbackend.rb; test_streamhandler.rb's test_basic_auth uses
- # the same pattern).
+ # SOAP::NetHttpClient#set_auth and SOAP::FaradayClient#set_auth both
+ # explicitly raise NotImplementedError -- neither soap4r + net/http nor
+ # soap4r + faraday (whose core has no bundled challenge-response
+ # middleware) support WWW-Authenticate-style auth. This is a real,
+ # permanent limitation for those backends, not a bug, so skip cleanly
+ # rather than fail (see lib/soap/httpbackend.rb;
+ # test_streamhandler.rb's test_basic_auth uses the same pattern).
+ # SOAP::CurbClient DOES support this (libcurl negotiates the challenge
+ # itself), so it's deliberately absent from this list.
+ AUTH_UNSUPPORTED_BACKENDS = %w[SOAP::NetHttpClient SOAP::FaradayClient]
+
def auth_supported?
- SOAP::HTTPStreamHandler::Client != SOAP::NetHttpClient
+ !AUTH_UNSUPPORTED_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name)
end
end
diff --git a/test/soap/auth/test_digest.rb b/test/soap/auth/test_digest.rb
index 53647c74..e73ee626 100644
--- a/test/soap/auth/test_digest.rb
+++ b/test/soap/auth/test_digest.rb
@@ -130,14 +130,19 @@ def test_proxy
private
- # SOAP::NetHttpClient#set_auth explicitly raises NotImplementedError --
- # soap4r + net/http never supported WWW-Authenticate-style auth. This is
- # a real, permanent backend limitation, not a bug, so skip cleanly under
- # SOAP4R_HTTP_CLIENTS=net_http instead of failing (see
- # lib/soap/httpbackend.rb; test_streamhandler.rb's test_basic_auth uses
- # the same pattern).
+ # SOAP::NetHttpClient#set_auth and SOAP::FaradayClient#set_auth both
+ # explicitly raise NotImplementedError -- neither soap4r + net/http nor
+ # soap4r + faraday (whose core has no bundled challenge-response
+ # middleware) support WWW-Authenticate-style auth. This is a real,
+ # permanent limitation for those backends, not a bug, so skip cleanly
+ # rather than fail (see lib/soap/httpbackend.rb;
+ # test_streamhandler.rb's test_basic_auth uses the same pattern).
+ # SOAP::CurbClient DOES support this (libcurl negotiates the challenge
+ # itself), so it's deliberately absent from this list.
+ AUTH_UNSUPPORTED_BACKENDS = %w[SOAP::NetHttpClient SOAP::FaradayClient]
+
def auth_supported?
- SOAP::HTTPStreamHandler::Client != SOAP::NetHttpClient
+ !AUTH_UNSUPPORTED_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name)
end
end
diff --git a/test/soap/ssl/test_ssl.rb b/test/soap/ssl/test_ssl.rb
index 717085f5..f750e0c8 100644
--- a/test/soap/ssl/test_ssl.rb
+++ b/test/soap/ssl/test_ssl.rb
@@ -5,17 +5,30 @@
require 'httpclient'
rescue LoadError
end
+begin
+ require 'curb'
+rescue LoadError
+end
+begin
+ require 'faraday'
+rescue LoadError
+end
require 'soap/rpc/driver'
# Checking defined?(HTTPClient) alone isn't enough now that the HTTP client
# backend is independently selectable (SOAP4R_HTTP_CLIENTS -- see
-# lib/soap/httpbackend.rb): the require above pulls in the gem regardless of
-# which backend SOAP::HTTPStreamHandler actually picked, so HTTPClient can be
-# defined while, say, SOAP::NetHttpClient (whose #ssl_config is always nil)
-# is the active one. Every assertion below assumes httpclient's SSLConfig
-# shape, so this must check the ACTIVE backend, not merely whether the gem
-# loaded.
-if SOAP::HTTPStreamHandler::Client == HTTPClient and defined?(OpenSSL)
+# lib/soap/httpbackend.rb): the requires above pull in these gems
+# regardless of which backend SOAP::HTTPStreamHandler actually picked, so
+# e.g. HTTPClient can be defined while SOAP::NetHttpClient (whose
+# #ssl_config is always nil) is the active one. This must check the ACTIVE
+# backend, not merely whether a gem happens to be loaded.
+#
+# httpclient, curb, and faraday (any SOAP4R_FARADAY_ADAPTER) all get real
+# SSL config plumbing exercised here -- SOAP::NetHttpClient is excluded
+# since it has no SSL configuration surface of its own at all (see
+# lib/soap/netHttpClient.rb).
+SSL_TESTABLE_BACKENDS = %w[HTTPClient SOAP::CurbClient SOAP::FaradayClient]
+if SSL_TESTABLE_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name) and defined?(OpenSSL)
module SOAP; module SSL
@@ -43,7 +56,38 @@ def teardown
teardown_server
end
+ def httpclient_backend?
+ SOAP::HTTPStreamHandler::Client == HTTPClient
+ end
+
+ def faraday_backend?
+ SOAP::HTTPStreamHandler::Client.name == 'SOAP::FaradayClient'
+ end
+
+ # Each backend surfaces a TLS verification/handshake failure as a
+ # different exception class -- confirmed empirically against the real
+ # server this file spins up, not guessed from documentation:
+ # httpclient: OpenSSL::SSL::SSLError
+ # curb: Curl::Err::CurlError (base class for its whole SSL error
+ # family -- SSLPeerCertificateError, SSLCACertificateError,
+ # SSLCypherError, etc. -- rather than pinning to one)
+ # faraday: Faraday::Error (SSLError and ConnectionFailed are BOTH
+ # direct subclasses of it, and different Faraday adapters
+ # raise different ones for the same failure -- confirmed
+ # :typhoeus raises ConnectionFailed, not SSLError)
+ def expected_ssl_error_class
+ case SOAP::HTTPStreamHandler::Client.name
+ when 'SOAP::CurbClient'
+ Curl::Err::CurlError
+ when 'SOAP::FaradayClient'
+ Faraday::Error
+ else
+ OpenSSL::SSL::SSLError
+ end
+ end
+
def test_options
+ return unless httpclient_backend?
cfg = @client.streamhandler.client.ssl_config
assert_nil(cfg.client_cert)
assert_nil(cfg.client_key)
@@ -70,7 +114,14 @@ def test_options
end
end
+ # verify_callback is not portable across backends: no libcurl-based
+ # backend (curb, or Faraday riding on typhoeus/patron/etc.) exposes a
+ # per-certificate Ruby callback hook the way OpenSSL::SSL::SSLContext
+ # does -- confirmed there's no equivalent in either library's public API.
+ # See test_ca_verification below for the backend-neutral equivalent of
+ # what this test covers, minus the callback-specific assertions.
def test_verification
+ return unless httpclient_backend?
cfg = @client.options
cfg["protocol.http.ssl_config.verify_callback"] = method(:verify_callback).to_proc
begin
@@ -132,7 +183,9 @@ def test_verification
assert_equal("Hello World, from ssl client", @client.hello_world("ssl client"))
end
+ # Also verify_callback-dependent throughout -- see test_verification above.
def test_property
+ return unless httpclient_backend?
testpropertyname = File.join(DIR, 'soapclient.properties')
File.open(testpropertyname, "w") do |f|
f<<<<__EOP__
@@ -200,18 +253,73 @@ def test_ciphers
#cfg.timeout = 123
cfg["protocol.http.ssl_config.ciphers"] = "!ALL"
#
- begin
- @client.hello_world("ssl client")
- assert(false)
- rescue OpenSSL::SSL::SSLError => ssle
- # depends on OpenSSL version. (?:0.9.8|0.9.7)
- assert_match(/\A(?:SSL_CTX_set_cipher_list:+ no cipher match|no ciphers available)\z/, ssle.message)
+ if faraday_backend?
+ # Confirmed empirically: Faraday's own :typhoeus adapter never
+ # forwards Faraday::SSLOptions#ciphers to ethon's ssl_cipher_list at
+ # all, so "!ALL" is silently ignored rather than rejected -- a real
+ # gap in that third-party adapter (not this bridge, and not
+ # something soap4r-ng's own code can fix). The handshake succeeds
+ # here regardless of the (unhonored) cipher restriction.
+ assert_equal("Hello World, from ssl client", @client.hello_world("ssl client"))
+ else
+ begin
+ @client.hello_world("ssl client")
+ assert(false)
+ rescue expected_ssl_error_class => ssle
+ if httpclient_backend?
+ # depends on OpenSSL version. (?:0.9.8|0.9.7)
+ assert_match(/\A(?:SSL_CTX_set_cipher_list:+ no cipher match|no ciphers available)\z/, ssle.message)
+ end
+ # curb's own message ("Could not use specified SSL cipher: failed
+ # setting cipher list: !ALL") is asserted only by class above --
+ # its exact wording isn't pinned since it's libcurl/OpenSSL-version
+ # dependent the same way httpclient's is.
+ end
end
#
cfg["protocol.http.ssl_config.ciphers"] = "ALL"
assert_equal("Hello World, from ssl client", @client.hello_world("ssl client"))
end
+ # Backend-neutral equivalent of test_verification/test_property, minus
+ # the verify_callback-specific assertions those can't support on
+ # anything but httpclient (see the comment on test_verification). This
+ # is what actually matters for "does soap4r-ng's own config-loading
+ # bridge correctly plumb ca_file/client_cert/client_key through to a real
+ # TLS handshake" -- runs identically for every SSL-testable backend.
+ def test_ca_verification
+ cfg = @client.options
+ cfg["protocol.http.ssl_config.client_cert"] = File.join(DIR, "client.cert")
+ cfg["protocol.http.ssl_config.client_key"] = File.join(DIR, "client.key")
+ #
+ # No ca_file configured at all -- client has no reason to trust this
+ # private test CA, so verification must fail regardless of backend.
+ assert_ssl_verification_fails { @client.hello_world("ssl client") }
+ #
+ # Wrong CA (the root, not the intermediate that actually signed the
+ # server's cert) -- still fails.
+ cfg["protocol.http.ssl_config.ca_file"] = File.join(DIR, "ca.cert")
+ assert_ssl_verification_fails { @client.hello_world("ssl client") }
+ #
+ # Correct CA (the signing intermediate) -- succeeds.
+ cfg["protocol.http.ssl_config.ca_file"] = File.join(DIR, "subca.cert")
+ assert_equal("Hello World, from ssl client", @client.hello_world("ssl client"))
+ end
+
+ # test-unit's assert_raise wants an EXACT class match, not
+ # kind_of?/subclass-inclusive like plain Ruby rescue -- confirmed
+ # empirically (curb/Faraday both raise a specific subclass of the base
+ # class expected_ssl_error_class returns, e.g. Curl::Err::SSLPeerCertificateError
+ # rather than bare Curl::Err::CurlError, and assert_raise rejected it).
+ # Every other SSL-exception check in this file already routes around
+ # that via begin/rescue instead of assert_raise; this just gives
+ # test_ca_verification the same treatment.
+ def assert_ssl_verification_fails
+ yield
+ assert(false, "expected an SSL verification failure")
+ rescue expected_ssl_error_class
+ end
+
private
def setup_server
diff --git a/test/soap/test_cookie.rb b/test/soap/test_cookie.rb
index 9db26d75..5463bdf7 100644
--- a/test/soap/test_cookie.rb
+++ b/test/soap/test_cookie.rb
@@ -103,15 +103,19 @@ def do_server_proc(req, res)
end
+ # SOAP::NetHttpClient/CurbClient/FaradayClient all expose #request_filter
+ # (so streamHandler.rb's respond_to?(:request_filter) check passes and
+ # wires this filter in), but none of them actually invoke it anywhere in
+ # their own request/response code -- it's a genuinely inert accessor on
+ # all three, like #ssl_config is for NetHttpClient. Only httpclient (which
+ # has its own native filter-chain support) really honors it.
+ FILTER_CHAIN_INERT_BACKENDS = %w[SOAP::NetHttpClient SOAP::CurbClient SOAP::FaradayClient]
+
def test_normal
- # SOAP::NetHttpClient exposes #request_filter (so streamHandler.rb's
- # respond_to?(:request_filter) check passes and wires this filter in),
- # but never actually invokes it anywhere in its own request/response
- # code -- it's a genuinely inert accessor, like #ssl_config. Skip
- # cleanly under that backend rather than fail on a feature it was never
- # wired up to support (see lib/soap/httpbackend.rb for how the active
- # backend is selected/forced).
- return if SOAP::HTTPStreamHandler::Client == SOAP::NetHttpClient
+ # Skip cleanly under a backend that never wired this up, rather than
+ # fail on a feature it doesn't support (see lib/soap/httpbackend.rb for
+ # how the active backend is selected/forced).
+ return if FILTER_CHAIN_INERT_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name)
@client.wiredump_dev = STDOUT if $DEBUG
filter = CookieFilter.new
@client.streamhandler.filterchain << filter
diff --git a/test/soap/test_nethttpclient.rb b/test/soap/test_nethttpclient.rb
index f0cabf3f..f8733b94 100644
--- a/test/soap/test_nethttpclient.rb
+++ b/test/soap/test_nethttpclient.rb
@@ -7,12 +7,12 @@ module SOAP
# Covers SOAP::NetHttpClient#create_connection directly (via #send, since
# it's private) rather than through the full driver/streamHandler stack:
-# by default HTTPStreamHandler only falls back to NetHttpClient when both
-# httpclient and http-access2 fail to load, and this project's Gemfile
-# always installs httpclient, so nothing else in the suite exercises this
-# file under the default backend order. create_connection only builds and
-# configures a Net::HTTP object (no #start call), so this is safe to test
-# without a real network call or proxy server.
+# by default HTTPStreamHandler only falls back to NetHttpClient when
+# httpclient fails to load, and this project's Gemfile always installs
+# httpclient, so nothing else in the suite exercises this file under the
+# default backend order. create_connection only builds and configures a
+# Net::HTTP object (no #start call), so this is safe to test without a real
+# network call or proxy server.
#
# For real end-to-end coverage of this backend (live WEBrick round trips,
# proxying, basic auth, etc.), force it via SOAP4R_HTTP_CLIENTS=net_http --
diff --git a/test/soap/test_no_indent.rb b/test/soap/test_no_indent.rb
index 84454879..3fab57d8 100644
--- a/test/soap/test_no_indent.rb
+++ b/test/soap/test_no_indent.rb
@@ -1,15 +1,9 @@
# encoding: UTF-8
require 'helper'
+require 'testutil'
require 'soap/rpc/standaloneServer'
require 'soap/rpc/driver'
-# Assertions below assume httpclient's wiredump format, so this must check
-# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see
-# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded
-# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally
-# regardless of the active backend).
-if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient
-
module SOAP
@@ -90,11 +84,9 @@ def test_no_indent
end
def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
+ TestUtil.parse_wiredump_request_body(str)
end
end
-end
-
end
diff --git a/test/soap/test_streamhandler.rb b/test/soap/test_streamhandler.rb
index 0ec509ab..e8105087 100644
--- a/test/soap/test_streamhandler.rb
+++ b/test/soap/test_streamhandler.rb
@@ -189,16 +189,26 @@ def test_basic_auth
assert_nil(@client.do_server_proc_basic_auth)
end
+ # Every backend that owns a NO_PROXY_HOSTS constant treats 'localhost' as
+ # never-proxied by default (see e.g. lib/soap/netHttpClient.rb,
+ # lib/soap/curbClient.rb, lib/soap/faradayClient.rb) -- this test needs
+ # the opposite for its own assertions, so it clears whichever class is
+ # ACTIVE, not a hardcoded HTTPClient-or-NetHttpClient binary choice (that
+ # binary check predates SOAP4R_HTTP_CLIENTS making more than two backends
+ # reachable -- see lib/soap/httpbackend.rb).
+ def no_proxy_hosts_holder
+ # const_defined?'s 2-arg (inherit) form is Ruby 1.9+ only -- the 1-arg
+ # form works identically here across every supported Ruby version since
+ # SOAP::HTTPStreamHandler::Client itself (not some ancestor) is always
+ # what actually defines NO_PROXY_HOSTS on every backend that has one.
+ SOAP::HTTPStreamHandler::Client.const_defined?(:NO_PROXY_HOSTS) ?
+ SOAP::HTTPStreamHandler::Client : nil
+ end
+
def test_proxy
- # See test_basic_auth above for why this checks the ACTIVE backend
- # rather than merely whether HTTPClient happens to be loaded.
- if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient
- backup = HTTPClient::NO_PROXY_HOSTS.dup
- HTTPClient::NO_PROXY_HOSTS.clear
- else
- backup = SOAP::NetHttpClient::NO_PROXY_HOSTS.dup
- SOAP::NetHttpClient::NO_PROXY_HOSTS.clear
- end
+ holder = no_proxy_hosts_holder
+ backup = holder ? holder::NO_PROXY_HOSTS.dup : nil
+ holder::NO_PROXY_HOSTS.clear if holder
setup_proxyserver
str = String.new
@client.wiredump_dev = str
@@ -211,11 +221,8 @@ def test_proxy
@client.options["protocol.http.proxy"] = 'ftp://foo:8080'
end
ensure
- if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient
- HTTPClient::NO_PROXY_HOSTS.replace(backup)
- else
- SOAP::NetHttpClient::NO_PROXY_HOSTS.replace(backup)
- end
+ holder = no_proxy_hosts_holder
+ holder::NO_PROXY_HOSTS.replace(backup) if holder && backup
end
def test_charset
diff --git a/test/testutil.rb b/test/testutil.rb
index 815e3c09..369e8c5c 100644
--- a/test/testutil.rb
+++ b/test/testutil.rb
@@ -61,6 +61,27 @@ def self.webrick_proxy_server(options)
webrick_server(WEBrick::HTTPProxyServer, options)
end
+ # Extracts the request body XML out of a wiredump_dev capture. Every HTTP
+ # client backend (see lib/soap/httpbackend.rb) writes the same block
+ # layout -- "Wire dump:", "= Request", the raw request-line+headers, the
+ # body, "= Response", the raw response-line+headers, the response body --
+ # separated by blank lines, so the body is always the 4th block (index 3)
+ # regardless of which backend produced the dump. This was confirmed by
+ # design, not by accident: SOAP::NetHttpClient/CurbClient/FaradayClient
+ # were all built to match httpclient's own block layout exactly (see the
+ # "Mirrors httpclient's wiredump block layout" comments in
+ # lib/soap/netHttpClient.rb, curbClient.rb, and faradayClient.rb) precisely
+ # so callers like this one don't need to special-case any of them.
+ def self.parse_wiredump_request_body(str)
+ str.split(/\r?\n\r?\n/)[3]
+ end
+
+ # Same deal, for the response body (block index 6 -- see
+ # parse_wiredump_request_body above for the full block layout).
+ def self.parse_wiredump_response_body(str)
+ str.split(/\r?\n\r?\n/)[6]
+ end
+
def self.webrick_server(klass, options)
try = 0
begin
diff --git a/test/wsdl/anonymous/test_anonymous.rb b/test/wsdl/anonymous/test_anonymous.rb
index 9d3e1ed1..28810cef 100644
--- a/test/wsdl/anonymous/test_anonymous.rb
+++ b/test/wsdl/anonymous/test_anonymous.rb
@@ -6,13 +6,6 @@
require 'soap/wsdlDriver'
-# Assertions below assume httpclient's wiredump format, so this must check
-# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see
-# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded
-# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally
-# regardless of the active backend).
-if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient
-
module WSDL; module Anonymous
@@ -138,5 +131,3 @@ def test_anonymous_mapping
end; end
-
-end
diff --git a/test/wsdl/qualified/test_qualified.rb b/test/wsdl/qualified/test_qualified.rb
index 5540128b..515882ad 100644
--- a/test/wsdl/qualified/test_qualified.rb
+++ b/test/wsdl/qualified/test_qualified.rb
@@ -6,13 +6,6 @@
require 'soap/wsdlDriver'
-# Assertions below assume httpclient's wiredump format, so this must check
-# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see
-# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded
-# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally
-# regardless of the active backend).
-if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient
-
module WSDL
@@ -143,11 +136,9 @@ def test_naive
end
def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
+ TestUtil.parse_wiredump_request_body(str)
end
end
-end
-
end
diff --git a/test/wsdl/qualified/test_unqualified.rb b/test/wsdl/qualified/test_unqualified.rb
index 1d3163a2..cc87cabd 100644
--- a/test/wsdl/qualified/test_unqualified.rb
+++ b/test/wsdl/qualified/test_unqualified.rb
@@ -6,13 +6,6 @@
require 'soap/wsdlDriver'
-# Assertions below assume httpclient's wiredump format, so this must check
-# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see
-# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded
-# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally
-# regardless of the active backend).
-if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient
-
module WSDL
@@ -144,11 +137,9 @@ def test_naive
end
def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
+ TestUtil.parse_wiredump_request_body(str)
end
end
-end
-
end
diff --git a/test/wsdl/rpc/test_rpc_lit.rb b/test/wsdl/rpc/test_rpc_lit.rb
index 4f27f176..66995b1e 100644
--- a/test/wsdl/rpc/test_rpc_lit.rb
+++ b/test/wsdl/rpc/test_rpc_lit.rb
@@ -6,13 +6,6 @@
require 'soap/wsdlDriver'
-# Assertions below assume httpclient's wiredump format, so this must check
-# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see
-# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded
-# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally
-# regardless of the active backend).
-if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient and defined?(OpenSSL)
-
module WSDL; module RPC
@@ -468,15 +461,13 @@ def test_stub_echoStructArray
end
def parse_requestxml(str)
- str.split(/\r?\n\r?\n/)[3]
+ TestUtil.parse_wiredump_request_body(str)
end
def parse_responsexml(str)
- str.split(/\r?\n\r?\n/)[6]
+ TestUtil.parse_wiredump_response_body(str)
end
end
end; end
-
-end