idempo 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70c151d3823c956aac177dbf39ee4e5f216b9464a739b134c8a47822a01aa765
4
- data.tar.gz: 4306e629c9d0f1bd59ad74bc6e61623e441e4713b88bccb8f69c08e8553dc251
3
+ metadata.gz: 3db7fbb3c612bae5675890f118986c3d026e49c73f3c74c24f15fea51e6a67c7
4
+ data.tar.gz: 65a193261af6e424fe8f7e3437d28e02c20ddb4c9ae199c9556f39676474b8ac
5
5
  SHA512:
6
- metadata.gz: 5e80a42f84a311e633697123f7213af21b0f79d501937d5963e4dd918eb5cabd5d01174d781b0528d4f51923fc9eb038af20afeb164c8cdd51d2cc1983ea1111
7
- data.tar.gz: 4d2ee3d361d009aacc23c42ec38323c9b9373d874c9abbd70596882dbd48d6ed4edcf9eb5c32f44e366aa091e178dfcd71c919b45d6bc1b8c17804094b23629f
6
+ metadata.gz: bd32562481a479070ed2806ef3201d370417fd9acfbbb8499d1aefb2f1000f278ee3dff47ccb0ac640f33001be8c41cccba7c498980d07c893dee519e53f539b
7
+ data.tar.gz: df3b05f901ac7e49bb82bfde9a522e2a179b96953db10751b7814858597a5a0af0aa82ff0fd6824e3a11cae6bdf4ebc84004a35b09cfae7a39958114a307eef8
@@ -34,19 +34,13 @@ jobs:
34
34
  - name: Standard (Lint)
35
35
  run: bundle exec rake standard
36
36
  test:
37
- name: Specs
37
+ name: "Specs (Rack 2 and 3)"
38
38
  runs-on: ubuntu-22.04
39
39
  if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
40
- env:
41
- IDEMPO_RACK_VERSION: ${{ matrix.rack }}
42
40
  strategy:
43
41
  matrix:
44
42
  ruby:
45
43
  - "2.7"
46
- - "3.2"
47
- rack:
48
- - "2.0"
49
- - "3.0"
50
44
  services:
51
45
  mysql:
52
46
  image: mysql:5.7
@@ -76,8 +70,8 @@ jobs:
76
70
  with:
77
71
  ruby-version: ${{ matrix.ruby }}
78
72
  bundler-cache: true
79
- - name: RSpec
80
- run: bundle exec rspec
73
+ - name: RSpec via Appraisal
74
+ run: "bundle exec appraisal install && bundle exec appraisal rspec"
81
75
  env:
82
76
  MYSQL_HOST: 127.0.0.1
83
77
  MYSQL_PORT: 3306
data/Appraisals ADDED
@@ -0,0 +1,9 @@
1
+ appraise "rack-3" do
2
+ gem "rack", ">= 3.0"
3
+ gem "activerecord", "~> 7.0", "< 9.0"
4
+ end
5
+
6
+ appraise "rack-2" do
7
+ gem "rack", ">= 2.0", "< 3.0"
8
+ gem "activerecord", "~> 6", "< 7.0"
9
+ end
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 1.3.0
2
+
3
+ - Streamline integration with both Rack 2 and 3, add tests for request fingerprinting.
4
+
1
5
  ## 1.2.2
2
6
 
3
7
  - Support `#to_ary` on Rack response bodies on newer Rails/Rack versions
data/Rakefile CHANGED
@@ -3,7 +3,11 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
5
  require "standard/rake"
6
+ require "appraisal"
6
7
 
7
8
  RSpec::Core::RakeTask.new(:spec)
8
-
9
9
  task default: :spec
10
+
11
+ if !ENV["APPRAISAL_INITIALIZED"]
12
+ task default: :appraisal
13
+ end
data/idempo.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
29
29
  # Specify which files should be added to the gem when it is released.
30
30
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
31
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
32
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
32
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|gemfiles)/}) }
33
33
  end
