ruby-http-session 1.0.1 → 2.1.0

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: '0787793708be50172480874386353726e9c1ea3c84a78dd3850772d8f5e0a5df'
4
- data.tar.gz: '0519180cc38ad17de9f85cb24758c9e057f6e847868b05203ac4b566eecf9329'
3
+ metadata.gz: cfd1a068f4386cf1a7965a369a01283836a314425141cdfedfdd14edb0330d88
4
+ data.tar.gz: 6efc3597fa28d442fd793a24a9dc9a1020a1effc123bdad55fbd116470f2c641
5
5
  SHA512:
6
- metadata.gz: 734e2a67d0719a07fec84366fbb3bdd1417302a38b3c881db1a832870ecb3ade5006419f49041b4e29e55f8c310e4b77b92e824d9fef1815a20f7bb20bf6ac0e
7
- data.tar.gz: ee8338fc2a6ab4026d41fa9cd8f3e96cb61662d54e5b5c26c9f57df1ca8c9a005989d8529764b5638760723ec8c819e452d13fc24edfd72ec650ec6f6ad385d8
6
+ metadata.gz: 4b03ee19f4e6cbdb1f74ff6046e0f11edae268821ee76408614c505a9adcb8c06614415d0b042acf84db09a833eda46623cb4d1e906bc4f3db3afb0133edae9f
7
+ data.tar.gz: c1fdd8b754c9fa7fbbcb18d4f7383ffee61dcc7a296a5f2c170a700606afcc8e46b92971196dc92994b8f4e34f54db12a46932578095bd77eff78706b00f676d
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ gemspec
8
8
  # optional dependencies
9
9
  gem "activesupport", "~> 7.0"
10
10
  gem "brotli", platforms: :ruby
11
+ gem "sqlite3", platforms: :ruby
11
12
 
12
13
  # rake
13
14
  gem "rake", "~> 13.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby-http-session (1.0.1)
4
+ ruby-http-session (2.1.0)
5
5
  http (~> 5.1, >= 5.1.1)
6
6
 
7
7
  GEM
@@ -125,6 +125,7 @@ GEM
125
125
  thor (~> 1.0)
126
126
  tilt (~> 2.0)
127
127
  yard (~> 0.9, >= 0.9.24)
128
+ sqlite3 (1.6.7-x86_64-linux)
128
129
  standard (1.31.1)
129
130
  language_server-protocol (~> 3.17.0.2)
130
131
  lint_roller (~> 1.0)
@@ -169,6 +170,7 @@ DEPENDENCIES
169
170
  rubocop (~> 1.29)
170
171
  ruby-http-session!
171
172
  solargraph
173
+ sqlite3
172
174
  standard (~> 1.3)
173
175
  timecop
174
176
  vcr
data/README.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # HTTP::Session
2
2
 
3
- HTTP::Session - a session abstraction for [http.rb] in order to support **cookies** and **caching**.
3
+ HTTP::Session - a session abstraction for [http.rb] in order to support **Cookies** and **Caching**.
4
+
5
+ ```ruby
6
+ require "http-session"
7
+ require "logger"
8
+
9
+ http = HTTP.session(
10
+ cookies: true,
11
+ cache: {private: true, store: [:file_store, "./tmp/cache"]},
12
+ persistent: {pools: {"https://httpbin.org" => true}}
13
+ )
14
+ .timeout(8)
15
+ .follow
16
+ .use(logging: {logger: Logger.new($stdout)})
17
+ .freeze
18
+
19
+ http.get("https://httpbin.org/get")
20
+ ```
4
21
 
5
22
 
6
23
  ## Quickstart
@@ -15,81 +32,55 @@ gem 'ruby-http-session', require: "http-session"
15
32
 
16
33
  ### Cookies
17
34
 
18
- The cookies are set automatically each time a request is made:
35
+ Use `cookies: true` to enable this feature. Once enabled, the cookies are
36
+ automatically set each time a request is made.
19
37
 
