savon 2.17.1 → 2.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00ae14802f108cb0df2757b85888a35df4b6b0e462ae0d01c1eedc9b4f9c123c
4
- data.tar.gz: cee1d2d13ed6e869611817271c43352c470c4c7fb79e72dc7e6e7a4d9ac37191
3
+ metadata.gz: cde79beaab26839d499267e94eee281c4f35f34906fd6dfdd4fc4590dc2c710a
4
+ data.tar.gz: 93f661f0156d8331a68e440176d9aaef92d2744974858ee4f866e63f14a210a1
5
5
  SHA512:
6
- metadata.gz: 8b754234ac51e2213f92254a88345743cc54e09026e220d85931b8bfe0ba78f5a417f81e561047b37410f52713f3583120c6d3095ab68a2b9705427f03822d07
7
- data.tar.gz: 49a3260e064073d9370b80b376db5f9c75bab5e38dbdf7db2ffc8533dc2a58652bb0699830468f1e59319a98fd7210503a6c999b386e3ec6c9f4d23ac7f813e1
6
+ metadata.gz: 3f7436bc2d8c53b89a96a373ab015ce7461b86501549034b99a5ae585a8f25d113bd7e1d1897acc613360d8487bb198eeccf5d0cf4835be7e8f8e44dd4b637a3
7
+ data.tar.gz: 8e45f5f6494c8256878b54b4092f7a253686ddc5c9ea1f98cd372c9ed4b0c784a4b7c885604c70036a1398fe0ac5ff08847a13def8ed931c8ce7d5da6f7475df
data/CHANGELOG.md CHANGED
@@ -1,38 +1,99 @@
1
1
  # Savon changelog
2
2
 
3
- ## 2.17.1 (2026-05-21)
3
+ All notable changes to this project will be documented in this file.
4
4
 
