Skip to content

Commit f7648a9

Browse files
committed
Remove expand! by raising errors during expand
GitHub: #1 Rather than having separate expand and expand! methods (one swallowing all exceptions, one not), simplify the API of Embiggen::URI to a single expand method. This no longer aggressively swallows exceptions outside of its control but may raise subclasses of Embiggen::Error, namely: * Embiggen::BadShortenedURI; * Embiggen::TooManyRedirects; * Embiggen::NetworkError. The last of these wraps any network-related exceptions including timeouts, connection resets, unreachable hosts, etc. The reason for this change is that these truly are exceptional circumstances outside of the library's control (unlike, say, an unshortened URI being passed to Embiggen::URI which can be handled gracefully). The previous API took too much responsibility away from the client by silently discarding exceptions by default and therefore obscuring failures. Originally, the idea of using a [sum type][0] was appealing (e.g. returning some sort of result object which could be interrogated for its success) but this later seemed overengineered and unidiomatic. Instead, we prefer to return standard Ruby types where appropriate (e.g. standard library URIs) and raise exceptions in case of failure. This simplification lead to a refactoring that extracted an HttpClient class which is currently used privately within the library. This potentially opens the way for pluggable HTTP clients (rather than being forced to use Net::HTTP) in future (c.f. #9). [0]: http://en.wikipedia.org/wiki/Tagged_union
1 parent e3632f5 commit f7648a9

File tree

5 files changed

+158
-154
lines changed

5 files changed

+158
-154
lines changed

README.md

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ uri.shortened?
4444
uri.expand
4545
#=> #<URI:HTTP http://www.altmetric.com>
4646

47-
# Noisier expand! for explicit error handling
48-
Embiggen::URI('http://bit.ly/bad').expand!
47+
# Raises errors with bad shortened URIs
48+
Embiggen::URI('http://bit.ly/bad').expand
4949
#=> TooManyRedirects: http://bit.ly/bad redirected too many times
5050
# or...
5151
#=> BadShortenedURI: following http://bit.ly/bad did not redirect
@@ -111,9 +111,18 @@ Embiggen::URI('https://youtu.be/dQw4w9WgXcQ').expand(:redirects => 2)
111111
```
112112

113113
Expand the given URI, returning the full version as a [`URI`][URI] if it is
114-
shortened or the original if it cannot be expanded. Will not raise any
115-
exceptions thrown during expansion (e.g. timeouts, network errors, invalid
116-
return URIs); see `expand!` for an alternative.
114+
shortened or the original if it is not. Can raise the following exceptions
115+
during expansion:
116+
117+
* `Embiggen::TooManyRedirects`: when a URI redirects more than the configured
118+
number of times;
119+
* `Embiggen::BadShortenedURI`: when a URI appears to be shortened but
120+
following it does not result in a redirect;
121+
* `Embiggen::NetworkError`: when an error occurs during expansion (e.g. a
122+
network timeout, connection reset, unreachable host, etc.).
123+
124+
All of the above inherit from `Embiggen::Error` and have a `uri` method for
125+
determining the problematic URI.
117126

118127
Takes an optional hash of options for expansion:
119128

@@ -124,30 +133,6 @@ Uses a whitelist of shortening domains (as configured through
124133
`Embiggen.configure`) to determine whether a URI is shortened or not. Be sure
125134
to [configure this to suit your needs](#shorteners).
126135

127-
### `Embiggen::URI#expand!`
128-
129-
```ruby
130-
Embiggen::URI('https://youtu.be/dQw4w9WgXcQ').expand!
131-
#=> #<URI:HTTPS https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be>
132-
133-
Embiggen::URI('http://bit.ly/some-bad-link').expand!
134-
# TooManyRedirects: http://bit.ly/some-bad-link redirected too many times
135-
```
136-
137-
Expand the given URI as with `Embiggen::URI#expand` but don't suppress any
138-
exceptions raised during expansion (including timeouts, network errors,
139-
invalid return URIs, too many redirects or no redirects whatsoever).
140-
141-
Takes the same options as `Embiggen::URI#expand`.
142-
143-
Two Embiggen-specific errors (both inheriting from `Embiggen::Error`) can be
144-
raised:
145-
146-
* `Embiggen::TooManyRedirects`: when a URI redirects more than the configured
147-
number of times;
148-
* `Embiggen::BadShortenedURI`: when a URI appears to be shortened but
149-
following it does not result in a redirect.
150-
151136
### `Embiggen::URI#shortened?`
152137

