final_redirect_url 0.1.0 → 2.0.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
- SHA1:
3
- metadata.gz: f25dc104a0c41353b83a49d45abd4146fe306b6d
4
- data.tar.gz: 8ec79086342e96fec99c9ff81e2b71122021e95e
2
+ SHA256:
3
+ metadata.gz: 522f5bf5f63725bfacfd9615a3b37ae003e42712a9663fd5f7af11b8ce2c2f52
4
+ data.tar.gz: b25c7ff42bade483244d24c1642da9097cd930326f6ebbee4f1e1aba5e854699
5
5
  SHA512:
6
- metadata.gz: 91de374d15554f862bf75ca659e1f4f69d1425d3df59f95f382d19150d896308ed4462e95278412a54eca21bdb8aba25160f6456f4cdd3d531817b11776d142f
7
- data.tar.gz: 73b1cc9a2031fdb1a744957f13ce4707a46e8a26b3307341ad4fa01730cb786f1a44ea8c848e05e0873151f61294b4af95c3c08fac9c48859e433866a201ad6c
6
+ metadata.gz: bafed8a367867eee62492c7f101682595a7a0c37736e3d71e7e79b29f0716c962b5e00af2ef3c5338148fb7e2e533a003a8e20b42dc852dbd0fa16d866b30514
7
+ data.tar.gz: e3dd7b15536e3e4bdcccfed12b227042707c6134f64f32d8045f86dd9c015ed1d5dfd05e119673416a1bd97efd0d4d79a7863e929adb893685fef2e1d429982e
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main, "feature/**"]
6
+ pull_request:
7
+ branches: [master, main]
8
+
9
+ jobs:
10
+ test:
11
+ name: Ruby ${{ matrix.ruby }}
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby:
17
+ - "3.0"
18
+ - "3.1"
19
+ - "3.2"
20
+ - "3.3"
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Set up Ruby ${{ matrix.ruby }}
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: ${{ matrix.ruby }}
29
+ bundler-cache: true
30
+
31
+ - name: Run tests
32
+ run: bundle exec rspec
data/.gitignore CHANGED
@@ -7,6 +7,7 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /vendor/bundle
10
11
 
11
12
  # rspec failure tracking