5
- * Fix: [#1008](https://github.com/savonrb/savon/pull/1008) - The HTTPI and Faraday transports no longer set an explicit `Content-Length` request header. The underlying HTTP library already computes it from the body; sending it as well produced a duplicate header on adapters that do not deduplicate (e.g. httpclient), which some servers reject.
6
- * Fix: Requests using `attachments` were sent with a plain `text/xml` Content-Type instead of `multipart/related`. The 2.17.0 transport refactor assembled the request headers before the multipart body was built, leaving `Builder#multipart` empty at header time, so servers received a multipart body labelled as plain XML. 2.16.x and earlier are unaffected.
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## 2.17.0 (2026-05-19)
8
+ ## [2.17.3] - 2026-06-23
9
+
10
+ ### Fixed
11
+
12
+ **Fix Savon::HTTPError compatibility with Faraday transport**
13
+
14
+ * **`Savon::HTTPError` works with the Faraday transport** ([#1050](https://github.com/savonrb/savon/issues/1050)). When a the WSDL could not be fetched under `transport: :faraday`, `Savon::HTTPError#to_s` and `#to_hash` raised `NoMethodError: undefined method 'code'` because they were handed a raw `Faraday::Response`, which exposes `#status` rather than `#code`. Transports now normalize adapter-specific response objects into `Savon::Transport::Response` before they reach `Savon::HTTPError`.
15
+
16
+ ## [2.17.2] - 2026-06-10
17
+
18
+ **Fix CVE-2026-53510 and restore 2.17.0 cookie regressions**
19
+
20
+ ### Fixed
21
+
22
+ * **Fix CVE-2026-53510** `Savon::Model` generated SOAP operation methods by interpolating operation names into Ruby source passed to `module_eval`. An attacker who can control the operation names of a WSDL, can inject Ruby code that executes in the application process. This affects only the `.all_operations` class method provided by `Savon::Model` to automatically register all operations provided by the WSDL. Configuring `Savon::Model` with trusted operation names via `.operations` is safe. Thanks to @connorshea for securely disclosing this, providing a proof and a great report.
23
+ * **`:cookies` request option works again.** The 2.17.0 transport refactor reimplemented cookie handling on top of `Array#map`, which broke callers passing an object that responds to `#cookies` and lost cookie-name de-duplication via `HTTPI::CookieStore`. The HTTPI transport delegates to `HTTPI::Request#set_cookies` again, restoring both shapes.
24
+ * **`response.http.cookies` works again.** 2.17.0's `Savon::Transport::Response` only exposed `code`, `headers`, and `body`. The HTTPI transport now returns `Array<HTTPI::Cookie>` (matching 2.12.1). The Faraday transport returns `Hash<String, String>` so Faraday callers do not need HTTPI types.
25
+ * **`:attachments` now works with a user-supplied `:xml` envelope** ([#761](https://github.com/savonrb/savon/pull/761)). Multipart support shipped in 2.13.0 but only wrapped envelopes Savon built itself. When a caller passed their own `:xml`, attachments were silently dropped.
26
+
27
+ ### Added
28
+
29
+ * **Faraday `:cookies` option accepts a `String` or `Hash`.** Strings are used verbatim, Hashes are formatted as `"name=value; name=value"`. Round-trippable with the Faraday response shape.
30
+ * **Three Nori response-parsing options exposed as Savon globals:** `:empty_tag_value` (default `nil`), `:convert_dashes_to_underscores` (default `true`), and `:scrub_xml` (default `true`). Defaults match Nori's own for backwards compatibility.
31
+
32
+ ### Changed
33
+
34
+ * **Minimum Nori version is now `~> 2.7`** (was `~> 2.4`). Needed for the new parsing options (`:empty_tag_value` arrived in Nori 2.6.0, `:scrub_xml` in 2.7.0). The 2.5–2.7 series also brings fixes callers benefit from automatically: invalid byte sequences parse instead of raising, REXML no longer turns `&lt;` inside CDATA into `<`, `xs:date`/`xs:time`/`xs:dateTime` typecasting was corrected, and Nori stopped monkey-patching `String` and `Object`.
35
+ * **Faraday migration hints are now value-aware and verified.** Each hint prints the caller's actual option value and spells out the full gem/require/setup where needed. Fixed several incorrect examples and added tests to verify every hint.
36
+
37
+ ### Deprecated
38
+
39
+ * Deprecated the global and local `:multipart` options. They have been no-ops since v2.13.0. Specifically since commit 4e7ae5e. Savon detects multipart responses by checking the `Content-Type` header.
40
+
41
+ ## [2.17.1] - 2026-05-21
42
+
43
+ ### Fixed
44
+
45
+ * [#1008](https://github.com/savonrb/savon/pull/1008) - The HTTPI and Faraday transports no longer set an explicit `Content-Length` request header. The underlying HTTP library already computes it from the body. Sending it as well produced a duplicate header on adapters that do not deduplicate (e.g. httpclient), which some servers reject.
46
+ * Requests using `attachments` were sent with a plain `text/xml` Content-Type instead of `multipart/related`. The 2.17.0 transport refactor assembled the request headers before the multipart body was built, leaving `Builder#multipart` empty at header time, so servers received a multipart body labelled as plain XML. 2.16.x and earlier are unaffected.
47
+
48
+ ## [2.17.0] - 2026-05-19
9
49
 
10
50
  **Add opt-in Faraday transport**
11
51
 
12
52
  Callers who set `transport: :faraday` get a memoized `Faraday::Connection` via `client.faraday` and full control over middleware, SSL, auth, and timeouts. Callers who do not set this option see no behavior change. HTTPI remains the default for 2.x.
13
53
 
14
- * Add: `transport: :faraday` global option. Defaults to `:httpi` (#992).
15
- * Add: `client.faraday` returns a memoized `Faraday::Connection` for configuring middleware, SSL, auth, and timeouts when using the Faraday transport.
16
- * Add: `Savon.client` raises if `transport: :faraday` is set but the faraday gem is not installed, or if any httpi-specific global option (`proxy`, timeouts, `ssl`, auth, `adapter`) is set alongside it. All conflicts are reported with their Faraday equivalents.
17
- * Change: Observers must return `Savon::Transport::Response` (or `nil`) instead of `HTTPI::Response`. Returning `HTTPI::Response` still works but emits a deprecation warning.
18
- * Unblocks:
19
- * redirect following for WSDL fetches via `faraday-follow-redirects` middleware (#1033, savonrb/wasabi#18)
20
- * digest authentication via `faraday-digestauth` middleware (#1021, savonrb/httpi#250)
21
- * proxy authentication with special characters in passwords (#941)
22
- * and setting an `Accept` header for WSDL requests from Rails apps (savonrb/wasabi#115)
54
+ ### Added
55
+
56
+ * `transport: :faraday` global option. Defaults to `:httpi` (#992).
57
+ * `client.faraday` returns a memoized `Faraday::Connection` for configuring middleware, SSL, auth, and timeouts when using the Faraday transport.
58
+ * `Savon.client` raises if `transport: :faraday` is set but the faraday gem is not installed, or if any httpi-specific global option (`proxy`, timeouts, `ssl`, auth, `adapter`) is set alongside it. All conflicts are reported with their Faraday equivalents.
59
+
60
+ ### Changed
61
+
62
+ * Observers must return `Savon::Transport::Response` (or `nil`) instead of `HTTPI::Response`.
63
+
64
+ ### Deprecated
65
+
66
+ * The HTTPI transport (currently the default) and all HTTPI-specific global options enumerated in `Savon::FaradayMigrationHint::OPTIONS` (`proxy`, `open_timeout`, `read_timeout`, `write_timeout`, the `ssl_*` family, `basic_auth`, `digest_auth`, `ntlm`, `follow_redirects`, `adapter`) will be removed in 3.0. Migrate to `transport: :faraday`. `FaradayMigrationHint` shows the Faraday equivalent for each option.
67
+ * Returning `HTTPI::Response` from observers emits a deprecation warning and will be removed in 3.0. Return `Savon::Transport::Response` instead.
68
+
69
+ The Faraday transport unblocks:
23
70
 
24
- ## 2.16.0 (2026-05-18)
71
+ * redirect following for WSDL fetches via `faraday-follow-redirects` middleware (#1033, savonrb/wasabi#18)
72
+ * digest authentication via `faraday-digestauth` middleware (#1021, savonrb/httpi#250)
73
+ * proxy authentication with special characters in passwords (#941)
74
+ * and setting an `Accept` header for WSDL requests from Rails apps (savonrb/wasabi#115)
75
+
76
+ ## [2.16.0] - 2026-05-18
25
77
 
26
78
  **Restore compatibility**
27
79
 
28
80
  If you stayed on 2.12.1 because a later version broke something, this release is for you. The fixes below target the most commonly reported upgrade blockers. Existing code should work without modification.
29
81
 
30
- * Fix: Restore `Savon::Response#hash` removed in 2.14.0 (#985). Callers on 2.12.1 that use `response.hash` get the soap body back instead of Ruby's integer object id. A deprecation warning is emitted on each call. Use `#full_hash` going forward.
31
- * Fix: Require wasabi >= 5.1.0 (#1015, #1016). Wasabi 4.x used message names as soap body element names and SOAPAction header values instead of operation names (savonrb/wasabi#122), causing servers to return a fault or reject the action for operations whose message name carried an `In` suffix.
32
- * Fix: Stop dumping all WSDL namespaces into every soap envelope (#1014, #942). 2.13.0 injected every namespace from the entire WSDL document into each request, including structural ones that have no place in a request body. Strict servers reject envelopes with unexpected or duplicate declarations.
33
- * Fix: Raise a proper `SOAPFault` instead of a raw exception when `soap:Fault` contains invalid encoding (#923).
34
- * Fix: `SOAPFault.present?` was ignoring its `xml` argument and always operating on the instance's own body.
35
- * Change: Added Ruby 3.4 (#1024) and Ruby 4.0 (#1039) to the CI test matrix.
82
+ ### Fixed
83
+
84
+ * Restore `Savon::Response#hash` removed in 2.14.0 (#985). Callers on 2.12.1 that use `response.hash` get the soap body back instead of Ruby's integer object id.
85
+ * Require wasabi >= 5.1.0 (#1015, #1016). Wasabi 4.x used message names as soap body element names and SOAPAction header values instead of operation names (savonrb/wasabi#122), causing servers to return a fault or reject the action for operations whose message name carried an `In` suffix.
86
+ * Stop dumping all WSDL namespaces into every soap envelope (#1014, #942). 2.13.0 injected every namespace from the entire WSDL document into each request, including structural ones that have no place in a request body. Strict servers reject envelopes with unexpected or duplicate declarations.
87
+ * Raise a proper `SOAPFault` instead of a raw exception when `soap:Fault` contains invalid encoding (#923).
88
+ * `SOAPFault.present?` was ignoring its `xml` argument and always operating on the instance's own body.
89
+
90
+ ### Changed
91
+
92
+ * Added Ruby 3.4 (#1024) and Ruby 4.0 (#1039) to the CI test matrix.
93
+
94
+ ### Deprecated
95
+
96
+ * `Savon::Response#hash` emits a deprecation warning on each call. Use `#full_hash` going forward. Will be removed in 3.0.
36
97
 
37
98
  ## 2.15.1 (2024-07-08)
38
99
 
@@ -1186,3 +1247,8 @@ Pay attention to the following list and read the updated Wiki: http://wiki.githu
1186
1247
  ## 0.5.0 (2009-11-29)
1187
1248
 
1188
1249
  * Complete rewrite and public release.
1250
+
1251
+ [2.17.2]: https://github.com/savonrb/savon/compare/v2.17.1...v2.17.2
1252
+ [2.17.1]: https://github.com/savonrb/savon/compare/v2.17.0...v2.17.1
1253
+ [2.17.0]: https://github.com/savonrb/savon/compare/v2.16.0...v2.17.0
1254
+ [2.16.0]: https://github.com/savonrb/savon/compare/v2.15.1...v2.16.0
data/README.md CHANGED
@@ -6,7 +6,7 @@ Heavy metal SOAP client
6
6
  [![Gem Version](https://badge.fury.io/rb/savon.svg)](http://badge.fury.io/rb/savon)
7
7
  [![Coverage Status](https://coveralls.io/repos/savonrb/savon/badge.svg)](https://coveralls.io/r/savonrb/savon)
8
8
 
9
- Savon is a SOAP client for Ruby. [SOAP is the protocol](https://www.w3.org/TR/soap/) spoken by many enterprise systems in banking, government, ERP or payroll. When they hand you a WSDL URL or file instead of a REST spec, it's SOAP. Savon reads the WSDL, maps available operations to Ruby symbols, converts Ruby hashes to SOAP envelopes and turns XML responses into hashes you can work with.
9
+ Savon is a SOAP client for Ruby. [SOAP is the protocol](https://www.w3.org/TR/soap/) spoken by many enterprise systems. When they hand you a WSDL URL or file instead of a REST spec, it's SOAP. Savon reads the WSDL, maps available operations to Ruby symbols, converts Ruby hashes to SOAP envelopes and turns XML responses into hashes you can work with.
10
10
 
11
11
  Full documentation is at [savonrb.com](https://savonrb.com).
12
12
 
@@ -68,7 +68,17 @@ Savon uses [HTTPI](https://github.com/savonrb/httpi) for HTTP by default. Since
68
68
 
69
69
  ## Ruby support
70
70
 
71
- Savon 2.x requires Ruby 3.0 or later. See the [changelog](CHANGELOG.md) for historical compatibility.
71
+ Savon 2.x requires Ruby >= 3.0.0, kept as the lower bound for backward compatibility. Note that Ruby 3.0–3.3 are EOL or security-only. Ruby 3.4+ is the minimum version with active maintenance.
72
+
73
+ ## Versioning & stability
74
+
75
+ Savon follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). The changelog format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
76
+
77
+ **The 2.x line is anchored on 2.12.1.** Every 2.x release is supposed to be safe to upgrade to from 2.12.1, and anything that worked in 2.12.1 keeps working. We do not remove or rename public APIs in the 2.x line. New behavior is opt-in and requires an explicit option change. If you found a problem with that, please let us know.
78
+
79
+ We only soft-deprecate APIs we plan to remove. The `Deprecated` sections of the [changelog](CHANGELOG.md) list every API that is planned to be removed with version 3.0.
80
+
81
+ Callers on 2.13.0–2.15.x may see specific post-2.12.1 behaviors restored to the 2.12.1 contract. This is intentional.
72
82
 
73
83
  ## Known limitations
74
84
 
data/lib/savon/builder.rb CHANGED
@@ -70,10 +70,17 @@ module Savon
70
70
  @body_attributes ||= @signature.nil? ? {} : @signature.body_attributes
71
71
  end
72
72
 
73
+ # Returns the request body as a String. When the caller supplies a pre-built
74
+ # envelope via the :xml local option it is used verbatim, but it must still
75
+ # be wrapped in a multipart message when :attachments are present.
76
+ # Otherwise the attachments are silently dropped.
73
77
  def to_s
74
- return @locals[:xml] if @locals.include? :xml
75
-
76
- build_document
78
+ if @locals.include?(:xml)
79
+ xml = @locals[:xml]
80
+ @locals[:attachments] ? build_multipart_message(xml) : xml
81
+ else
82
+ build_document
83
+ end
77
84
  end
78
85
 
79
86
  private
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Savon
6
+ # Formats the replacement Faraday setup for one HTTPI-only option.
7
+ class FaradayMigrationHint
8
+ VALUE_AWARE_OPTIONS = %i[
9
+ proxy
10
+ open_timeout
11
+ read_timeout
12
+ write_timeout
13
+ ssl_version
14
+ ssl_min_version
15
+ ssl_max_version
16
+ ssl_verify_mode
17
+ ssl_cert_key_file
18
+ ssl_cert_file
19
+ ssl_ca_cert_file
20
+ ssl_ciphers
21
+ ssl_ca_cert_path
22
+ adapter
23
+ ].freeze
24
+
25
+ STATIC_HINTS = {
26
+ ssl_cert_key: "client.faraday.ssl.client_key = key",
27
+ ssl_cert_key_password: [
28
+ "key = OpenSSL::PKey.read(File.read(key_path), password)",
29
+ "client.faraday.ssl.client_key = key"
30
+ ],
31
+ ssl_cert: "client.faraday.ssl.client_cert = cert",
32
+ ssl_ca_cert: [
33
+ "store = OpenSSL::X509::Store.new",
34
+ "store.set_default_paths",
35
+ "store.add_cert(cert)",
36
+ "client.faraday.ssl.cert_store = store"
37
+ ],
38
+ ssl_cert_store: "client.faraday.ssl.cert_store = store",
39
+ basic_auth: "client.faraday.request :authorization, :basic, user, pass",
40
+ digest_auth: [
41
+ "gem 'faraday-digestauth'",
42
+ "require 'faraday/digestauth'",
43
+ "client.faraday.request :digest, user, pass"
44
+ ],
45
+ ntlm: [
46
+ "gem 'faraday-ntlm_auth'",
47
+ "require 'faraday/ntlm_auth'",
48
+ "client.faraday.adapter :net_http_persistent",
49
+ "client.faraday.request :ntlm_auth, auth: [user, pass, domain]"
50
+ ],
51
+ follow_redirects: [
52
+ "gem 'faraday-follow_redirects'",
53
+ "require 'faraday/follow_redirects'",
54
+ "client.faraday.response :follow_redirects"
55
+ ]
56
+ }.freeze
57
+
58
+ OPTIONS = (VALUE_AWARE_OPTIONS + STATIC_HINTS.keys).freeze
59
+
60
+ def initialize(option, value)
61
+ @option = option
62
+ @value = value
63
+ end
64
+
65
+ def message
66
+ hint = hint_lines
67
+ return " #{option} - Use: #{hint}" unless hint.is_a?(Array)
68
+
69
+ " #{option} - Use:\n#{hint.map { |line| " #{line}" }.join("\n")}"
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :option, :value
75
+
76
+ def hint_lines
77
+ case option
78
+ when :proxy
79
+ "client.faraday.proxy = #{redacted_proxy_value}"
80
+ when :open_timeout, :read_timeout, :write_timeout
81
+ "client.faraday.options.#{option} = #{value.inspect}"
82
+ when :ssl_version, :ssl_min_version, :ssl_max_version
83
+ ssl_version_hint
84
+ when :ssl_verify_mode
85
+ ssl_verify_mode_hint
86
+ when :ssl_cert_key_file
87
+ ssl_cert_key_file_hint
88
+ when :ssl_cert_file
89
+ ssl_cert_file_hint
90
+ when :ssl_ca_cert_file
91
+ "client.faraday.ssl.ca_file = #{value.inspect}"
92
+ when :ssl_ciphers
93
+ "client.faraday.ssl.ciphers = #{value.inspect}"
94
+ when :ssl_ca_cert_path
95
+ "client.faraday.ssl.ca_path = #{value.inspect}"
96
+ when :adapter
97
+ adapter_hint
98
+ else
99
+ STATIC_HINTS.fetch(option)
100
+ end
101
+ end
102
+
103
+ def redacted_proxy_value
104
+ uri = URI.parse(value.to_s)
105
+ return value.inspect unless uri.absolute? && uri.host
106
+
107
+ redacted = uri.dup
108
+ redacted.userinfo = "..." if redacted.userinfo
109
+ redacted.path = "" if redacted.respond_to?(:path=)
110
+ redacted.query = nil if redacted.respond_to?(:query=)
111
+ redacted.fragment = nil if redacted.respond_to?(:fragment=)
112
+ redacted.to_s.inspect
113
+ rescue URI::InvalidURIError
114
+ '"[redacted proxy URL]"'
115
+ end
116
+
117
+ def ssl_version_hint
118
+ faraday_option = {
119
+ ssl_version: "version",
120
+ ssl_min_version: "min_version",
121
+ ssl_max_version: "max_version"
122
+ }.fetch(option)
123
+
124
+ "client.faraday.ssl.#{faraday_option} = #{value.inspect}"
125
+ end
126
+
127
+ def ssl_cert_key_file_hint
128
+ [
129
+ "key = OpenSSL::PKey.read(File.read(#{value.inspect}))",
130
+ "client.faraday.ssl.client_key = key"
131
+ ]
132
+ end
133
+
134
+ def ssl_cert_file_hint
135
+ [
136
+ "cert = OpenSSL::X509::Certificate.new(File.read(#{value.inspect}))",
137
+ "client.faraday.ssl.client_cert = cert"
138
+ ]
139
+ end
140
+
141
+ def adapter_hint
142
+ case value
143
+ when :net_http
144
+ "client.faraday.adapter :net_http"
145
+ when :httpclient
146
+ [
147
+ "gem 'faraday-httpclient'",
148
+ "require 'faraday/httpclient'",
149
+ "client.faraday.adapter :httpclient"
150
+ ]
151
+ when :excon
152
+ [
153
+ "gem 'faraday-excon'",
154
+ "require 'faraday/excon'",
155
+ "client.faraday.adapter :excon"
156
+ ]
157
+ when :net_http_persistent
158
+ [
159
+ "gem 'faraday-net_http_persistent'",
160
+ "require 'faraday/net_http_persistent'",
161
+ "client.faraday.adapter :net_http_persistent"
162
+ ]
163
+ else
164
+ [
165
+ "choose and install a Faraday adapter matching #{value.inspect}",
166
+ "client.faraday.adapter :net_http"
167
+ ]
168
+ end
169
+ end
170
+
171
+ def ssl_verify_mode_hint
172
+ case value
173
+ when :none
174
+ "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_NONE"
175
+ when :peer
176
+ "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_PEER"
177
+ when :fail_if_no_peer_cert
178
+ "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT"
179
+ when :client_once
180
+ "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_CLIENT_ONCE"
181
+ else
182
+ "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_PEER or OpenSSL::SSL::VERIFY_NONE"
183
+ end
184
+ end
185
+ end
186
+ end
@@ -6,20 +6,22 @@ module Savon
6
6
  http.error?
7
7
  end
8
8
 
9
- def initialize(http)
10
- @http = http
9
+ def initialize(transport_response)
10
+ @transport_response = transport_response
11
11
  end
12
12
 
13
- attr_reader :http
13
+ def http
14
+ @transport_response
15
+ end
14
16
 
15
17
  def to_s
16
- String.new("HTTP error (#{@http.code})").tap do |str_error|
17
- str_error << ": #{@http.body}" unless @http.body.empty?
18
+ String.new("HTTP error (#{http.code})").tap do |str_error|
19
+ str_error << ": #{http.body}" unless http.body.empty?
18
20
  end
19
21
  end
20
22
 
21
23
  def to_hash
22
- { code: @http.code, headers: @http.headers, body: @http.body }
24
+ { code: http.code, headers: http.headers, body: http.body }
23
25
  end
24
26
  end
25
27
  end
data/lib/savon/model.rb CHANGED
@@ -28,20 +28,25 @@ module Savon
28
28
 
29
29
  # Defines a class-level SOAP operation.
30
30
  def define_class_operation(operation)
31
- class_operation_module.module_eval %{
32
- def #{StringUtils.snakecase(operation.to_s)}(locals = {})
33
- client.call #{operation.inspect}, locals
34
- end
35
- }, __FILE__, __LINE__ - 4 # -4 points to the line where the eval string starts
31
+ method_name = operation_method_name(operation)
32
+
33
+ class_operation_module.define_method(method_name) do |locals = {}|
34
+ client.call operation, locals
35
+ end
36
36
  end
37
37
 
38
38
  # Defines an instance-level SOAP operation.
39
39
  def define_instance_operation(operation)
40
- instance_operation_module.module_eval %{
41
- def #{StringUtils.snakecase(operation.to_s)}(locals = {})
42
- self.class.#{StringUtils.snakecase(operation.to_s)} locals
43
- end
44
- }, __FILE__, __LINE__ - 4 # -4 points to the line where the eval string starts
40
+ method_name = operation_method_name(operation)
41
+
42
+ instance_operation_module.define_method(method_name) do |locals = {}|
43
+ self.class.public_send(method_name, locals)
44
+ end
45
+ end
46
+
47
+ # Returns the generated Ruby method name for a SOAP operation.
48
+ def operation_method_name(operation)
49
+ StringUtils.snakecase(operation.to_s).to_sym
45
50
  end
46
51
 
47
52
  # Class methods.
@@ -33,19 +33,20 @@ module Savon
33
33
  def self.create(operation_name, wsdl, globals, transport)
34
34
  if wsdl.document?
35
35
  ensure_name_is_symbol! operation_name
36
- ensure_exists! operation_name, wsdl
36
+ ensure_exists! operation_name, wsdl, transport
37
37
  end
38
38
 
39
39
  new(operation_name, wsdl, globals, transport)
40
40
  end
41
41
 
42
- def self.ensure_exists!(operation_name, wsdl)
42
+ # Verifies the operation is provided by the WSDL.
43
+ def self.ensure_exists!(operation_name, wsdl, transport)
43
44
  unless wsdl.soap_actions.include? operation_name
44
45
  raise UnknownOperationError, "Unable to find SOAP operation: #{operation_name.inspect}\n" \
45
46
  "Operations provided by your service: #{wsdl.soap_actions.inspect}"
46
47
  end
47
48
  rescue Wasabi::Resolver::HTTPError => e
48
- raise HTTPError, e.response
49
+ raise HTTPError, transport.normalize_response(e.response)
49
50
  end
50
51
 
51
52
  def self.ensure_name_is_symbol!(operation_name)
@@ -73,19 +74,19 @@ module Savon
73
74
  # Transport::Response (or legacy HTTPI::Response), the HTTP call
74
75
  # is skipped and that response is used directly.
75
76
  def call(locals = {}, &block)
76
- builder = build(locals, &block)
77
- response = Savon.notify_observers(@name, builder, @globals, @locals)
77
+ builder = build(locals, &block)
78
+ observer_response = Savon.notify_observers(@name, builder, @globals, @locals)
78
79
 
79
- response =
80
- if response.nil?
80
+ transport_response =
81
+ if observer_response.nil?
81
82
  body = builder.to_s
82
83
  headers = soap_headers(builder)
83
84
  @transport.post(endpoint.to_s, headers, body, @locals)
84
85
  else
85
- normalize_observer_response(response)
86
+ normalize_observer_response(observer_response)
86
87
  end
87
88
 
88
- create_response(response)
89
+ Response.new(transport_response, @globals, @locals)
89
90
  end
90
91
 
91
92
  # Builds and returns the HTTPI::Request that would be sent for this
@@ -106,10 +107,6 @@ module Savon
106
107
 
107
108
  private
108
109
 
109
- def create_response(response)
110
- Response.new(response, @globals, @locals)
111
- end
112
-
113
110
  def set_locals(locals, block)
114
111
  locals = LocalOptions.new(locals)
115
112
  BlockInterface.new(locals).evaluate(block) if block
@@ -173,12 +170,12 @@ module Savon
173
170
  # Accepts Transport::Response directly (current contract), wraps
174
171
  # HTTPI::Response with a deprecation warning (legacy observer support),
175
172
  # and raises on anything else.
176
- def normalize_observer_response(response)
177
- return response if response.is_a?(Transport::Response)
173
+ def normalize_observer_response(observer_response)
174
+ return observer_response if observer_response.is_a?(Transport::Response)
178
175
 
179
- if response.is_a?(HTTPI::Response)
180
- warn "Observers returning HTTPI::Response is deprecated - return Savon::Transport::Response instead."
181
- return Transport::Response.from_httpi(response)
176
+ if observer_response.is_a?(HTTPI::Response)
177
+ warn "Observers returning HTTPI::Response is deprecated - return Savon::Transport::Response instead.", uplevel: 1
178
+ return Transport::Response.from_httpi(observer_response)
182
179
  end
183
180
 
184
181
  raise Error, "Observers need to return a Savon::Transport::Response " \
data/lib/savon/options.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "logger"
4
4
  require "httpi"
5
+ require "savon/faraday_migration_hint"
5
6
 
6
7
  module Savon
7
8
  # Base class for GlobalOptions and LocalOptions.
@@ -73,46 +74,24 @@ module Savon
73
74
  end
74
75
  end
75
76
 
76
- # HTTPI-specific transport options included in GlobalOptions.
77
+ # Options that belong to HTTPI's transport layer.
77
78
  #
78
- # Every option in this module is handled by HTTPI and has no effect when
79
- # transport: :faraday is set. Faraday callers configure these concerns
80
- # directly on the Faraday::Connection returned by client.faraday.
79
+ # They still work with the default HTTPI transport. With `transport: :faraday`,
80
+ # Savon does not translate them because Faraday exposes its own setup API for
81
+ # proxy, timeout, TLS, auth, redirects, and adapter choices.
81
82
  module HTTPITransportOptions
82
- # Maps each httpi-only option to the Faraday equivalent so the
83
- # error raised at init tells the caller exactly what to do instead.
84
- # Options with a :default entry are only flagged when the caller
85
- # sets a value that differs from the GlobalOptions default.
86
- FARADAY_INCOMPATIBLE_GLOBALS = {
87
- proxy: { hint: "client.faraday.proxy = url" },
88
- open_timeout: { hint: "client.faraday.options.timeout = N" },
89
- read_timeout: { hint: "client.faraday.options.timeout = N" },
90
- write_timeout: { hint: "client.faraday.options.write_timeout = N" },
91
- ssl_version: { hint: "client.faraday.ssl.version = version" },
92
- ssl_min_version: { hint: "client.faraday.ssl.min_version = version" },
93
- ssl_max_version: { hint: "client.faraday.ssl.max_version = version" },
94
- ssl_verify_mode: { hint: "client.faraday.ssl.verify = true/false" },
95
- ssl_cert_key_file: { hint: "client.faraday.ssl.client_key_file = path" },
96
- ssl_cert_key: { hint: "client.faraday.ssl.client_key = key" },
97
- ssl_cert_key_password: { hint: "configure ssl context on client.faraday.ssl" },
98
- ssl_cert_file: { hint: "client.faraday.ssl.client_cert_file = path" },
99
- ssl_cert: { hint: "client.faraday.ssl.client_cert = cert" },
100
- ssl_ca_cert_file: { hint: "client.faraday.ssl.ca_file = path" },
101
- ssl_ca_cert: { hint: "client.faraday.ssl.ca_cert = cert" },
102
- ssl_ciphers: { hint: "client.faraday.ssl.ciphers = ciphers" },
103
- ssl_ca_cert_path: { hint: "client.faraday.ssl.ca_path = path" },
104
- ssl_cert_store: { hint: "client.faraday.ssl.cert_store = store" },
105
- basic_auth: { hint: "client.faraday.request :basic_auth, user, pass" },
106
- digest_auth: { hint: "client.faraday.request :authorization, :Digest, credentials" },
107
- ntlm: { hint: "client.faraday.request :ntlm, user, pass" },
108
- follow_redirects: { hint: "client.faraday.use :follow_redirects", default: false },
109
- adapter: { hint: "client.faraday.adapter :net_http", default: nil }
83
+ TRANSPORT_DEFAULTS = {
84
+ adapter: nil,
85
+ follow_redirects: false
110
86
  }.freeze
111
87
 
112
- # Validates that the chosen transport is compatible with the options set.
113
- # Must be called after all options (including any block-form options) are set.
114
- # Collects every conflict and raises a single InitializationError listing all
115
- # problems and solutions at once.
88
+ # These are rejected for `transport: :faraday` once the caller has set a
89
+ # non-default value. The error points to the matching Faraday setup.
90
+ FARADAY_INCOMPATIBLE_GLOBALS = FaradayMigrationHint::OPTIONS
91
+
92
+ # Runs after all global options have been assigned, including options set in
93
+ # the client block. Reports every HTTPI-only option in one error so callers
94
+ # can move their transport setup to Faraday in one pass.
116
95
  def validate_transport!
117
96
  return unless self[:transport] == :faraday
118
97
 
@@ -122,11 +101,11 @@ module Savon
122
101
  "Add to your Gemfile: gem 'faraday'"
123
102
  end
124
103
 
125
- violations = FARADAY_INCOMPATIBLE_GLOBALS.filter_map { |option, config|
104
+ violations = FARADAY_INCOMPATIBLE_GLOBALS.filter_map { |option|
126
105
  next unless include?(option)
127
- next if config.key?(:default) && self[option] == config[:default]
106
+ next if default_transport_option?(option)
128
107
 
129
- " #{option} - Use: #{config[:hint]}"
108
+ FaradayMigrationHint.new(option, self[option]).message
130
109
  }
131
110
 
132
111
  return if violations.empty?
@@ -252,6 +231,10 @@ module Savon
252
231
 
253
232
  private
254
233
 
234
+ def default_transport_option?(option)
235
+ TRANSPORT_DEFAULTS.key?(option) && self[option] == TRANSPORT_DEFAULTS[option]
236
+ end
237
+
255
238
  # Attempts to load faraday. Returns true if available, false on LoadError.
256
239
  def faraday_loaded?
257
240
  require "faraday"
@@ -271,33 +254,6 @@ module Savon
271
254
 
272
255
  def initialize(options = {})
273
256
  @option_type = :global
274
-
275
- defaults = {
276
- encoding: "UTF-8",
277
- soap_version: 1,
278
- namespaces: {},
279
- logger: Logger.new($stdout),
280
- log: false,
281
- log_headers: true,
282
- filters: [],
283
- pretty_print_xml: false,
284
- raise_errors: true,
285
- strip_namespaces: true,
286
- delete_namespace_attributes: false,
287
- convert_response_tags_to: ->(tag) { StringUtils.snakecase(tag).to_sym },
288
- convert_attributes_to: ->(k, v) { [k, v] },
289
- multipart: false,
290
- use_wsa_headers: false,
291
- no_message_tag: false,
292
- unwrap: false,
293
- host: nil,
294
- transport: :httpi,
295
-
296
- # httpi transport defaults
297
- adapter: nil,
298
- follow_redirects: false
299
- }
300
-
301
257
  options = defaults.merge(options)
302
258
 
303
259
  # this option is a shortcut on the logger which needs to be set
@@ -427,6 +383,26 @@ module Savon
427
383
  @options[:delete_namespace_attributes] = delete_namespace_attributes
428
384
  end
429
385
 
386
+ # The value Nori assigns to empty XML tags in the SOAP response.
387
+ # Defaults to nil, matching Nori's default; set to "" to map empty tags
388
+ # to an empty String instead.
389
+ def empty_tag_value(value)
390
+ @options[:empty_tag_value] = value
391
+ end
392
+
393
+ # Instruct Nori whether to convert dashes in response tag names to
394
+ # underscores before they become Hash keys. Defaults to true.
395
+ def convert_dashes_to_underscores(convert)
396
+ @options[:convert_dashes_to_underscores] = convert
397
+ end
398
+
399
+ # Instruct Nori whether to scrub invalid byte sequences from the response
400
+ # body before parsing it. Defaults to true, which lets responses containing
401
+ # invalid characters still be parsed.
402
+ def scrub_xml(scrub)
403
+ @options[:scrub_xml] = scrub
404
+ end
405
+
430
406
  # Tell Gyoku how to convert Hash key Symbols to XML tags.
431
407
  # Accepts one of :lower_camelcase, :camelcase, :upcase, or :none.
432
408
  def convert_request_keys_to(converter)
@@ -455,6 +431,12 @@ module Savon
455
431
 
456
432
  # Instruct Savon to create a multipart response if available.
457
433
  def multipart(multipart)
434
+ if multipart
435
+ warn "The global :multipart option has been a no-op since v2.13.0. " \
436
+ "Savon detects whether the response is multipart by checking if the " \
437
+ "response Content-Type header contains 'multipart'. You can remove" \
438
+ "this option from your code to make this warning disappear.", uplevel: 1
439
+ end
458
440
  @options[:multipart] = multipart
459
441
  end
460
442
 
@@ -469,12 +451,41 @@ module Savon
469
451
  end
470
452
 
471
453
  # HTTP transport to use. Accepts :httpi (default) or :faraday.
472
- # When set to :faraday, configure transport concerns directly on the
473
- # Faraday::Connection returned by client.faraday instead of using
474
- # the HTTPITransportOptions.
454
+ # With :faraday, set proxy, timeout, TLS, auth, redirect, and adapter
455
+ # details on `client.faraday`; the HTTPI transport options are not copied.
475
456
  def transport(transport)
476
457
  @options[:transport] = transport
477
458
  end
459
+
460
+ private
461
+
462
+ # The default value for every global option.
463
+ def defaults
464
+ HTTPITransportOptions::TRANSPORT_DEFAULTS.merge(
465
+ encoding: "UTF-8",
466
+ soap_version: 1,
467
+ namespaces: {},
468
+ logger: Logger.new($stdout),
469
+ log: false,
470
+ log_headers: true,
471
+ filters: [],
472
+ pretty_print_xml: false,
473
+ raise_errors: true,
474
+ strip_namespaces: true,
475
+ delete_namespace_attributes: false,
476
+ empty_tag_value: nil,
477
+ convert_dashes_to_underscores: true,
478
+ scrub_xml: true,
479
+ convert_response_tags_to: ->(tag) { StringUtils.snakecase(tag).to_sym },
480
+ convert_attributes_to: ->(k, v) { [k, v] },
481
+ multipart: false,
482
+ use_wsa_headers: false,
483
+ no_message_tag: false,
484
+ unwrap: false,
485
+ host: nil,
486
+ transport: :httpi
487
+ )
488
+ end
478
489
  end
479
490
 
480
491
  # Per-request options passed to client.call.
@@ -578,6 +589,12 @@ module Savon
578
589
 
579
590
  # Instruct Savon to create a multipart response if available.
580
591
  def multipart(multipart)
592
+ if multipart
593
+ warn "The local :multipart option has been a no-op since v2.13.0. " \
594
+ "Savon detects whether the response is multipart by checking if the " \
595
+ "response Content-Type header contains 'multipart'. You can remove" \
596
+ "this option from your code to make this warning disappear.", uplevel: 1
597
+ end
581
598
  @options[:multipart] = multipart
582
599
  end
583
600
 
@@ -9,19 +9,23 @@ module Savon
9
9
  CRLF = /\r\n/
10
10
  WSP = /[\t ]/
11
11
 
12
- def initialize(http, globals, locals)
13
- @http = http
14
- @globals = globals
15
- @locals = locals
16
- @attachments = []
17
- @xml = ''
18
- @has_parsed_body = false
12
+ def initialize(transport_response, globals, locals)
13
+ @transport_response = transport_response
14
+ @globals = globals
15
+ @locals = locals
16
+ @attachments = []
17
+ @xml = ''
18
+ @has_parsed_body = false
19
19
 
20
20
  build_soap_and_http_errors!
21
21
  raise_soap_and_http_errors! if @globals[:raise_errors]
22
22
  end
23
23
 
24
- attr_reader :http, :globals, :locals, :soap_fault, :http_error
24
+ attr_reader :globals, :locals, :soap_fault, :http_error
25
+
26
+ def http
27
+ @transport_response
28
+ end
25
29
 
26
30
  def success?
27
31
  !soap_fault? && !http_error?
@@ -29,11 +33,11 @@ module Savon
29
33
  alias successful? success?
30
34
 
31
35
  def soap_fault?
32
- SOAPFault.present?(@http, xml)
36
+ SOAPFault.present?(http, xml)
33
37
  end
34
38
 
35
39
  def http_error?
36
- HTTPError.present? @http
40
+ HTTPError.present? http
37
41
  end
38
42
 
39
43
  def header
@@ -70,7 +74,7 @@ module Savon
70
74
  parse_body unless @has_parsed_body
71
75
  @xml
72
76
  else
73
- @http.body
77
+ http.body
74
78
  end
75
79
  end
76
80
 
@@ -132,8 +136,8 @@ module Savon
132
136
  end
133
137
 
134
138
  def build_soap_and_http_errors!
135
- @soap_fault = SOAPFault.new(@http, nori, xml) if soap_fault?
136
- @http_error = HTTPError.new(@http) if http_error?
139
+ @soap_fault = SOAPFault.new(http, nori, xml) if soap_fault?
140
+ @http_error = HTTPError.new(http) if http_error?
137
141
  end
138
142
 
139
143
  def raise_soap_and_http_errors!
@@ -155,6 +159,9 @@ module Savon
155
159
  nori_options = {
156
160
  delete_namespace_attributes: @globals[:delete_namespace_attributes],
157
161
  strip_namespaces: @globals[:strip_namespaces],
162
+ empty_tag_value: @globals[:empty_tag_value],
163
+ convert_dashes_to_underscores: @globals[:convert_dashes_to_underscores],
164
+ scrub_xml: @globals[:scrub_xml],
158
165
  convert_tags_to: @globals[:convert_response_tags_to],
159
166
  convert_attributes_to: @globals[:convert_attributes_to],
160
167
  advanced_typecasting: @locals[:advanced_typecasting],
@@ -38,13 +38,34 @@ module Savon
38
38
  log_request(url, headers, body) if log?
39
39
 
40
40
  faraday_response = @connection.post(url, body, headers)
41
- response = Response.from_faraday(faraday_response)
41
+ response = normalize_response(faraday_response)
42
42
 
43
43
  log_response(response) if log?
44
-
45
44
  response
46
45
  end
47
46
 
47
+ # Normalizes a native Faraday::Response into a Transport::Response.
48
+ #
49
+ # @param faraday_response [Faraday::Response]
50
+ # @return [Transport::Response]
51
+ def normalize_response(faraday_response)
52
+ Response.from_faraday(faraday_response)
53
+ end
54
+
55
+ # Parses Set-Cookie headers into a Hash of name => value. Accepts both
56
+ # the Array and String form. Attributes after the first ';' are discarded.
57
+ def self.parse_cookies(headers)
58
+ raw = headers["set-cookie"] || headers["Set-Cookie"]
59
+ return {} unless raw
60
+
61
+ raw_array = raw.is_a?(Array) ? raw : raw.split(/,\s*/)
62
+ raw_array.each_with_object({}) do |cookie_str, hash|
63
+ name_value = cookie_str.split(";", 2).first.to_s.strip
64
+ name, value = name_value.split("=", 2)
65
+ hash[name] = value if name && !name.empty?
66
+ end
67
+ end
68
+
48
69
  private
49
70
 
50
71
  # Merges all header sources in precedence order:
@@ -58,12 +79,23 @@ module Savon
58
79
  # soap_headers are lowest priority
59
80
  soap_headers.each do |k, v| headers[k] ||= v end
60
81
 
61
- if locals[:cookies]&.any?
62
- headers["Cookie"] = locals[:cookies].map(&:name_and_value).join(";")
63
- end
82
+ cookie_header = format_cookies(locals[:cookies])
83
+ headers["Cookie"] = cookie_header if cookie_header
64
84
 
65
85
  headers
66
86
  end
87
+
88
+ # Builds the Cookie header from a given value.
89
+ # Accepts:
90
+ # * String - passed through verbatim
91
+ # * Hash - formatted as "name=value; name=value" (browser style)
92
+ # Returns nil when no cookies were supplied.
93
+ def format_cookies(cookies)
94
+ return nil if cookies.nil?
95
+ return cookies if cookies.is_a?(String)
96
+
97
+ cookies.map { |name, value| "#{name}=#{value}" }.join("; ")
98
+ end
67
99
  end
68
100
  end
69
101
  end
@@ -33,14 +33,21 @@ module Savon
33
33
 
34
34
  log_request(http_request.url, http_request.headers, http_request.body) if log?
35
35
 
36
- http_response = ::HTTPI.post(http_request, @globals[:adapter])
37
- response = Response.from_httpi(http_response)
36
+ httpi_response = ::HTTPI.post(http_request, @globals[:adapter])
37
+ response = normalize_response(httpi_response)
38
38
 
39
39
  log_response(response) if log?
40
-
41
40
  response
42
41
  end
43
42
 
43
+ # Normalizes a native HTTPI::Response into a Transport::Response.
44
+ #
45
+ # @param httpi_response [HTTPI::Response]
46
+ # @return [Transport::Response]
47
+ def normalize_response(httpi_response)
48
+ Response.from_httpi(httpi_response)
49
+ end
50
+
44
51
  # Builds a fully-configured HTTPI::Request.
45
52
  #
46
53
  # @param url [String] the SOAP endpoint URL
@@ -56,14 +63,11 @@ module Savon
56
63
  # soap_headers are lowest priority
57
64
  soap_headers.each do |k, v| headers[k] ||= v end
58
65
 
59
- if locals[:cookies]&.any?
60
- headers["Cookie"] = locals[:cookies].map(&:name_and_value).join(";")
61
- end
62
-
63
66
  http_request = ::HTTPI::Request.new
64
67
  http_request.url = url
65
68
  http_request.body = body
66
69
  http_request.headers = headers
70
+ http_request.set_cookies(locals[:cookies]) if locals[:cookies]
67
71
  configure_http_request(http_request)
68
72
  http_request
69
73
  end
@@ -1,29 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "httpi"
4
+ require "savon/transport/faraday"
5
+
3
6
  module Savon
4
7
  module Transport
5
8
  # Transport-agnostic HTTP response value object.
6
9
  #
7
10
  # Every transport produces a Transport::Response so that higher-level code
8
11
  # never depends on transport-specific code. Immutable once constructed.
12
+ #
13
+ # The shape of #cookies is transport-specific: HTTPI responses expose an
14
+ # Array of HTTPI::Cookie, while Faraday responses expose a plain Hash so
15
+ # Faraday users do not depend on HTTPI types.
9
16
  class Response
10
- # Creates a Transport::Response from an HTTPI::Response.
17
+ # Builds a Response from an HTTPI::Response.
18
+ #
19
+ # @param httpi_response [HTTPI::Response]
20
+ # @return [Transport::Response]
11
21
  def self.from_httpi(httpi_response)
12
- new(httpi_response.code, httpi_response.headers, httpi_response.body)
22
+ new(
23
+ httpi_response.code,
24
+ httpi_response.headers,
25
+ httpi_response.body,
26
+ cookies: ::HTTPI::Cookie.list_from_headers(httpi_response.headers)
27
+ )
13
28
  end
14
29
 
15
- # Creates a Transport::Response from a Faraday::Response.
30
+ # Builds a Response from a Faraday::Response.
31
+ #
32
+ # @param faraday_response [Faraday::Response]
33
+ # @return [Transport::Response]
16
34
  def self.from_faraday(faraday_response)
17
- new(faraday_response.status, faraday_response.headers.to_h, faraday_response.body)
35
+ new(
36
+ faraday_response.status,
37
+ faraday_response.headers.to_h,
38
+ faraday_response.body,
39
+ cookies: Savon::Transport::Faraday.parse_cookies(faraday_response.headers)
40
+ )
18
41
  end
19
42
 
20
43
  # @param code [Integer] HTTP status code
21
44
  # @param headers [Hash] response headers
22
45
  # @param body [String] response body
23
- def initialize(code, headers, body)
46
+ # @param cookies [Object] parsed cookies in a transport-specific shape
47
+ def initialize(code, headers, body, cookies: nil)
24
48
  @code = code
25
49
  @headers = headers
26
50
  @body = body
51
+ @cookies = cookies
27
52
  end
28
53
 
29
54
  # Returns the HTTP status code.
@@ -35,6 +60,10 @@ module Savon
35
60
  # Returns the response body string.
36
61
  attr_reader :body
37
62
 
63
+ # Returns the parsed cookies in a transport-specific shape.
64
+ # See class-level docs.
65
+ attr_reader :cookies
66
+
38
67
  # Returns true when the HTTP status code indicates an error (>= 300).
39
68
  def error?
40
69
  @code >= 300
data/lib/savon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Savon
4
- VERSION = '2.17.1'
4
+ VERSION = '2.17.3'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: savon
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.17.1
4
+ version: 2.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Harrington
@@ -105,14 +105,14 @@ dependencies:
105
105
  requirements:
106
106
  - - "~>"
107
107
  - !ruby/object:Gem::Version
108
- version: '2.4'
108
+ version: '2.7'
109
109
  type: :runtime
110
110
  prerelease: false
111
111
  version_requirements: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - "~>"
114
114
  - !ruby/object:Gem::Version
115
- version: '2.4'
115
+ version: '2.7'
116
116
  - !ruby/object:Gem::Dependency
117
117
  name: wasabi
118
118
  requirement: !ruby/object:Gem::Requirement
@@ -151,22 +151,16 @@ dependencies:
151
151
  name: puma
152
152
  requirement: !ruby/object:Gem::Requirement
153
153
  requirements:
154
- - - ">="
155
- - !ruby/object:Gem::Version
156
- version: 4.3.8
157
- - - "<"
154
+ - - "~>"
158
155
  - !ruby/object:Gem::Version
159
- version: '7'
156
+ version: 8.0.2
160
157
  type: :development
161
158
  prerelease: false
162
159
  version_requirements: !ruby/object:Gem::Requirement
163
160
  requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: 4.3.8
167
- - - "<"
161
+ - - "~>"
168
162
  - !ruby/object:Gem::Version
169
- version: '7'
163
+ version: 8.0.2
170
164
  - !ruby/object:Gem::Dependency
171
165
  name: rack
172
166
  requirement: !ruby/object:Gem::Requirement
@@ -265,6 +259,7 @@ files:
265
259
  - lib/savon/block_interface.rb
266
260
  - lib/savon/builder.rb
267
261
  - lib/savon/client.rb
262
+ - lib/savon/faraday_migration_hint.rb
268
263
  - lib/savon/header.rb
269
264
  - lib/savon/http_error.rb
270
265
  - lib/savon/log_message.rb
@@ -290,7 +285,7 @@ licenses:
290
285
  metadata:
291
286
  rubygems_mfa_required: 'true'
292
287
  source_code_uri: https://github.com/savonrb/savon
293
- changelog_uri: https://github.com/savonrb/savon/blob/v2.x/CHANGELOG.md
288
+ changelog_uri: https://github.com/savonrb/savon/blob/main/CHANGELOG.md
294
289
  bug_tracker_uri: https://github.com/savonrb/savon/issues
295
290
  rdoc_options: []
296
291
  require_paths: