files.com 1.1.803 → 1.1.805

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: 3fde439a751fae76d99298db114c359f6dd675ad51b17d173ebdb8d4be011284
4
- data.tar.gz: e716632be7616edf7ce3cf7c51ad61325e87d1b093e021e22d45fcb520cb3e8d
3
+ metadata.gz: ec7bf62c47e0eaca608b506497fd71e4466fcf5759e180b5c3f63b08ad9b4d07
4
+ data.tar.gz: c0f50f7d3a72d3ae50a57cdccf114da044711f16ab1c2abffe6217516c56030e
5
5
  SHA512:
6
- metadata.gz: faf8e3363b6ef1e4fb47e0bcdefe86ad0462f15a8bcf4bcdb739f45b0e63fd28f6c1ba4c991ba52cceb4ba73e8d7b35ff35a2fae152377bc9bbd5d400dd66fda
7
- data.tar.gz: 33dc5267810ce31d2da8dac9142662d279a5f9913baaeac0b1758597ca59fc36c6545178a9914b15f8b5d6bdce1701bebe1a405334bb82409339eb8167df312c
6
+ metadata.gz: 4ccda75a1ac5b95145654244f5619ab001a187c63a5a07f1ce54a3becb1226b1963499ccf295d7508996fd17cef4d1131cf7acd220f974154957604f4279a3de
7
+ data.tar.gz: 6d7681f622b12d307abcc66bca066b2038e3f95c93cbe4823af517b9d6f1f908d245d8df1ce4e70101416e9516db27a526864917ab35ad0b1ae8d2ac6ebae943
data/Gemfile.lock CHANGED
@@ -22,7 +22,8 @@ GEM
22
22
  base64 (0.2.0)
23
23
  benchmark (0.4.1)
24
24
  bigdecimal (4.1.3)
25
- byebug (11.1.3)
25
+ byebug (13.0.0)
26
+ reline (>= 0.6.0)
26
27
  coderay (1.1.3)
27
28
  concurrent-ruby (1.3.8)
28
29
  connection_pool (2.2.5)
@@ -31,12 +32,13 @@ GEM
31
32
  faraday-net_http (>= 2.0, < 3.5)
32
33
  json
33
34
  logger
34
- faraday-multipart (1.0.4)
35
- multipart-post (~> 2)
35
+ faraday-multipart (1.2.0)
36
+ multipart-post (~> 2.0)
36
37
  faraday-net_http (3.0.0)
37
38
  faraday-net_http_persistent (2.1.0)
38
39
  faraday (~> 2.5)
39
40
  net-http-persistent (~> 4.0)
41
+ io-console (0.9.5)
40
42
  json (2.6.2)
41
43
  logger (1.6.6)
42
44
  memory_profiler (1.0.0)
@@ -51,14 +53,16 @@ GEM
51
53
  pry (0.14.1)
52
54
  coderay (~> 1.1)
53
55
  method_source (~> 1.0)
54
- pry-byebug (3.10.1)
55
- byebug (~> 11.0)
56
- pry (>= 0.13, < 0.15)
56
+ pry-byebug (3.12.0)
57
+ byebug (~> 13.0)
58
+ pry (>= 0.13, < 0.17)
57
59
  public_suffix (5.0.0)
58
60
  racc (1.8.1)
59
61
  rainbow (3.1.1)
60
62
  rake (13.0.6)
61
63
  regexp_parser (2.5.0)
64
+ reline (0.7.0)
65
+ io-console (~> 0.5)
62
66
  rexml (3.4.4)
63
67
  rspec (3.11.0)
64
68
  rspec-core (~> 3.11.0)
data/_VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.803
1
+ 1.1.805
@@ -341,8 +341,8 @@ module Files
341
341
  end
342
342
 
343
343
  # The origin a lockout region mismatch moves this request to, or nil to raise the error as it is. The API answers
