final_redirect_url 2.0.0 → 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: 522f5bf5f63725bfacfd9615a3b37ae003e42712a9663fd5f7af11b8ce2c2f52
4
- data.tar.gz: b25c7ff42bade483244d24c1642da9097cd930326f6ebbee4f1e1aba5e854699
3
+ metadata.gz: 7c9fa9551dd7959832dfebc8b24e0cf5c5b326100385a2bf287dab1e50b98581
4
+ data.tar.gz: 2ebc2f9667d598f693fbf766c9eb55991477281cc8fff327ef8d2b693570e0f6
5
5
  SHA512:
6
- metadata.gz: bafed8a367867eee62492c7f101682595a7a0c37736e3d71e7e79b29f0716c962b5e00af2ef3c5338148fb7e2e533a003a8e20b42dc852dbd0fa16d866b30514
7
- data.tar.gz: e3dd7b15536e3e4bdcccfed12b227042707c6134f64f32d8045f86dd9c015ed1d5dfd05e119673416a1bd97efd0d4d79a7863e929adb893685fef2e1d429982e
6
+ metadata.gz: 54a6f3c70af375bf6743eb59f2c4441e4e0d072db4cd654ca94001daa0994638c896dcb2940a36f969004fd42e2f0675c2a278c64535ea6225e33e9a529ac237
7
+ data.tar.gz: 87c4b58fd877c878b109ad2f5ade9f802710e038cab50e9cf1c47ab89ef420c18cbe49570044313a26831f293176fb228df98a7fa78052c68da534f91b666b9e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,55 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [2.1.0] – 2026-11-05
11
+
12
+ ### Fork Contributions Integrated
13
+ Analysed all six public forks. Two contained work worth considering:
14
+
15
+ - **counterfind/final_redirect_url** (9 commits ahead): introduced Nokogiri meta-refresh parsing,
16
+ per-request timeouts, and HTTPS via `use_ssl`. The meta-refresh concept was adopted and
17
+ rewritten from scratch (removed `pry`, removed `KNOWN_SHORTENERS`, eliminated the
18
+ thread-unsafe module-level `@final_url`, fixed the deprecated `URI.regexp` call, replaced
19
+ `rescue Exception`, and switched `uri.host` → `uri.to_s` to preserve ports). Architecture
20
+ unchanged.
21
+ - **HowardSnowden/final_redirect_url** (4 commits ahead): extracted `get_response` as a named
22
+ method and added explicit SSL cipher configuration. The pattern was incorporated into the
23
+ new `fetch_response` method; the hardcoded cipher list was not adopted (unnecessary for
24
+ modern Ruby/OpenSSL).
25
+ - **pilot34, mbv, ymow**: trivial (limit logging, version bump, no changes) — discarded.
26
+
27
+ ### Added
28
+ - **`FinalRedirectUrl.trace(url, options)`** — returns `Array<Hop>` documenting every URL and
29
+ HTTP status code visited in the redirect chain. Useful for debugging and link audits.
30
+ - **`FinalRedirectUrl::Hop`** struct with `url` (String) and `status` (Integer, nil when depth
31
+ exhausted) attributes.
32
+ - **Custom headers** (`options[:headers]`) — pass any HTTP headers (e.g. `User-Agent`, `Accept`,
33
+ `Authorization`) to bypass anti-bot blocks or authenticate requests.
34
+ - **Configurable timeouts** — `options[:open_timeout]` and `options[:read_timeout]` default to
35
+ **60 seconds** (up from the previous 10 s hard-coded value).
36
+ - **`verify_ssl` option** (`true` by default) — set `verify_ssl: false` to bypass strict SSL
37
+ certificate validation (e.g. for self-signed certificates in development).
38
+ - **Stateful cookie handling** — `Set-Cookie` headers from every 3xx response are captured and
39
+ injected as `Cookie` headers into subsequent requests, enabling cookie-gated redirect chains
40
+ to be followed correctly.
41
+ - **Meta-refresh parsing** (via Nokogiri, adapted from the `counterfind` fork) — when a 200 OK
42
+ response contains `<meta http-equiv="refresh">`, the gem follows the redirect URL. Handles
43
+ case-insensitive `HTTP-EQUIV`, quoted URLs, and malformed `content` attributes. Only fires
44
+ for `text/html` responses.
45
+ - **nokogiri** added as a runtime dependency.
46
+
47
+ ### Changed
48
+ - Switched from `HEAD` to `GET` requests. `GET` is required so that the response body is
49
+ available for meta-refresh parsing; it also improves compatibility with servers that reject
50
+ `HEAD` with 405.
51
+ - `final_redirect_url` now delegates to `trace` internally, eliminating duplicated logic.
52
+ - Default timeouts raised from 10 s to **60 s**.
53
+
54
+ ### No Open Issues
55
+ All issues were resolved in v2.0.0. No new open issues exist at the time of this release.
56
+
57
+ ---
58
+
10
59
  ## [2.0.0] – 2026-11-05