34
34
  spec.bindir = "exe"
35
35
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
@@ -37,8 +37,8 @@ Gem::Specification.new do |spec|
37
37
 
38
38
  spec.add_dependency "msgpack"
39
39
  spec.add_dependency "measurometer", "~> 1.3"
40
+ spec.add_dependency "rack", "< 4"
40
41
 
41
- spec.add_development_dependency "rack", "~> 3"
42
42
  spec.add_development_dependency "rake", "~> 13.0"
43
43
  spec.add_development_dependency "rspec", "~> 3.0"
44
44
  spec.add_development_dependency "redis", "~> 4"
@@ -47,6 +47,7 @@ Gem::Specification.new do |spec|
47
47
  spec.add_development_dependency "mysql2"
48
48
  spec.add_development_dependency "pg"
49
49
  spec.add_development_dependency "standard"
50
+ spec.add_development_dependency "appraisal"
50
51
 
51
52
  # For more information and examples about making a new gem, checkout our
52
53
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -8,6 +8,6 @@ class Idempo::ConcurrentRequestErrorApp
8
8
  message: "Another request with this idempotency key is still in progress, please try again later"
9
9
  }
10
10
  }
11
- [429, {"Retry-After" => RETRY_AFTER_SECONDS, "Content-Type" => "application/json"}, [JSON.pretty_generate(res)]]
11
+ [429, {"retry-after" => RETRY_AFTER_SECONDS, "content-type" => "application/json"}, [JSON.pretty_generate(res)]]
12
12
  end
13
13
  end
@@ -6,6 +6,6 @@ class Idempo::MalformedKeyErrorApp
6
6
  message: "The Idempotency-Key header provided was empty or malformed"
7
7
  }
8
8
  }
9
- [400, {"Content-Type" => "application/json"}, [JSON.pretty_generate(res)]]
9
+ [400, {"content-type" => "application/json"}, [JSON.pretty_generate(res)]]
10
10
  end
11
11
  end
@@ -5,11 +5,24 @@ module Idempo::RequestFingerprint
5
5
  d << rack_request.url << "\n"
6
6
  d << rack_request.request_method << "\n"
7
7
  d << rack_request.get_header("HTTP_AUTHORIZATION").to_s << "\n"
8
- while (chunk = rack_request.env["rack.input"].read(1024 * 65))
9
- d << chunk
8
+
9
+ # Under Rack 3.0 the rack.input may or may not be rewindable (this is done to support
10
+ # streaming HTTP request bodies). If we know a request body is rewindable we can read it
11
+ # out in full and add it to the request fingerprint. If the request body cannot be
12
+ # rewound, we can't really rely on it as it can be fairly large (and we want the
13
+ # downstream app to read the request body, not us).
14
+ if rack_request.env["rack.input"].respond_to?(:rewind)
15
+ read_and_rewind(rack_request.env["rack.input"], d)
10
16
  end
17
+
11
18
  Base64.strict_encode64(d.digest)
19
+ end
20
+
21
+ def self.read_and_rewind(source_io, to_destination_io)
22
+ while (chunk = source_io.read(1024 * 65))
23
+ to_destination_io << chunk
24
+ end
12
25
  ensure
13
- rack_request.env["rack.input"].rewind
26
+ source_io.rewind
14
27
  end
15
28
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Idempo
4
- VERSION = "1.2.2"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/idempo.rb CHANGED
@@ -55,14 +55,16 @@ class Idempo
55
55
  return from_persisted_response(stored_response)
56
56
  end
57
57
 
58
- status, headers, body = @app.call(env)
58
+ status, raw_headers, body = @app.call(env)
59
+ headers = downcase_keys(raw_headers)
59
60
 
60
- expires_in_seconds = (headers.delete("X-Idempo-Persist-For-Seconds") || @persist_for_seconds).to_i
61
+ expires_in_seconds = (headers.delete("x-idempo-persist-for-seconds") || @persist_for_seconds).to_i
61
62
 