344
- # with that error, naming the site's own origin in data.host, when the site is pinned to another region or to a
345
- # custom domain. Only the configured HTTPS origin, reached over this client's verified connection, may send the
344
+ # with that error, naming the site's own origin in data.host or X-Files-Host, when the site is pinned to another
345
+ # region or to a custom domain. Only the configured HTTPS origin, reached over this client's verified connection, may send the
346
346
  # credentials elsewhere, apart from an http:// origin to the destination +http_pairs+ pairs it with.
347
347
  private def region_continuation_base_url(error_response, base_url, body, http_pairs)
348
348
  return if Files.region_policy == :disabled
@@ -351,7 +351,11 @@ module Files
351
351
  return unless error_response.http_status == 403 && error_object[:type] == "not-authenticated/lockout-region-mismatch"
352
352
  return unless replayable?(body)
353
353
 
354
- host = error_object[:data].is_a?(Hash) ? error_object[:data][:host] : nil
354
+ data = error_object[:data]
355
+ return unless data.nil? || data.is_a?(Hash)
356
+
357
+ host = data && data[:host]
358
+ host = Faraday::Utils::Headers.new(error_response.http_headers || {})["X-Files-Host"] if host.nil? || host == ""
355
359
  return http_pair_origin(base_url, host, http_pairs) unless verified_https_connection?(base_url)
356
360
 
357
361
  configured = Addressable::URI.parse(base_url)
@@ -507,10 +511,11 @@ module Files
507
511
 
508
512
  case e
509
513
  when Faraday::ClientError, Faraday::ServerError
510
- error_response = api_error_response(e.response)
511
- if relocate && error_response && (regional_base_url = relocate.call(error_response))
514
+ error_response = api_error_response(e.response, allow_legacy_region_mismatch: !is_transfer)
515
+ if relocate && num_retries < Files.max_network_retries && error_response && (regional_base_url = relocate.call(error_response))
512
516
  relocate = nil
513
517
  base_url = regional_base_url
518
+ num_retries += 1
514
519
  retry
515
520
  end
516
521
 
@@ -547,16 +552,29 @@ module Files
547
552
  end
548
553
 
549
554
  # A failed response carrying a Files.com error object: a JSON object with an
550
- # error message or type. Any other body, JSON or not, is not an answer the
551
- # SDK can use.
552
- private def api_error_response(http_resp)
555
+ # error message or type, or an ordinary API request's recognized legacy
556
+ # plain-text region mismatch. Other bodies give the SDK no usable answer.
557
+ private def api_error_response(http_resp, allow_legacy_region_mismatch: false)
553
558
  return unless http_resp
554
559
 
555
560
  resp = Response.from_faraday_hash(http_resp)
556
561
  error_object = resp.data
557
562
  resp if error_object.is_a?(Hash) && (error_object[:error] || error_object[:errors] || error_object[:type])
558
563
  rescue JSON::ParserError
559
- nil
564
+ return unless allow_legacy_region_mismatch && http_resp[:status] == 403
565
+
566
+ body = http_resp[:body]
567
+ host = Faraday::Utils::Headers.new(http_resp[:headers] || {})["X-Files-Host"]
568
+ return unless body.is_a?(String) && body.start_with?("You have connected to a URL") && host.is_a?(String) && !host.empty?
569
+
570
+ # The legacy API answer predates JSON errors. Give callers the same typed error and routing data while keeping
571
+ # the original text and headers on the response; a storage response never acquires API routing authority.
572
+ resp = Response.new
573
+ resp.http_status = http_resp[:status]
574
+ resp.http_headers = http_resp[:headers]
575
+ resp.http_body = body
576
+ resp.data = { type: "not-authenticated/lockout-region-mismatch", error: body, data: { host: host } }
577
+ resp
560
578
  end
561
579
 
562
580
  private def handle_error_response(resp, context, is_transfer: false)
data/lib/files.com.rb CHANGED
@@ -285,7 +285,10 @@ module Files
285
285
  end
286
286
 
287
287
  # What a request does when the API answers that the site must be reached through another origin, such as its