12
13
  .rspec_status
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project
6
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [2.0.0] – 2026-11-05
11
+
12
+ ### Breaking Changes
13
+ - **Minimum Ruby version raised to 3.0.** Ruby 2.x is no longer supported.
14
+ - Private helper methods (`valid_url?`, `resolve_redirects`, `fetch_response`, `absolute_url`) are now
15
+ truly private (defined inside `class << self` with `private`). Calling them from outside the module
16
+ now raises `NoMethodError`. This was always the intended behaviour; previous versions accidentally
17
+ exposed them as public singleton methods (fixes [#4][i4]).
18
+
19
+ ### Added
20
+ - **GitHub Actions CI** replacing the removed Travis CI configuration. The matrix tests Ruby 3.0,
21
+ 3.1, 3.2, and 3.3 on every push and pull request.
22
+ - **WebMock** test dependency — the full test suite now runs without any real network connections.
23
+ - `CHANGELOG.md` (this file).
24
+ - Gem metadata fields (`homepage_uri`, `source_code_uri`, `changelog_uri`).
25
+
26
+ ### Fixed
27
+ - **Port numbers are now preserved** in final URLs. For example,
28
+ `final_redirect_url("http://localhost:3000/path")` now correctly returns
29
+ `"http://localhost:3000/path"` instead of `"http://localhost/path"` (fixes [#6][i6]).
30
+ - **Redirect depth limit now works correctly.** When the hop limit was reached the gem previously
31
+ returned a bare `String` to `url_string_from_uri`, causing a `NoMethodError`. The limit guard now
32
+ returns the current `URI` object (fixes [#2][i2]).
33
+ - **Relative-path redirects now carry forward the original host's port.** The gem uses `URI.join`
34
+ instead of manual string concatenation, so a redirect `Location: /new-path` from
35
+ `http://localhost:3000/old-path` resolves to `http://localhost:3000/new-path`.
36
+ - **Non-redirect, non-200 responses** (403, 404, 500 …) are now handled gracefully: the gem returns
37
+ the last reachable URL rather than crashing with a `TypeError` on a nil `Location` header.
38
+ - **HTTPS support** — connections now set `use_ssl = true` when the scheme is `https`. Previously
39
+ only plain-HTTP URLs worked.
40
+ - **`URI::regexp` removed** — the old `is_valid_url?` used the deprecated (and Ruby-3.x-removed)
41
+ `URI::regexp`. Replaced with `URI.parse` + `URI::HTTP` class check.
42
+ - **`rescue Exception`** replaced with `rescue StandardError` to avoid masking signals and other
43
+ non-recoverable errors.
44
+ - **`$stdout`** used instead of bare `STDOUT` constant (style/frozen-string consistency).
45
+
46
+ ### Changed
47
+ - Switched from `Net::HTTP.get_response` (GET) to explicit `Net::HTTP::Head` requests. HEAD is
48
+ sufficient for redirect resolution and avoids downloading response bodies.
49
+ - Added `open_timeout` and `read_timeout` (10 s each) to prevent indefinite hangs.
50
+ - Added a `User-Agent` header so servers that require it don't return unexpected responses.
51
+
52
+ ### Known Limitations
53
+ - JavaScript- and AMP-based redirects (e.g. certain Google News AMP URLs — [#3][i3]) require a
54
+ headless browser and are outside the scope of this gem. The gem returns the last HTTP-level
55
+ reachable URL in such cases.
56
+
57
+ [2.0.0]: https://github.com/indyarocks/final_redirect_url/compare/v0.1.1...v2.0.0
58
+ [i2]: https://github.com/indyarocks/final_redirect_url/issues/2
59
+ [i3]: https://github.com/indyarocks/final_redirect_url/issues/3
60
+ [i4]: https://github.com/indyarocks/final_redirect_url/issues/4
61
+ [i6]: https://github.com/indyarocks/final_redirect_url/issues/6
data/README.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # FinalRedirectUrl
2
2
 
3
- Ruby gem to get a final redirect url for any given url.
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
+ [![Gem Version](https://badge.fury.io/rb/final_redirect_url.svg)](https://badge.fury.io/rb/final_redirect_url)
5
+
6
+ Ruby gem that follows HTTP/HTTPS redirects and returns the final destination URL.
7
+
8
+ ## Requirements
9
+
10
+ - Ruby >= 3.0
4
11
 
5
12
  ## Installation
6
13
 
@@ -10,30 +17,55 @@ Add this line to your application's Gemfile:
10
17
  gem 'final_redirect_url'
11
18
  ```
12
19
 
13
- And then execute:
20
+ Then run:
14
21
 
15
- $ bundle
22
+ ```
23
+ $ bundle install
24
+ ```
16
25
 
17
- Or install it yourself as:
26
+ Or install directly:
18
27
 
19
- $ gem install final_redirect_url
28
+ ```
29
+ $ gem install final_redirect_url
30
+ ```
20
31
 
21
32
  ## Usage
22
33
 
23
- Usually the gem checks for redirect upto 10 depth level. However you can pass `{depth: 20}` to check redirects upto `20` depth.
34
+ The input URL **must** include the `http://` or `https://` scheme.
24
35
 
25
- ## Development
36
+ ```ruby
37
+ require 'final_redirect_url'
26
38
 
27
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
39
+ # Simple redirect following
40
+ FinalRedirectUrl.final_redirect_url('http://google.com')
41
+ # => "https://www.google.com/"
28
42
 
29
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
43
+ # Ports are preserved
44
+ FinalRedirectUrl.final_redirect_url('http://localhost:3000/items/42')
45
+ # => "http://localhost:3000/items/42"
30
46
 
31
- ## Contributing
47
+ # Custom redirect depth (default: 10)
48
+ FinalRedirectUrl.final_redirect_url('http://example.com', depth: 5)
49
+ # => "http://final.example.com/"
50
+
51
+ # Invalid / missing protocol → empty string
52
+ FinalRedirectUrl.final_redirect_url('www.example.com')
53
+ # => ""
54
+ ```
32
55
 
33
- Bug reports and pull requests are welcome on GitHub at https://github.com/indyarocks/final_redirect_url. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
56
+ ## Limitations
34
57
 
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.
62
+
63
+ ## Contributing
64
+
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.
35
68
 
36
69
  ## License
37
70
 
38
71
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
39
-
@@ -1,37 +1,34 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'final_redirect_url/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "final_redirect_url"
6
+ spec.name = 'final_redirect_url'
8
7
  spec.version = FinalRedirectUrl::VERSION
9
- spec.authors = ["Chandan Jhunjhunwal"]
10
- spec.email = ["chandan.jhun@gmail.com"]
8
+ spec.authors = ['Chandan Jhunjhunwal']
9
+ spec.email = ['chandan.jhun@gmail.com']
11
10
 
12
- spec.summary = %q{Get a final redirect url for a given url.}
13
- spec.description = %q{A URL might have multiple redirects. The objective of this gem is to
14
- extract the final redirected url.}
15
- spec.homepage = "https://github.com/indyarocks/final_redirect_url"
16
- spec.license = "MIT"
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.'
14
+ spec.homepage = 'https://github.com/indyarocks/final_redirect_url'
15
+ spec.license = 'MIT'
17
16
 
18
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
- # to allow pushing to a single host or delete this section to allow pushing to any host.
20
- # if spec.respond_to?(:metadata)
21
- # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
22
- # else
23
- # raise "RubyGems 2.0 or newer is required to protect against " \
24
- # "public gem pushes."
25
- # end
17
+ spec.required_ruby_version = '>= 3.0'
26
18
 
27
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = spec.homepage
21
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md"
22
+
23
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
28
24
  f.match(%r{^(test|spec|features)/})
29
25
  end
30
- spec.bindir = "exe"
26
+ spec.bindir = 'exe'
31
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
- spec.require_paths = ["lib"]
28
+ spec.require_paths = ['lib']
33
29
 
34
- spec.add_development_dependency "bundler", "~> 1.14"
35
- spec.add_development_dependency "rake", "~> 10.0"
36
- spec.add_development_dependency "rspec", "~> 3.0"
30
+ spec.add_development_dependency 'bundler', '~> 2.0'
31
+ spec.add_development_dependency 'rake', '~> 13.0'
32
+ spec.add_development_dependency 'rspec', '~> 3.0'
33
+ spec.add_development_dependency 'webmock', '~> 3.0'
37
34
  end
@@ -1,3 +1,3 @@
1
1
  module FinalRedirectUrl
2
- VERSION = "0.1.0"
2
+ VERSION = '2.0.0'
3
3
  end
@@ -1,50 +1,90 @@
1
1
  require 'final_redirect_url/version'
2
2
  require 'net/http'
3
+ require 'net/https'
4
+ require 'uri'
3
5
  require 'logger'
4
6
 
5
7
  module FinalRedirectUrl
8
+ DEFAULT_DEPTH = 10
9
+ USER_AGENT = "FinalRedirectUrl/#{VERSION} (+https://github.com/indyarocks/final_redirect_url)".freeze
6
10
 
7
- def self.final_redirect_url(url, options={})
8
- final_url = ''
9
- if is_valid_url?(url)
10
- begin
11
- redirect_lookup_depth = options[:depth].to_i > 0 ? options[:depth].to_i : 10
12
- response_uri = get_final_redirect_url(url, redirect_lookup_depth)
13
- final_url = url_string_from_uri(response_uri)
14
- rescue Exception => ex
15
- logger = Logger.new(STDOUT)
16
- logger.error(ex.message)
17
- end
11
+ class << self
12
+ # 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.
15
+ #
16
+ # @param url [String] the starting URL (must include http:// or https://)
17
+ # @param options [Hash]
18
+ # @option options [Integer] :depth maximum redirect hops (default: 10)
19
+ # @return [String]
20
+ def final_redirect_url(url, options = {})
21
+ return '' unless valid_url?(url)
22
+
23
+ limit = options.fetch(:depth, DEFAULT_DEPTH).to_i
24
+ limit = DEFAULT_DEPTH unless limit.positive?
25
+
26
+ resolve_redirects(url, limit).to_s
27
+ rescue StandardError => ex
28
+ Logger.new($stdout).error(ex.message)
29
+ ''
18
30
  end
19
- final_url
20
- end
21
31
 
22
- private
23
- def self.is_valid_url?(url)
24
- url.to_s =~ /\A#{URI::regexp(['http', 'https'])}\z/
25
- end
32
+ private
33
+
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
26
43
 
27
- def self.get_final_redirect_url(url, limit = 10)
28
- uri = URI.parse(url)
29
- response = ::Net::HTTP.get_response(uri)
30
- if response.class == Net::HTTPOK
31
- return uri
32
- else
33
- redirect_location = response['location']
34
- location_uri = URI.parse(redirect_location)
35
- if location_uri.host.nil?
36
- redirect_location = uri.scheme + '://' + uri.host + redirect_location
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)
51
+ uri = URI.parse(url)
52
+ return uri if limit <= 0
53
+
54
+ response = fetch_response(uri)
55
+
56
+ case response
57
+ when Net::HTTPOK
58
+ uri
59
+ when Net::HTTPRedirection
60
+ location = response['location']
61
+ warn "redirected to #{location}"
62
+ resolve_redirects(absolute_url(uri, location), limit - 1)
63
+ 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
37
67
  end
38
- warn "redirected to #{redirect_location}"
39
- get_final_redirect_url(redirect_location, limit - 1)
40
68
  end
41
- end
42
69
 
43
- def self.url_string_from_uri(uri)
44
- url_str = "#{uri.scheme}://#{uri.host}#{uri.request_uri}"
45
- if uri.fragment
46
- url_str = url_str + "##{uri.fragment}"
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)
74
+ http.use_ssl = (uri.scheme == 'https')
75
+ http.open_timeout = 10
76
+ http.read_timeout = 10
77
+
78
+ request = Net::HTTP::Head.new(uri.request_uri)
79
+ request['User-Agent'] = USER_AGENT
80
+
81
+ http.request(request)
82
+ end
83
+
84
+ # Converts a potentially relative +location+ into an absolute URL,
85
+ # keeping the scheme, host, and port of +base_uri+.
86
+ def absolute_url(base_uri, location)
87
+ URI.join(base_uri.to_s, location).to_s
47
88
  end
48
- url_str
49
89
  end
50
90
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: final_redirect_url
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chandan Jhunjhunwal
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-05-02 00:00:00.000000000 Z
11
+ date: 2026-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.14'
19
+ version: '2.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.14'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '13.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,18 +52,33 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
- description: |-
56
- A URL might have multiple redirects. The objective of this gem is to
57
- extract the final redirected url.
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description: Follows HTTP redirects (including HTTPS, relative paths, and non-standard
70
+ ports) to resolve the final destination URL.
58
71
  email:
59
72
  - chandan.jhun@gmail.com
60
73
  executables: []
61
74
  extensions: []
62
75
  extra_rdoc_files: []
63
76
  files:
77
+ - ".github/workflows/ci.yml"
64
78
  - ".gitignore"
65
79
  - ".rspec"
66
- - ".travis.yml"
80
+ - ".ruby-version"
81
+ - CHANGELOG.md
67
82
  - CODE_OF_CONDUCT.md
68
83
  - Gemfile
69
84
  - LICENSE.txt
@@ -77,8 +92,11 @@ files:
77
92
  homepage: https://github.com/indyarocks/final_redirect_url
78
93
  licenses:
79
94
  - MIT
80
- metadata: {}
81
- post_install_message:
95
+ metadata:
96
+ homepage_uri: https://github.com/indyarocks/final_redirect_url
97
+ source_code_uri: https://github.com/indyarocks/final_redirect_url
98
+ changelog_uri: https://github.com/indyarocks/final_redirect_url/blob/master/CHANGELOG.md
99
+ post_install_message:
82
100
  rdoc_options: []
83
101
  require_paths:
84
102
  - lib
@@ -86,16 +104,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
86
104
  requirements:
87
105
  - - ">="
88
106
  - !ruby/object:Gem::Version
89
- version: '0'
107
+ version: '3.0'
90
108
  required_rubygems_version: !ruby/object:Gem::Requirement
91
109
  requirements:
92
110
  - - ">="
93
111
  - !ruby/object:Gem::Version
94
112
  version: '0'
95
113
  requirements: []
96
- rubyforge_project:
97
- rubygems_version: 2.6.10
98
- signing_key:
114
+ rubygems_version: 3.5.9
115
+ signing_key:
99
116
  specification_version: 4
100
- summary: Get a final redirect url for a given url.
117
+ summary: Get the final redirect URL for any given URL.
101
118
  test_files: []
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.4.0
5
- before_install: gem install bundler -v 1.14.6