153138
```ruby

lib/embiggen/error.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Embiggen
2+
class Error < ::StandardError
3+
attr_reader :uri
4+
5+
def initialize(message, uri)
6+
super(message)
7+
@uri = uri
8+
end
9+
end
10+
11+
class BadShortenedURI < Error; end
12+
class NetworkError < Error; end
13+
class TooManyRedirects < Error; end
14+
end

lib/embiggen/http_client.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require 'embiggen/error'
2+
require 'net/http'
3+
4+
module Embiggen
5+
class HttpClient
6+
attr_reader :uri, :http
7+
8+
def initialize(uri)
9+
@uri = uri
10+
@http = ::Net::HTTP.new(uri.host, uri.port)
11+
@http.use_ssl = true if uri.scheme == 'https'
12+
end
13+
14+
def follow(timeout)
15+
response = request(timeout)
16+
return unless response.is_a?(::Net::HTTPRedirection)
17+
18+
response.fetch('Location')
19+
rescue StandardError, ::Timeout::Error => e
20+
raise NetworkError.new(
21+
"could not follow #{uri}: #{e.message}", uri)
22+
end
23+
24+
private
25+
26+
def request(timeout)
27+
http.open_timeout = timeout
28+
http.read_timeout = timeout
29+
30+
http.head(uri.request_uri)
31+
end
32+
end
33+
end

lib/embiggen/uri.rb

Lines changed: 19 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,26 @@
11
require 'embiggen/configuration'
2+
require 'embiggen/error'
3+
require 'embiggen/http_client'
24
require 'addressable/uri'
3-
require 'net/http'
5+
require 'uri'
46

57
module Embiggen
68
class URI
7-
attr_reader :uri
9+
attr_reader :uri, :http_client
810

911
def initialize(uri)
1012
@uri = URI(::Addressable::URI.parse(uri).normalize.to_s)
13+
@http_client = HttpClient.new(@uri)
1114
end
1215

1316
def expand(request_options = {})
14-
expand!(request_options)
15-
rescue TooManyRedirects => error
16-
error.uri
17-
rescue StandardError, ::Timeout::Error
18-
uri
19-
end
20-
21-
def expand!(request_options = {})
2217
return uri unless shortened?
2318

24-
redirects = request_options.fetch(:redirects) { Configuration.redirects }
25-
check_redirects(redirects)
19+
redirects = extract_redirects(request_options)
20+
location = follow(request_options)
2621

27-
location = head_location(request_options)
28-
check_location(location)
29-
30-
URI.new(location).
31-
expand!(request_options.merge(:redirects => redirects - 1))
22+
self.class.new(location).
23+
expand(request_options.merge(:redirects => redirects - 1))
3224
end
3325

3426
def shortened?
@@ -37,46 +29,22 @@ def shortened?
3729

3830
private
3931

40-
def check_redirects(redirects)
41-
return unless redirects.zero?
42-
43-
fail TooManyRedirects.new("#{uri} redirected too many times", uri)
44-
end
45-
46-
def check_location(location)
47-
return if location
32+
def extract_redirects(request_options = {})
33+
redirects = request_options.fetch(:redirects) { Configuration.redirects }
34+
fail TooManyRedirects.new(
35+
"#{uri} redirected too many times", uri) if redirects.zero?
4836

49-
fail BadShortenedURI, "following #{uri} did not redirect"
37+
redirects
5038
end
5139

52-
def head_location(request_options = {})
40+
def follow(request_options = {})
5341
timeout = request_options.fetch(:timeout) { Configuration.timeout }
5442

55-
http.open_timeout = timeout
56-
http.read_timeout = timeout
57-
58-
response = http.head(uri.request_uri)
59-
60-
response.fetch('Location') if response.is_a?(::Net::HTTPRedirection)
61-
end
62-
63-
def http
64-
http = ::Net::HTTP.new(uri.host, uri.port)
65-
http.use_ssl = true if uri.scheme == 'https'
66-
67-
http
68-
end
69-
end
70-
71-
class Error < ::StandardError; end
72-
class BadShortenedURI < Error; end
73-
74-
class TooManyRedirects < Error
75-
attr_reader :uri
43+
location = http_client.follow(timeout)
44+
fail BadShortenedURI.new(
45+
"following #{uri} did not redirect", uri) unless location
7646

77-
def initialize(message, uri)
78-
super(message)
79-
@uri = uri
47+
location
8048
end
8149
end
8250
end

0 commit comments

Comments
 (0)