288
- # region or its custom domain (Files::LockoutRegionMismatchError, with that origin in data[:host]).
288
+ # region or its custom domain: the typed HTTP 403 Files::LockoutRegionMismatchError, with that origin in data[:host]
289
+ # or X-Files-Host when the data host is absent or empty, or the legacy plain-text HTTP 403 beginning "You have connected to a URL"
290
+ # with X-Files-Host. A recognized legacy answer raises the same typed error when the request cannot continue, keeping
291
+ # its original text and headers in http_body and http_headers. An unrelated 403 never moves the request.
289
292
  #
290
293
  # :automatic (the default) sends the same request once more to that origin, within the same retry count, and keeps
291
294
  # using it for the records and pages the request returns. Other requests still start at Files.base_url. It does so
@@ -305,8 +308,8 @@ module Files
305
308
  # Each pair is [source, destination], such as [["http://localhost:3000", "http://site.localhost:3000"]].
306
309
  #
307
310
  # With region_policy :automatic, a request to the source (Files.base_url, or the request's own base_url) that gets
308
- # the error with data[:host] naming the destination, as that full http:// origin or as its bare host (with its port
309
- # when it is not 80), is sent once more to the destination, within the same retry count, and the records and pages
311
+ # the mismatch naming the destination in data[:host] or X-Files-Host, as that full http:// origin or as its bare host
312
+ # (with its port when it is not 80), is sent once more to the destination, within the same retry count, and the records and pages
310
313
  # it returns keep using it. region_handler, when set, is still asked first and can decline. Nothing else continues
311
314
  # over plain HTTP, a request to an https:// origin never does, and :disabled still raises the error.
312
315
  #