11
60
 
12
61
  ### Breaking Changes
@@ -54,6 +103,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
54
103
  headless browser and are outside the scope of this gem. The gem returns the last HTTP-level
55
104
  reachable URL in such cases.
56
105
 
106
+ [2.1.0]: https://github.com/indyarocks/final_redirect_url/compare/v2.0.0...v2.1.0
57
107
  [2.0.0]: https://github.com/indyarocks/final_redirect_url/compare/v0.1.1...v2.0.0
58
108
  [i2]: https://github.com/indyarocks/final_redirect_url/issues/2
59
109
  [i3]: https://github.com/indyarocks/final_redirect_url/issues/3
data/README.md CHANGED
@@ -3,69 +3,128 @@
3
3
  [![CI](https://github.com/indyarocks/final_redirect_url/actions/workflows/ci.yml/badge.svg)](https://github.com/indyarocks/final_redirect_url/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/final_redirect_url.svg)](https://badge.fury.io/rb/final_redirect_url)
5
5
 
6
- Ruby gem that follows HTTP/HTTPS redirects and returns the final destination URL.
6
+ Ruby gem that follows HTTP/HTTPS redirects — including `<meta http-equiv="refresh">` and
7
+ cookie-gated chains — and returns the final destination URL.
7
8
 
8
9
  ## Requirements
9
10
 
10
11
  - Ruby >= 3.0
12
+ - [Nokogiri](https://nokogiri.org) (bundled as a runtime dependency for meta-refresh parsing)
11
13
 
12
14
  ## Installation
13
15
 
14
- Add this line to your application's Gemfile:
15
-
16
16
  ```ruby
17
17
  gem 'final_redirect_url'
18
18
  ```
19
19
 
20
- Then run:
21
-
22
20
  ```
23
21
  $ bundle install
24
22
  ```
25
23
 
26
- Or install directly:
24
+ or
27
25
 
28
26
  ```
29
27
  $ gem install final_redirect_url
30
28
  ```
31
29
 
32
- ## Usage
33
-
34
- The input URL **must** include the `http://` or `https://` scheme.
30
+ ## Basic usage
35
31
 
36
32
  ```ruby
37
33
  require 'final_redirect_url'
38
34
 
39
- # Simple redirect following
40
35
  FinalRedirectUrl.final_redirect_url('http://google.com')
41
36
  # => "https://www.google.com/"
42
37
 
43
- # Ports are preserved
44
- FinalRedirectUrl.final_redirect_url('http://localhost:3000/items/42')
45
- # => "http://localhost:3000/items/42"
46
-
47
- # Custom redirect depth (default: 10)
48
- FinalRedirectUrl.final_redirect_url('http://example.com', depth: 5)
49
- # => "http://final.example.com/"
50
-
51
38
  # Invalid / missing protocol → empty string
52
39
  FinalRedirectUrl.final_redirect_url('www.example.com')
53
40
  # => ""
54
41
  ```
55
42
 
43
+ ## Options
44
+
45
+ All options are passed as a second argument hash:
46
+
47
+ | Option | Type | Default | Description |
48
+ |----------------|---------|---------|-------------|
49
+ | `:depth` | Integer | `10` | Maximum redirect hops to follow |
50
+ | `:open_timeout` | Integer | `60` | TCP connect timeout in seconds |
51
+ | `:read_timeout` | Integer | `60` | Read timeout in seconds |
52
+ | `:verify_ssl` | Boolean | `true` | Enforce SSL certificate validation |
53
+ | `:headers` | Hash | `{}` | Extra HTTP headers sent with every request |
54
+
55
+ ```ruby
56
+ # Limit redirect depth
57
+ FinalRedirectUrl.final_redirect_url('http://example.com', depth: 5)
58
+
59
+ # Custom timeouts (seconds)
60
+ FinalRedirectUrl.final_redirect_url('http://slow.example.com', open_timeout: 5, read_timeout: 30)
61
+
62
+ # Bypass SSL certificate verification (e.g. self-signed certs in development)
63
+ FinalRedirectUrl.final_redirect_url('https://self-signed.local', verify_ssl: false)
64
+
65
+ # Custom headers — override User-Agent or add Authorization / Accept
66
+ FinalRedirectUrl.final_redirect_url(
67
+ 'http://bot-protected.example.com',
68
+ headers: {
69
+ 'User-Agent' => 'Mozilla/5.0 (compatible; MyApp/1.0)',
70
+ 'Accept' => 'text/html'
71
+ }
72
+ )
73
+ ```
74
+
75
+ ## Redirect trace
76
+
77
+ `FinalRedirectUrl.trace` returns every hop in the chain as an array of `Hop` structs.
78
+ Each `Hop` has a `url` (String) and a `status` (Integer, or `nil` when the depth limit is
79
+ reached before a request is made).
80
+
81
+ ```ruby
82
+ hops = FinalRedirectUrl.trace('http://hop1.example.com')
83
+
84
+ hops.each { |hop| puts "#{hop.status} #{hop.url}" }
85
+ # 301 http://hop1.example.com/
86
+ # 302 http://hop2.example.com/
87
+ # 200 https://final.example.com/
88
+
89
+ hops.last.url # => "https://final.example.com/"
90
+ hops.last.status # => 200
91
+ ```
92
+
93
+ `trace` accepts the same options as `final_redirect_url`.
94
+
95
+ ## Meta-refresh support
96
+
97
+ When the gem reaches a `200 OK` response whose body contains a
98
+ `<meta http-equiv="refresh" content="N; url=...">` tag, it follows that URL as if it were an
99
+ HTTP redirect. Case-insensitive attribute matching and quoted URL values are both handled.
100
+
101
+ ```ruby
102
+ # Page at http://meta.example.com/ contains:
103
+ # <meta http-equiv="refresh" content="0; url=http://final.example.com/">
104
+ FinalRedirectUrl.final_redirect_url('http://meta.example.com/')
105
+ # => "http://final.example.com/"
106
+ ```
107
+
108
+ ## Cookie-gated redirects
109
+
110
+ `Set-Cookie` headers from every `3xx` response are captured and re-sent as `Cookie` headers on
111
+ all subsequent requests in the same chain, allowing the gem to follow redirects that require an
112
+ active session cookie.
113
+
56
114
  ## Limitations
57
115
 
58
- - **JavaScript / AMP redirects** (e.g. some Google News AMP URLs) are not followed. These require a
59
- headless browser. The gem returns the last HTTP-reachable URL in such cases.
60
- - Redirects are followed using HTTP `HEAD` requests. Servers that reject `HEAD` with `405 Method Not Allowed`
61
- may not resolve correctly.
116
+ - **JavaScript / AMP redirects** (e.g. some Google News AMP URLs) require a headless browser
117
+ and are outside the scope of this gem. The gem returns the last HTTP-reachable URL in such
118
+ cases.
119
+ - Servers that reject `HEAD` requests with `405 Method Not Allowed` are handled correctly
120
+ because the gem uses `GET` for all requests.
62
121
 
63
122
  ## Contributing
64
123
 
65
- Bug reports and pull requests are welcome on GitHub at <https://github.com/indyarocks/final_redirect_url>.
66
- This project is intended to be a safe, welcoming space for collaboration, and contributors are expected
67
- to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
124
+ Bug reports and pull requests are welcome on GitHub at
125
+ <https://github.com/indyarocks/final_redirect_url>. Contributors are expected to adhere to the
126
+ [Contributor Covenant](http://contributor-covenant.org) code of conduct.
68
127
 
69
128
  ## License
70
129
 
71
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
130
+ Available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -9,8 +9,9 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ['chandan.jhun@gmail.com']
10
10
 
11
11
  spec.summary = 'Get the final redirect URL for any given URL.'
12
- spec.description = 'Follows HTTP redirects (including HTTPS, relative paths, and ' \
13
- 'non-standard ports) to resolve the final destination URL.'
12
+ spec.description = 'Follows HTTP/HTTPS redirects (including meta-refresh, cookie-gated ' \
13
+ 'redirects, relative paths, and non-standard ports) to resolve the ' \
14
+ 'final destination URL. Provides a trace method for full hop inspection.'
14
15
  spec.homepage = 'https://github.com/indyarocks/final_redirect_url'
15
16
  spec.license = 'MIT'
16
17
 
@@ -27,6 +28,8 @@ Gem::Specification.new do |spec|
27
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
29
  spec.require_paths = ['lib']
29
30
 
31
+ spec.add_dependency 'nokogiri', '~> 1.0'
32
+
30
33
  spec.add_development_dependency 'bundler', '~> 2.0'
31
34
  spec.add_development_dependency 'rake', '~> 13.0'
32
35
  spec.add_development_dependency 'rspec', '~> 3.0'
@@ -1,3 +1,3 @@
1
1
  module FinalRedirectUrl
2
- VERSION = '2.0.0'
2
+ VERSION = '2.1.0'
3
3
  end
@@ -3,88 +3,164 @@ require 'net/http'
3
3
  require 'net/https'
4
4
  require 'uri'
5
5
  require 'logger'
6
+ require 'nokogiri'
6
7
 
7
8
  module FinalRedirectUrl
8
- DEFAULT_DEPTH = 10
9
+ DEFAULT_DEPTH = 10
10
+ DEFAULT_OPEN_TIMEOUT = 60
11
+ DEFAULT_READ_TIMEOUT = 60
9
12
  USER_AGENT = "FinalRedirectUrl/#{VERSION} (+https://github.com/indyarocks/final_redirect_url)".freeze
10
13
 
14
+ # Value object representing one step in a redirect chain.
15
+ # hop.url # => "http://example.com/" (String)
16
+ # hop.status # => 301 (Integer, or nil when depth limit reached)
17
+ Hop = Struct.new(:url, :status, keyword_init: true)
18
+
11
19
  class << self
12
20
  # Resolves all HTTP/HTTPS redirects for the given URL and returns the final
13
- # destination as a string. Returns an empty string for invalid input or on
14
- # unrecoverable network errors.
21
+ # destination as a String. Returns "" for invalid input or unrecoverable errors.
15
22
  #
16
- # @param url [String] the starting URL (must include http:// or https://)
23
+ # @param url [String] starting URL (must include http:// or https://)
17
24
  # @param options [Hash]
18
- # @option options [Integer] :depth maximum redirect hops (default: 10)
25
+ # @option options [Integer] :depth max redirect hops (default: 10)
26
+ # @option options [Integer] :open_timeout TCP connect timeout in sec (default: 60)
27
+ # @option options [Integer] :read_timeout read timeout in sec (default: 60)
28
+ # @option options [Boolean] :verify_ssl enforce SSL cert validation (default: true)
29
+ # @option options [Hash] :headers extra HTTP request headers
19
30
  # @return [String]
20
31
  def final_redirect_url(url, options = {})
21
- return '' unless valid_url?(url)
32
+ hops = trace(url, options)
33
+ hops.empty? ? '' : hops.last.url
34
+ end
35
+
36
+ # Follows all redirects and returns every hop as an Array of Hop structs.
37
+ # Useful for debugging, link audits, or building redirect-aware tooling.
38
+ #
39
+ # @param url [String]
40
+ # @param options [Hash] same keys as #final_redirect_url
41
+ # @return [Array<Hop>]
42
+ def trace(url, options = {})
43
+ return [] unless valid_url?(url)
22
44
 
23
- limit = options.fetch(:depth, DEFAULT_DEPTH).to_i
24
- limit = DEFAULT_DEPTH unless limit.positive?
45
+ limit = positive_int(options[:depth], DEFAULT_DEPTH)
46
+ hops = []
47
+ cookies = {}
25
48
 
26
- resolve_redirects(url, limit).to_s
49
+ resolve(url, limit, options, hops, cookies)
50
+ hops
27
51
  rescue StandardError => ex
28
52
  Logger.new($stdout).error(ex.message)
29
- ''
53
+ hops
30
54
  end
31
55
 
32
56
  private
33
57
 
34
- # Returns true only for syntactically valid HTTP/HTTPS URLs.
35
- # Fixes issue #4: private via class << self so the method is genuinely
36
- # inaccessible from outside the module.
37
- def valid_url?(url)
38
- uri = URI.parse(url.to_s)
39
- uri.is_a?(URI::HTTP)
40
- rescue URI::InvalidURIError
41
- false
42
- end
43
-
44
- # Recursively follows redirects up to +limit+ hops.
45
- # Fixes issue #2: when the hop limit is reached, returns the current URI
46
- # (a proper URI object, not a bare string) so url_string_from_uri never
47
- # receives a plain String.
48
- # Fixes issue #6: uses URI.join to build absolute redirect targets, which
49
- # preserves non-standard ports from the base URI (e.g. localhost:3000).
50
- def resolve_redirects(url, limit)
58
+ def resolve(url, limit, options, hops, cookies)
51
59
  uri = URI.parse(url)
52
- return uri if limit <= 0
53
60
 
54
- response = fetch_response(uri)
61
+ if limit <= 0
62
+ # Depth exhausted — record the pending URL without making a request.
63
+ hops << Hop.new(url: uri.to_s, status: nil)
64
+ return
65
+ end
66
+
67
+ response = fetch_response(uri, options, cookies)
68
+ collect_cookies(response, cookies)
55
69
 
56
70
  case response
57
71
  when Net::HTTPOK
58
- uri
72
+ if (meta_url = meta_refresh_url(response, uri))
73
+ hops << Hop.new(url: uri.to_s, status: response.code.to_i)
74
+ warn "meta-refresh to #{meta_url}"
75
+ resolve(meta_url, limit - 1, options, hops, cookies)
76
+ else
77
+ hops << Hop.new(url: uri.to_s, status: response.code.to_i)
78
+ end
59
79
  when Net::HTTPRedirection
60
- location = response['location']
80
+ location = absolute_url(uri, response['location'])
81
+ hops << Hop.new(url: uri.to_s, status: response.code.to_i)
61
82
  warn "redirected to #{location}"
62
- resolve_redirects(absolute_url(uri, location), limit - 1)
83
+ resolve(location, limit - 1, options, hops, cookies)
63
84
  else
64
- # Non-redirect, non-200 response (e.g. 404, 403, 500): return the
65
- # current URI rather than trying to follow a missing Location header.
66
- uri
85
+ # Non-200, non-redirect (403, 404, 500 …): record and stop.
86
+ hops << Hop.new(url: uri.to_s, status: response.code.to_i)
67
87
  end
68
88
  end
69
89
 
70
- # Opens an HTTP/HTTPS connection and fires a HEAD request.
71
- # HEAD is sufficient for redirect resolution and avoids downloading bodies.
72
- def fetch_response(uri)
73
- http = Net::HTTP.new(uri.host, uri.port)
90
+ def fetch_response(uri, options, cookies)
91
+ http = Net::HTTP.new(uri.host, uri.port)
74
92
  http.use_ssl = (uri.scheme == 'https')
75
- http.open_timeout = 10
76
- http.read_timeout = 10
93
+ http.verify_mode = options.fetch(:verify_ssl, true) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
94
+ http.open_timeout = positive_int(options[:open_timeout], DEFAULT_OPEN_TIMEOUT)
95
+ http.read_timeout = positive_int(options[:read_timeout], DEFAULT_READ_TIMEOUT)
77
96
 
78
- request = Net::HTTP::Head.new(uri.request_uri)
97
+ # GET is required so we can read the body for meta-refresh detection.
98
+ request = Net::HTTP::Get.new(uri.request_uri)
79
99
  request['User-Agent'] = USER_AGENT
80
100
 
101
+ # Caller-supplied headers override defaults (including User-Agent).
102
+ (options[:headers] || {}).each { |k, v| request[k.to_s] = v }
103
+
104
+ # Inject accumulated cookies for cookie-gated redirect chains.
105
+ request['Cookie'] = cookies.map { |k, v| "#{k}=#{v}" }.join('; ') unless cookies.empty?
106
+
81
107
  http.request(request)
82
108
  end
83
109
 
84
- # Converts a potentially relative +location+ into an absolute URL,
85
- # keeping the scheme, host, and port of +base_uri+.
110
+ # Harvests Set-Cookie values from a response into the shared cookie jar.
111
+ def collect_cookies(response, cookies)
112
+ Array(response.get_fields('set-cookie')).each do |cookie_str|
113
+ name, value = cookie_str.split(';', 2).first.to_s.split('=', 2)
114
+ cookies[name.to_s.strip] = value.to_s.strip
115
+ end
116
+ end
117
+
118
+ # Parses <meta http-equiv="refresh"> from an HTML body and returns the
119
+ # resolved absolute URL, or nil when no meta-refresh is present.
120
+ # Inspired by the counterfind fork; rewritten for correctness and safety.
121
+ def meta_refresh_url(response, base_uri)
122
+ content_type = response['content-type'].to_s
123
+ return nil unless content_type.empty? || content_type.include?('html')
124
+ return nil if response.body.to_s.empty?
125
+
126
+ doc = Nokogiri::HTML.parse(response.body)
127
+ meta = doc.at_xpath(
128
+ "//meta[translate(@http-equiv," \
129
+ "'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='refresh']/@content"
130
+ )
131
+ return nil unless meta
132
+
133
+ # Content examples:
134
+ # "0; url=https://final.example.com/"
135
+ # "5; URL='https://final.example.com/'"
136
+ # "0; https://final.example.com/" (some malformed pages omit "url=")
137
+ raw = meta.value.to_s
138
+ url_part = raw.split(/;\s*/i, 2).last.to_s
139
+ .sub(/\Aurl\s*=\s*/i, '')
140
+ .gsub(/['"]/, '')
141
+ .strip
142
+ return nil if url_part.empty?
143
+
144
+ absolute_url(base_uri, url_part)
145
+ rescue StandardError
146
+ nil
147
+ end
148
+
149
+ def valid_url?(url)
150
+ uri = URI.parse(url.to_s)
151
+ uri.is_a?(URI::HTTP)
152
+ rescue URI::InvalidURIError
153
+ false
154
+ end
155
+
86
156
  def absolute_url(base_uri, location)
87
157
  URI.join(base_uri.to_s, location).to_s
88
158
  end
159
+
160
+ def positive_int(value, default)
161
+ return default if value.nil?
162
+ v = value.to_i
163
+ v.positive? ? v : default
164
+ end
89
165
  end
90
166
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: final_redirect_url
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chandan Jhunjhunwal
@@ -10,6 +10,20 @@ bindir: exe
10
10
  cert_chain: []
11
11
  date: 2026-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -66,8 +80,9 @@ dependencies:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
82
  version: '3.0'
69
- description: Follows HTTP redirects (including HTTPS, relative paths, and non-standard
70
- ports) to resolve the final destination URL.
83
+ description: Follows HTTP/HTTPS redirects (including meta-refresh, cookie-gated redirects,
84
+ relative paths, and non-standard ports) to resolve the final destination URL. Provides
85
+ a trace method for full hop inspection.
71
86
  email:
72
87
  - chandan.jhun@gmail.com
73
88
  executables: []