20
38
  ```ruby
21
39
  require "http-session"
22
40
 
23
41
  http = HTTP.session(cookies: true)
24
- .follow
25
42
  .freeze
26
43
 
27
- r = http.get("https://httpbin.org/cookies/set/mycookies/abc")
44
+ r = http.get("https://httpbin.org/cookies/set/mycookies/abc", follow: true)
28
45
  pp JSON.parse(r.body)["cookies"] # -> {"mycookies"=>"abc"}
29
46
 
30
47
  r = http.get("https://httpbin.org/cookies")
31
48
  pp JSON.parse(r.body)["cookies"] # -> {"mycookies"=>"abc"}
32
-
33
- http.jar.map { |c| pp [c.domain, c.path, c.to_s] } # => ["httpbin.org", "/", "mycookies=abc"]
34
49
  ```
35
50
 
36
51
  ### Caching
37
52
 
38
- When responses can be reused from a cache, taking into account [HTTP RFC 9111] rules for user agents and
39
- shared caches. The following headers are used to determine whether the response is cacheable or not:
40
-
41
- * `Cache-Control` request header
42
- * `no-store`
43
- * `no-cache`
44
- * `Cache-Control` response header
45
- * `no-store`
46
- * `no-cache`
47
- * `private`
48
- * `public`
49
- * `max-age`
50
- * `s-maxage`
51
- * `Etag` & `Last-Modified` response header for conditional requests
52
- * `Vary` response header for content negotiation
53
-
54
- **This takes 60 times to deliver the request to the origin server:**
53
+ Use `cache: true` to enable this feature. Once enabled, the `Cache-Control`
54
+ will be used to determine whether the response is cacheable or not.
55
55
 
56
56
  ```ruby
57
- require "active_support/all"
58
- require "http"
59
-
60
- ActiveSupport::Notifications.subscribe('start_request.http') do |name, start, finish, id, payload|
61
- pp start: start, req: payload[:request].inspect
62
- end
57
+ require "http-session"
63
58
 
64
- http = HTTP
65
- .follow
66
- .timeout(8)
67
- .use(instrumentation: { instrumenter: ActiveSupport::Notifications.instrumenter })
59
+ http = HTTP.session(cache: true)
60
+ .freeze
68
61
 
62
+ # takes only 1 time to deliver the request to the origin server
69
63
  60.times do
70
- http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js", headers: {"Accept-Encoding" => ""})
64
+ http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js")
71
65
  end
72
66
  ```
73
67
 
74
- **This only takes 1 time to deliver the request to the origin server:**
68
+ ### Persistent Connections (Keep-Alive)
69
+
70
+ Use `persistent: true` to enable this feature. Once enabled, the connection pools
71
+ will be used to manage persistent connections.
75
72
 
76
73
  ```ruby
77
- require "active_support/all"
78
74
  require "http-session"
79
75
 
80
- ActiveSupport::Notifications.subscribe('start_request.http') do |name, start, finish, id, payload|
81
- pp start: start, req: payload[:request].inspect
82
- end
83
-
84
- http = HTTP.session(cache: true)
85
- .follow
86
- .timeout(8)
87
- .use(instrumentation: { instrumenter: ActiveSupport::Notifications.instrumenter })
76
+ http = HTTP.session(persistent: true)
88
77
  .freeze
89
78
 
90
- 60.times do
91
- http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js", headers: {"Accept-Encoding" => ""})
92
- end
79
+ http.get("https://httpbin.org/get") # create a persistent connection#1
80
+ http.get("https://httpbin.org/get") # reuse connection#1
81
+
82
+ http.get("https://example.org") # create a persistent connection#2
83
+ http.get("https://example.org") # reuse connection#2
93
84
  ```
94
85
 
95
86
 
@@ -97,29 +88,45 @@ end
97
88
 
98
89
  ### Caching
99
90
 