@@ -0,0 +1,254 @@
1
+ require "spec_helper"
2
+ require_relative "../support/stubbed_api"
3
+
4
+ RSpec.describe "API region routing" do
5
+ include StubbedApi
6
+
7
+ let(:source) { "http://issuer.example.com" }
8
+ let(:target) { "http://target.example.com" }
9
+ let(:path) { "/api/rest/v1/bundles/842" }
10
+ let(:mismatch_type) { "not-authenticated/lockout-region-mismatch" }
11
+ let(:legacy_body) { "You have connected to a URL that does not match this site's region." }
12
+
13
+ around do |example|
14
+ original = [ Files.base_url, Files.region_policy, Files.region_handler, Files.region_http_pairs,
15
+ Files.max_network_retries, Files.initial_network_retry_delay ]
16
+ Files.base_url = source
17
+ Files.region_policy = :automatic
18
+ Files.region_handler = nil
19
+ Files.region_http_pairs = [ [ source, target ] ]
20
+ Files.max_network_retries = 3
21
+ Files.initial_network_retry_delay = 0
22
+ example.run
23
+ ensure
24
+ Files.base_url, Files.region_policy, Files.region_handler, Files.region_http_pairs,
25
+ Files.max_network_retries, Files.initial_network_retry_delay = original
26
+ end
27
+
28
+ shared_examples "a header-named mismatch" do
29
+ it "replays the API request unchanged at the named origin and retains only its operation's routing context" do
30
+ requests = []
31
+ client = stubbed_api_client do |stub|
32
+ [ source, target ].each do |origin|
33
+ stub.post("#{origin}#{path}") do |env|
34
+ requests << [ env.url.to_s, env.method, env.body, env.request_headers.to_h ]
35
+ origin == source ? mismatch : json_response(200, { id: 842, code: "regional" })
36
+ end
37
+ end
38
+ end
39
+ params = { code: "regional", permissions: "read" }
40
+ options = { client: client, session_id: "request-session", workspace_id: 0, "X-Request-Context" => "caller" }
41
+
42
+ response, returned_options = Files::Api.send_request("/bundles/842", :post, params, options)
43
+
44
+ expect(response.data).to include(id: 842, code: "regional")
45
+ expect(requests.map(&:first)).to eq([ "#{source}#{path}", "#{target}#{path}" ])
46
+ expect(requests.last.drop(1)).to eq(requests.first.drop(1))
47
+ expect(requests.last[2]).to eq("code=regional&permissions=read")
48
+ expect(requests.last[3]).to include("X-FilesAPI-Auth" => "request-session", "X-Files-Workspace-Id" => "0", "X-Request-Context" => "caller")
49
+ expect(requests.last[3]).not_to have_key("X-FilesAPI-Key")
50
+ expect(returned_options).to include(base_url: target, session_id: "request-session", workspace_id: 0, region_http_pairs: [ [ source, target ] ])
51
+ expect(options).not_to have_key(:base_url)
52
+ expect(params).to eq(code: "regional", permissions: "read")
53
+ expect(Files.base_url).to eq(source)
54
+ end
55
+ end
56
+
57
+ context "for a legacy plain-text 403 without Content-Type" do
58
+ let(:mismatch) { [ 403, { "X-Files-Host" => "target.example.com" }, legacy_body ] }
59
+ include_examples "a header-named mismatch"
60
+ end
61
+
62
+ context "for a legacy plain-text 403 with Content-Type" do
63
+ let(:mismatch) { [ 403, { "Content-Type" => "text/plain", "x-files-host" => target }, legacy_body ] }
64
+ include_examples "a header-named mismatch"
65
+ end
66
+
67
+ context "for a typed 403 with no data" do
68
+ let(:mismatch) { json_response(403, { type: mismatch_type, error: "Region mismatch" }, "X-Files-Host" => "target.example.com") }
69
+ include_examples "a header-named mismatch"
70
+ end
71
+
72
+ context "for a typed 403 whose data names no host" do
73
+ let(:mismatch) { json_response(403, { type: mismatch_type, error: "Region mismatch", data: { host: nil } }, "x-files-host" => target) }
74
+ include_examples "a header-named mismatch"
75
+ end
76
+
77
+ context "for a typed 403 whose data host is empty" do
78
+ let(:mismatch) { json_response(403, { type: mismatch_type, error: "Region mismatch", data: { host: "" } }, "X-Files-Host" => "target.example.com") }
79
+ include_examples "a header-named mismatch"
80
+ end
81
+
82
+ it "uses the typed data host before a different host header and keeps the origin on returned models" do
83
+ requests = []
84
+ client = stubbed_api_client do |stub|
85
+ stub.get("#{source}#{path}") do |env|
86
+ requests << env.url.to_s
87
+ json_response(403, { type: mismatch_type, data: { host: "target.example.com" } }, "X-Files-Host" => "unapproved.example.com")
88
+ end
89
+ stub.get("#{target}#{path}") do |env|
90
+ requests << env.url.to_s
91
+ expect(env.request_headers["X-FilesAPI-Key"]).to eq("request-key")
92
+ json_response(200, { id: 842 })
93
+ end
94
+ end
95
+
96
+ bundle = Files::Bundle.find(842, {}, api_key: "request-key", client: client)
97
+
98
+ expect(bundle.options).to include(base_url: target, api_key: "request-key", client: client)
99
+ expect(requests).to eq([ "#{source}#{path}?id=842", "#{target}#{path}?id=842" ])
100
+ end
101
+
102
+ {
103
+ "a path" => "target.example.com/private",
104
+ "credentials" => "user@target.example.com",
105
+ "a query" => "target.example.com?token=private",
106
+ "a fragment" => "target.example.com#private",
107
+ "whitespace" => " target.example.com",
108
+ "an unapproved origin" => "unapproved.example.com",
109
+ "the original origin" => "issuer.example.com"
110
+ }.each do |description, host|
111
+ it "refuses a legacy target naming #{description}" do
112
+ client = stubbed_api_client do |stub|
113
+ stub.get("#{source}#{path}") { [ 403, { "X-Files-Host" => host }, legacy_body ] }
114
+ end
115
+
116
+ expect { Files::Bundle.find(842, {}, api_key: "key", client: client) }.to raise_error(Files::LockoutRegionMismatchError) { |error|
117
+ expect(error.http_status).to eq(403)
118
+ expect(error.http_body).to eq(legacy_body)
119
+ expect(error.http_headers["x-files-host"]).to eq(host)
120
+ expect(error.data).to eq(host: host)
121
+ }
122
+ end
123
+ end
124
+
125
+ [ "target.example.com/private", 42 ].each do |host|
126
+ it "does not mask an invalid typed data host #{host.inspect} with a valid host header" do
127
+ client = stubbed_api_client do |stub|
128
+ stub.get("#{source}#{path}") { json_response(403, { type: mismatch_type, data: { host: host } }, "X-Files-Host" => "target.example.com") }
129
+ end
130
+
131
+ expect { Files::Bundle.find(842, {}, api_key: "key", client: client) }.to raise_error(Files::LockoutRegionMismatchError)
132
+ end
133
+ end
134
+
135
+ it "does not treat non-object typed data as a host-header fallback" do
136
+ client = stubbed_api_client do |stub|
137
+ stub.get("#{source}#{path}") { json_response(403, { type: mismatch_type, data: [] }, "X-Files-Host" => "target.example.com") }
138
+ end
139
+
140
+ expect { Files::Bundle.find(842, {}, api_key: "key", client: client) }.to raise_error(Files::LockoutRegionMismatchError)
141
+ end
142
+
143
+ it "requires the configured source's authority even when a region handler approves" do
144
+ Files.region_http_pairs = []
145
+ Files.region_handler = ->(_event) { raise "an untrusted answer must not ask for approval" }
146
+ client = stubbed_api_client do |stub|
147
+ stub.get("#{source}#{path}") { [ 403, { "X-Files-Host" => "target.example.com" }, legacy_body ] }
148
+ stub.get("https://issuer.example.com#{path}") { [ 403, { "X-Files-Host" => "target.example.com" }, legacy_body ] }
149
+ end
150
+
151
+ [ source, "https://issuer.example.com" ].each do |origin|
152
+ expect { Files::Bundle.find(842, {}, api_key: "key", base_url: origin, client: client) }.to raise_error(Files::LockoutRegionMismatchError)
153
+ end
154
+ end
155
+
156
+ [ :disabled, :declined ].each do |policy|
157
+ it "keeps a legacy mismatch available to callers when routing is #{policy}" do
158
+ Files.region_policy = :disabled if policy == :disabled
159
+ Files.region_handler = ->(_event) { false } if policy == :declined
160
+ client = stubbed_api_client do |stub|
161
+ stub.get("#{source}#{path}") { [ 403, { "X-Files-Host" => "target.example.com" }, legacy_body ] }
162
+ end
163
+
164
+ expect { Files::Bundle.find(842, {}, api_key: "key", client: client) }.to raise_error(Files::LockoutRegionMismatchError) { |error|
165
+ expect(error.http_body).to eq(legacy_body)
166
+ expect(error.response.http_body).to eq(legacy_body)
167
+ }
168
+ end
169
+ end
170
+
171
+ it "does not route unrelated 403 errors or a legacy-looking answer with another status or no host" do
172
+ answers = [
173
+ json_response(403, { type: "not-authenticated", error: "Forbidden" }, "X-Files-Host" => "target.example.com"),
174
+ [ 403, { "X-Files-Host" => "target.example.com" }, "Forbidden" ],
175
+ [ 403, {}, legacy_body ],
176
+ [ 400, { "X-Files-Host" => "target.example.com" }, legacy_body ]
177
+ ]
178
+ answers.each do |answer|
179
+ client = stubbed_api_client do |stub|
180
+ stub.get("#{source}#{path}") { answer }
181
+ end
182
+
183
+ expect { Files::Bundle.find(842, {}, api_key: "key", client: client) }.to raise_error(Files::Error) { |error|
184
+ expect(error).not_to be_a(Files::LockoutRegionMismatchError)
185
+ }
186
+ end
187
+ end
188
+
189
+ it "shares the retry budget with ordinary failures before and after a region continuation" do
190
+ [ 0, 1, 2 ].each do |retries|
191
+ Files.max_network_retries = retries
192
+ requests = []
193
+ client = stubbed_api_client do |stub|
194
+ stub.get("#{source}#{path}") do |env|
195
+ requests << env.url.host
196
+ requests.size == 1 ? json_response(429, { error: "Busy" }) : [ 403, { "X-Files-Host" => "target.example.com" }, legacy_body ]
197
+ end
198
+ stub.get("#{target}#{path}") do |env|
199
+ requests << env.url.host
200
+ json_response(429, { error: "Busy" })
201
+ end
202
+ end
203
+
204
+ expect { Files::Bundle.find(842, {}, api_key: "key", client: client) }.to raise_error(Files::APIError)
205
+ expect(requests).to eq([ "issuer.example.com", "issuer.example.com", "target.example.com" ].first(retries + 1))
206
+ end
207
+ end
208
+
209
+ it "does not continue a mismatch when retries are disabled" do
210
+ Files.max_network_retries = 0
211
+ client = stubbed_api_client do |stub|
212
+ stub.get("#{source}#{path}") { [ 403, { "X-Files-Host" => "target.example.com" }, legacy_body ] }
213
+ end
214
+
215
+ expect { Files::Bundle.find(842, {}, api_key: "key", client: client) }.to raise_error(Files::LockoutRegionMismatchError)
216
+ end
217
+
218
+ it "does not continue a second mismatch even when ordinary retries remain" do
219
+ requests = []
220
+ Files.region_http_pairs = [ [ source, target ], [ target, source ] ]
221
+ client = stubbed_api_client do |stub|
222
+ [ source, target ].each do |origin|
223
+ stub.get("#{origin}#{path}") do |env|
224
+ requests << env.url.host
225
+ [ 403, { "X-Files-Host" => origin == source ? target : source }, legacy_body ]
226
+ end
227
+ end
228
+ end
229
+
230
+ expect { Files::Bundle.find(842, {}, api_key: "key", client: client) }.to raise_error(Files::LockoutRegionMismatchError)
231
+ expect(requests).to eq([ "issuer.example.com", "target.example.com" ])
232
+ end
233
+
234
+ it "does not replay a stream body or reroute a signed storage URL" do
235
+ signed_url = "#{source}/part?X-Amz-Signature=unchanged%2Fvalue"
236
+ requests = []
237
+ client = stubbed_api_client do |stub|
238
+ stub.put("#{source}#{path}") do |env|
239
+ requests << env.url.to_s
240
+ [ 403, { "X-Files-Host" => "target.example.com" }, legacy_body ]
241
+ end
242
+ stub.put(signed_url) do |env|
243
+ requests << env.url.to_s
244
+ expect(env.request_headers).not_to have_key("X-FilesAPI-Key")
245
+ expect(env.request_headers).not_to have_key("X-FilesAPI-Auth")
246
+ [ 403, { "X-Files-Host" => "target.example.com" }, legacy_body ]
247
+ end
248
+ end
249
+
250
+ expect { client.execute_request(:put, "/bundles/842", api_key: "key", params: { content: StringIO.new("bytes") }) }.to raise_error(Files::LockoutRegionMismatchError)
251
+ expect { client.remote_request(:put, signed_url, {}, "part") }.to raise_error(Files::APIConnectionError)
252
+ expect(requests).to eq([ "#{source}#{path}", signed_url ])
253
+ end
254
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: files.com
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.803
4
+ version: 1.1.805
5
5
  platform: ruby
6
6
  authors:
7
7
  - files.com
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-10-27 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: addressable
@@ -487,6 +486,7 @@ files:
487
486
  - spec/decimal_contract_spec.rb
488
487
  - spec/language_spec.rb
489
488
  - spec/lib/api_client_spec.rb
489
+ - spec/lib/region_routing_spec.rb
490
490
  - spec/list_spec.rb
491
491
  - spec/models/file_download_default_path_spec.rb
492
492
  - spec/models/file_spec.rb
@@ -507,7 +507,6 @@ homepage: https://www.files.com
507
507
  licenses:
508
508
  - MIT
509
509
  metadata: {}
510
- post_install_message:
511
510
  rdoc_options: []
512
511
  require_paths:
513
512
  - lib
@@ -522,8 +521,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
522
521
  - !ruby/object:Gem::Version
523
522
  version: '0'
524
523
  requirements: []
525
- rubygems_version: 3.5.20
526
- signing_key:
524
+ rubygems_version: 4.0.20
527
525
  specification_version: 4
528
526
  summary: Files.com Ruby client.
529
527
  test_files: []