62
- # In some cases `body` could respond to to_ary. In this case, we don't need to call .close on body afterwards.
63
+ # In some cases `body` could respond to to_ary. In this case, we don't need to
64
+ # call .close on the body afterwards, as it is supposed to self-close as per Rack 3.0 SPEC
63
65
  #
64
66
  # @see https://github.com/rack/rack/blob/main/SPEC.rdoc#the-body-
65
- body = body.to_ary if rack_v3? && body.respond_to?(:to_ary)
67
+ body = body.to_ary if body.respond_to?(:to_ary)
66
68
 
67
69
  if response_may_be_persisted?(status, headers, body)
68
70
  # Body is replaced with a cached version since a Rack response body is not rewindable
@@ -83,8 +85,10 @@ class Idempo
83
85
 
84
86
  private
85
87
 
86
- def rack_v3?
87
- Gem::Version.new(Rack.release) >= Gem::Version.new("3.0")
88
+ def downcase_keys(raw_headers)
89
+ raw_headers.each_with_object({}) do |(name, value), hh|
90
+ hh[name.to_s.downcase] = value
91
+ end
88
92
  end
89
93
 
90
94
  def from_persisted_response(marshaled_response)
@@ -120,17 +124,18 @@ class Idempo
120
124
  end
121
125
 
122
126
  def response_may_be_persisted?(status, headers, body)
123
- return false if headers.delete("X-Idempo-Policy") == "no-store"
127
+ return false if headers.delete("x-idempo-policy") == "no-store"
124
128
  return false unless status_may_be_persisted?(status)
125
129
  return false unless body_size_within_limit?(headers, body)
126
130
  true
127
131
  end
128
132
 
129
133
  def body_size_within_limit?(response_headers, body)
130
- return response_headers["Content-Length"].to_i <= SAVED_RESPONSE_BODY_SIZE_LIMIT if response_headers["Content-Length"]
134
+ if response_headers["content-length"]
135
+ return response_headers["content-length"].to_i <= SAVED_RESPONSE_BODY_SIZE_LIMIT
136
+ end
131
137
 
132
138
  return false unless body.is_a?(Array) # Arbitrary iterable of unknown size
133
-
134
139
  sum_of_string_bytesizes(body) <= SAVED_RESPONSE_BODY_SIZE_LIMIT
135
140
  end
136
141
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idempo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2024-09-20 00:00:00.000000000 Z
12
+ date: 2024-11-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: msgpack
@@ -43,16 +43,16 @@ dependencies:
43
43
  name: rack
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - "~>"
46
+ - - "<"
47
47
  - !ruby/object:Gem::Version
48
- version: '3'
49
- type: :development
48
+ version: '4'
49
+ type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - "~>"
53
+ - - "<"
54
54
  - !ruby/object:Gem::Version
55
- version: '3'
55
+ version: '4'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: rake
58
58
  requirement: !ruby/object:Gem::Requirement
@@ -165,6 +165,20 @@ dependencies:
165
165
  - - ">="
166
166
  - !ruby/object:Gem::Version
167
167
  version: '0'
168
+ - !ruby/object:Gem::Dependency
169
+ name: appraisal
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
168
182
  description: Provides idempotency keys for Rack applications.
169
183
  email:
170
184
  - me@julik.nl
@@ -177,6 +191,7 @@ files:
177
191
  - ".gitignore"
178
192
  - ".rubocop.yml"
179
193
  - ".standard.yml"
194
+ - Appraisals
180
195
  - CHANGELOG.md
181
196
  - Gemfile
182
197
  - LICENSE.txt
@@ -220,7 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
235
  - !ruby/object:Gem::Version
221
236
  version: '0'
222
237
  requirements: []
223
- rubygems_version: 3.3.7
238
+ rubygems_version: 3.1.6
224
239
  signing_key:
225
240
  specification_version: 4
226
241
  summary: Idempotency keys for all.