91
+ When responses can be reused from a cache, taking into account [HTTP RFC 9111] rules for user agents and
92
+ shared caches. The following headers are used to determine whether the response is cacheable or not:
93
+
94
+ * `Cache-Control` request header
95
+ * `no-store`
96
+ * `no-cache`
97
+ * `Cache-Control` response header
98
+ * `no-store`
99
+ * `no-cache`
100
+ * `private`
101
+ * `public`
102
+ * `max-age`
103
+ * `s-maxage`
104
+ * `Etag` & `Last-Modified` response header for conditional requests
105
+ * `Vary` response header for content negotiation
106
+
100
107
  #### Shared Cache
101
108
 
102
109
  A [shared cache] is a cache that stores responses for **reuse by more than one user**; shared caches
103
110
  are usually (but not always) deployed as a part of an intermediary. **This is used by default**.
104
111
 
112
+ **Note**: Responses for requests with **Authorization** header fields will not be stored in a shared
113
+ cache unless explicitly allowed. Read [rfc9111#section-3.5] for more.
114
+
105
115
  ```ruby
106
116
  http = HTTP.session(cache: true) # or HTTP.session(cache: {shared: true})
107
- .follow
108
- .timeout(4)
109
- .use(hsf_auto_inflate: {br: true})
110
117
  .freeze
111
118
 
112
119
  res = http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js")
113
- p "cache-status: #{res.headers["x-httprb-cache-status"]}" # => miss
120
+ pp "Cache-Status: #{res.headers["X-Httprb-Cache-Status"]}" # -> "Cache-Status: MISS"
114
121
 
115
122
  res = http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js")
116
- p "cache-status: #{res.headers["x-httprb-cache-status"]}" # => hit
123
+ pp "Cache-Status: #{res.headers["X-Httprb-Cache-Status"]}" # -> "Cache-Status: HIT"
117
124
 
118
- res = http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js", headers: {"cache-control" => "no-cache"})
119
- p "cache-status: #{res.headers["x-httprb-cache-status"]}" # => revalidated
125
+ res = http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js", headers: {"Cache-Control" => "no-cache"})
126
+ pp "Cache-Status: #{res.headers["X-Httprb-Cache-Status"]}" # -> "Cache-Status: REVALIDATED"
120
127
 
121
- res = http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js", headers: {"cache-control" => "no-store"})
122
- p "cache-status: #{res.headers["x-httprb-cache-status"]}" # => uncacheable
128
+ res = http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js", headers: {"Cache-Control" => "no-store"})
129
+ pp "Cache-Status: #{res.headers["X-Httprb-Cache-Status"]}" # -> "Cache-Status: UNCACHEABLE"
123
130
  ```
124
131
 
125
132
  #### Private Cache
@@ -129,9 +136,6 @@ component of a user agent.
129
136
 
130
137
  ```ruby
131
138
  http = HTTP.session(cache: {private: true})
132
- .follow
133
- .timeout(4)
134
- .use(hsf_auto_inflate: {br: true})
135
139
  .freeze
136
140
  ```
137
141
 
@@ -143,9 +147,6 @@ can use ths `:store` option to set another store, e.g. `ActiveSupport::Cache::Me
143
147
  ```ruby
144
148
  store = ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
145
149
  http = HTTP.session(cache: {store: store})
146
- .follow
147
- .timeout(4)
148
- .use(hsf_auto_inflate: {br: true})
149
150
  .freeze
150
151
  ```
151
152
 
@@ -159,14 +160,109 @@ The following value is used in the `X-Httprb-Cache-Status` response header:
159
160
  * **MISS**: not found in cache, served from the origin server
160
161
  * **UNCACHEABLE**: the request can not use cached response
161
162
 
163
+ ### Persistent Connections (Keep-Alive)
164
+
165
+ ```ruby
166
+ http = HTTP.session(persistent: {
167
+ pools: {
168
+ "https://example.org" => false,
169
+ "https://httpbin.org" => {maxsize: 2},
170
+ "*" => true
171
+ }
172
+ }).freeze
173
+
174
+ # match host: "https://example.org"
175
+ # -> create a non-persistent connection
176
+ http.get("https://example.org")
177
+
178
+ # match host: "https://httpbin.org"
179
+ # -> create a connection pool with maxsize 2, and return a persistent connection
180
+ http.get("https://httpbin.org/get")
181
+
182
+ # match host: "*"
183
+ # -> create a connection pool with maxsize 5, and return a persistent connection
184
+ http.get("https://github.com")
185
+ ```
186
+
187
+ ### Thread Safe
188
+
189
+ The following **HTTP::Session** methods are thread-safe:
190
+
191
+ * **head**
192
+ * **get**
193
+ * **post**
194
+ * **put**
195
+ * **delete**
196
+ * **trace**
197
+ * **options**
198
+ * **connect**
199
+ * **patch**
200
+ * **request**
201
+
162
202
  ### HTTP::Features
163
203
 
164
- The following features are available with `http-session`:
204
+ #### logging
205
+
206
+ Log requests and responses.
207
+
208
+ ```ruby
209
+ require "http-session"
210
+ require "logger"
211
+
212
+ http = HTTP.session
213
+ .use(logging: { logger: Logger.new($stdout) })
214
+ .freeze
165
215
 
166
- * [logging]: Log requests and responses.
167
- * [instrumentation]: Instrument requests and responses. Expects an ActiveSupport::Notifications-compatible instrumenter.
168
- * [hsf_auto_inflate]: Simlar to [auto_inflate], used for automatically decompressing the response body.
169
- * etc.
216
+ http.get("https://httpbin.org/get")
217
+ # I, [2023-10-07T13:17:42.208296 #2708620] INFO -- : > GET https://httpbin.org/get
218
+ # D, [2023-10-07T13:17:42.208349 #2708620] DEBUG -- : Connection: close
219
+ # Host: httpbin.org
220
+ # User-Agent: http.rb/5.1.1
221
+ # ...
222
+ ```
223
+
224
+ #### instrumentation
225
+
226
+ Instrument requests and responses. Expects an ActiveSupport::Notifications-compatible instrumenter.
227
+
228
+ ```ruby
229
+ require "http-session"
230
+ require "active_support/all"
231
+
232
+ ActiveSupport::Notifications.subscribe('start_request.http') do |name, start, finish, id, payload|
233
+ pp start: start, req: payload[:request].inspect
234
+ end
235
+
236
+ ActiveSupport::Notifications.subscribe('request.http') do |name, start, finish, id, payload|
237
+ pp start: start, req: payload[:request].inspect, res: payload[:response].inspect
238
+ end
239
+
240
+ http = HTTP.session
241
+ .use(instrumentation: { instrumenter: ActiveSupport::Notifications.instrumenter })
242
+ .freeze
243
+
244
+ http.get("https://httpbin.org/get")
245
+ # {:start=>2023-10-07 13:14:36.953487129 +0800, :req=>"#<HTTP::Request/1.1 GET https://httpbin.org/get>"}
246
+ # {:start=>2023-10-07 13:14:36.954112865 +0800,
247
+ # :req=>"nil",
248
+ # :res=>"#<HTTP::Response/1.1 200 OK {\"Date\"=>\"Sat, 07 Oct 2023 05:14:37 GMT\", \"Content-Type\"=>\"application/json\", \"Content-Length\"=>\"236\", \"Connection\"=>\"close\", \"Server\"=>\"gunicorn/19.9.0\", \"Access-Control-Allow-Origin\"=>\"*\", \"Access-Control-Allow-Credentials\"=>\"true\", \"X-Httprb-Cache-Status\"=>\"MISS\"}>"}
249
+ ```
250
+
251
+ #### hsf_auto_inflate
252
+
253
+ Simlar to `auto_inflate`, used for automatically decompressing the response body.
254
+
255
+ ```ruby
256
+ require "http-session"
257
+ require "brotli"
258
+
259
+ http = HTTP.session
260
+ .use(hsf_auto_inflate: {br: true})
261
+ .freeze
262
+
263
+ res = http.get("https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js", headers: {"Accept-Encoding" => "br"})
264
+ pp res.body.to_s # => "/*! jQuery v3.6.4 | ...
265
+ ```
170
266
 
171
267
  ### Intergate with WebMock
172
268
 
@@ -198,10 +294,7 @@ Everyone interacting in the HTTP::Session project's codebases, issue trackers, c
198
294
 
199
295
 
200
296
  [HTTP RFC 9111]:https://datatracker.ietf.org/doc/html/rfc9111/
297
+ [rfc9111#section-3.5]:https://datatracker.ietf.org/doc/html/rfc9111/#section-3.5
201
298
  [shared cache]:https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#shared_cache
202
299
  [private cache]:https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#private_caches
203
300
  [http.rb]:https://github.com/httprb/http
204
- [logging]:https://github.com/httprb/http/wiki/Logging-and-Instrumentation#logging
205
- [instrumentation]:https://github.com/httprb/http/wiki/Logging-and-Instrumentation#instrumentation
206
- [auto_inflate]:https://github.com/httprb/http/wiki/Compression#automatic-inflating
207
- [hsf_auto_inflate]:https://github.com/souk4711/http-session/blob/main/lib/http/session/features/auto_inflate.rb
data/http-session.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["John Doe"]
9
9
  spec.email = ["johndoe@example.com"]
10
10
 
11
- spec.summary = "HTTP::Session - a session abstraction for http.rb in order to support cookies and caching."
12
- spec.description = "HTTP::Session - a session abstraction for http.rb in order to support cookies and caching."
11
+ spec.summary = "HTTP::Session - a session abstraction for http.rb in order to support Cookies and Caching."
12
+ spec.description = "HTTP::Session - a session abstraction for http.rb in order to support Cookies and Caching."
13
13
  spec.homepage = "https://github.com/souk4711/http-session"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 2.6.0"
@@ -1,18 +1,33 @@
1
1
  class HTTP::Session
2
2
  class Cache
3
+ extend Forwardable
3
4
  include MonitorMixin
4
5
 
6
+ # @!method enabled?
7
+ # True when it is enabled.
8
+ # @return [Boolean]
9
+ def_delegator :@options, :enabled?
10
+
11
+ # @!method shared_cache?
12
+ # True when it is a shared cache.
13
+ # @return [Boolean]
14
+ def_delegator :@options, :shared_cache?
15
+
16
+ # @!method private_cache?
17
+ # True when it is a private cache.
18
+ # @return [Boolean]
19
+ def_delegator :@options, :private_cache?
20
+
5
21
  # @param [Options::CacheOption] options
6
22
  def initialize(options)
7
23
  super()
8
-
9
24
  @options = options
10
25
  end
11
26
 
12
27
  # Read an entry from cache.
13
28
  #
14
29
  # @param [Request] req
15
- # @return [Entry]
30
+ # @return [nil, Entry]
16
31
  def read(req)
17
32
  synchronize do
18
33
  key = cache_key_for(req)
@@ -37,21 +52,6 @@ class HTTP::Session
37
52
  end
38
53
  end
39
54
 
40
- # True when it is a shared cache.
41
- def shared?
42
- @options.shared_cache?
43
- end
44
-
45
- # True when it is a private cache.
46
- def private?
47
- @options.private_cache?
48
- end
49
-
50
- # @!visibility private
51
- def store
52
- @options.store
53
- end
54
-
55
55
  private
56
56
 
57
57
  def entry_matched?(entry, req)
@@ -91,5 +91,10 @@ class HTTP::Session
91
91
  def cache_key_for(req)
92
92
  Digest::SHA256.hexdigest(req.uri)
93
93
  end
94
+
95
+ # Only available when #enbled? has the value true.
96
+ def store
97
+ @options.store
98
+ end
94
99
  end
95
100
  end
@@ -40,6 +40,12 @@ class HTTP::Session
40
40
  raise
41
41
  end
42
42
 
43
+ def close
44
+ @connection&.close
45
+ @connection = nil
46
+ @state = :clean
47
+ end
48
+
43
49
  private
44
50
 
45
51
  def verify_connection!(uri)
@@ -50,12 +56,6 @@ class HTTP::Session
50
56
  close if @state == :dirty
51
57
  end
52
58
 
53
- def close
54
- @connection&.close
55
- @connection = nil
56
- @state = :clean
57
- end
58
-
59
59
  def build_response(req, options)
60
60
  res = HTTP::Response.new(
61
61
  status: @connection.status_code,
@@ -77,7 +77,7 @@ class HTTP::Session
77
77
  end
78
78
 
79
79
  def build_request(verb, uri, opts = {})
80
- opts = @default_options.merge(opts)
80
+ opts = default_options.merge(opts)
81
81
  uri = make_request_uri(uri, opts)
82
82
  headers = make_request_headers(opts)
83
83
  body = make_request_body(opts, headers)
@@ -9,69 +9,93 @@ class HTTP::Session
9
9
  @session = session
10
10
  end
11
11
 
12
+ # Make an HTTP request.
13
+ #
12
14
  # @param verb
13
15
  # @param uri
14
16
  # @param [Hash] opts
17
+ # @param [nil, Context] ctx
15
18
  # @return [Response]
16
- def request(verb, uri, opts)
17
- data = @session.make_http_request_data
18
- hist = []
19
-
20
- opts = @default_options.merge(opts)
21
- opts = _hs_handle_http_request_options_cookies(opts, data[:cookies])
22
- opts = _hs_handle_http_request_options_follow(opts, hist)
23
-
24
- req = build_request(verb, uri, opts)
25
- res = perform(req, opts)
26
- return res unless opts.follow
27
-
28
- HTTP::Redirector.new(opts.follow).perform(req, res) do |request|
29
- request = HTTP::Session::Request.new(request)
30
- perform(request, opts)
31
- end.tap do |res|
32
- res.history = hist
33
- end
19
+ def request(verb, uri, opts, ctx)
20
+ opts = default_options.merge(opts)
21
+ opts = _hs_handle_http_request_options_cookies(uri, opts)
22
+
23
+ req = _hs_build_request(verb, uri, opts, ctx)
24
+ res = _hs_perform(req, opts)
25
+ _hs_cookies_save(res)
26
+
27
+ res
34
28
  end
35
29
 
36
30
  private
37
31
 
38
- # Make an HTTP request.
39
- #
40
- # @param [Request] req
41
- # @param [HTTP::Options] opts
42
- # @return [Response]
43
- def perform(req, opts)
44
- req = wrap_request(req, opts)
45
- wrap_response(_hs_perform(req, opts), opts)
32
+ # Add session cookies to the request's :cookies.
33
+ def _hs_handle_http_request_options_cookies(uri, opts)
34
+ cookies = _hs_cookies_load(uri)
35
+ cookies.nil? ? opts : opts.with_cookies(cookies)
46
36
  end
47
37
 
48
- # Add session cookie to the request's :cookies.
49
- def _hs_handle_http_request_options_cookies(opts, cookies)
50
- return opts if cookies.nil?
51
- opts.with_cookies(cookies)
38
+ # Load cookies.
39
+ def _hs_cookies_load(uri)
40
+ return unless @session.cookies_mgr.enabled?
41
+ @session.cookies_mgr.read(uri)
52
42
  end
53
43
 
54
- # Wrap the :on_redirect method in the request's :follow.
55
- def _hs_handle_http_request_options_follow(opts, hist)
56
- return opts unless opts.follow
44
+ # Save cookies.
45
+ def _hs_cookies_save(res)
46
+ return unless @session.cookies_mgr.enabled?
47
+ @session.cookies_mgr.write(res)
48
+ end
57
49
 
58
- follow = (opts.follow == true) ? {} : opts.follow
59
- opts.with_follow(follow.merge(
60
- on_redirect: _hs_handle_http_request_options_follow_hijack(follow[:on_redirect], hist)
61
- ))
50
+ # Build a HTTP request.
51
+ def _hs_build_request(verb, uri, opts, ctx)
52
+ return build_request(verb, uri, opts) unless ctx&.follow
53
+ _hs_build_redirection_request(verb, uri, opts, ctx.follow)
62
54
  end
63
55
 
64
- # Wrap the :on_redirect method.
65
- def _hs_handle_http_request_options_follow_hijack(fn, hist)
66
- lambda do |res, req|
67
- hist << res
68
- fn.call(res, req) if fn.respond_to?(:call)
56
+ # Build a HTTP request for redirection.
57
+ def _hs_build_redirection_request(verb, uri, opts, ctx)
58
+ opts = default_options.merge(opts)
59
+ headers = make_request_headers(opts)
60
+ body = make_request_body(opts, headers)
61
+
62
+ # Drop body when non-GET methods changed to GET.
63
+ if ctx.should_drop_body?
64
+ body = nil
65
+ headers.delete(HTTP::Headers::CONTENT_TYPE)
66
+ headers.delete(HTTP::Headers::CONTENT_LENGTH)
67
+ end
68
+
69
+ # Remove Authorization header when rediecting cross site.
70
+ if ctx.cross_origin?
71
+ headers.delete(HTTP::Headers::AUTHORIZATION)
69
72
  end
73
+
74
+ # Build a request.
75
+ req = HTTP::Request.new(
76
+ verb: verb,
77
+ uri: uri,
78
+ uri_normalizer: opts.feature(:normalize_uri)&.normalizer,
79
+ proxy: opts.proxy,
80
+ headers: headers,
81
+ body: body
82
+ )
83
+ HTTP::Session::Request.new(req)
70
84
  end
71
85
 
72
- # Make an HTTP request using cache.
86
+ # Perform a single HTTP request.
87
+ #
88
+ # @param [Request] req
89
+ # @param [HTTP::Options] opts
90
+ # @return [Response]
73
91
  def _hs_perform(req, opts)
74
- if @session.default_options.cache.enabled?
92
+ req = wrap_request(req, opts)
93
+ wrap_response(_hs_perform_no_features(req, opts), opts)
94
+ end
95
+
96
+ # Perform a single HTTP request without any features.
97
+ def _hs_perform_no_features(req, opts)
98
+ if @session.cache_mgr.enabled?
75
99
  req.cacheable? ? _hs_cache_lookup(req, opts) : _hs_cache_pass(req, opts)
76
100
  else
77
101
  _hs_forward(req, opts)
@@ -86,10 +110,10 @@ class HTTP::Session
86
110
  # entry with the backend using conditional GET.
87
111
  # * When no matching cache entry is found, trigger miss processing.
88
112
  def _hs_cache_lookup(req, opts)
89
- entry = @session.cache.read(req)
113
+ entry = @session.cache_mgr.read(req)
90
114
  if entry.nil?
91
115
  _hs_cache_fetch(req, opts)
92
- elsif entry.response.fresh?(shared: @session.cache.shared?) &&
116
+ elsif entry.response.fresh?(shared: @session.cache_mgr.shared_cache?) &&
93
117
  !entry.response.no_cache? &&
94
118
  !req.no_cache?
95
119
  _hs_cache_reuse(req, opts, entry)
@@ -144,14 +168,16 @@ class HTTP::Session
144
168
 
145
169
  # Store the response to cache.
146
170
  def _hs_cache_entry_store(req, res)
147
- if res.cacheable?(shared: @session.cache.shared?, req: req)
148
- @session.cache.write(req, res)
171
+ if res.cacheable?(shared: @session.cache_mgr.shared_cache?, req: req)
172
+ @session.cache_mgr.write(req, res)
149
173
  end
150
174
  end
151
175
 
152
176
  # Delegate the request to the backend and create the response.
153
177
  def _hs_forward(req, opts)
154
- httprb_perform(req, opts)
178
+ res = httprb_perform(req, opts)
179
+ res.flush if default_options.persistent?
180
+ res
155
181
  end
156
182
  end
157
183
  end
@@ -135,8 +135,7 @@ class HTTP::Session
135
135
  private
136
136
 
137
137
  # :nodoc:
138
- def branch(default_options)
139
- raise FrozenError, "can't modify frozen #{self.class.name}" if frozen?
138
+ def branch(_default_options)
140
139
  self
141
140
  end
142
141
  end