http 5.3.1 → 6.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 +4 -4
- data/CHANGELOG.md +241 -41
- data/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/UPGRADING.md +491 -0
- data/http.gemspec +32 -29
- data/lib/http/base64.rb +11 -1
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +232 -136
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +126 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +25 -1
- data/lib/http/feature.rb +65 -5
- data/lib/http/features/auto_deflate.rb +124 -17
- data/lib/http/features/auto_inflate.rb +38 -15
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +97 -17
- data/lib/http/features/logging.rb +183 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +18 -3
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +17 -36
- data/lib/http/headers.rb +172 -65
- data/lib/http/mime_type/adapter.rb +24 -9
- data/lib/http/mime_type/json.rb +19 -4
- data/lib/http/mime_type.rb +21 -3
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +172 -125
- data/lib/http/redirector.rb +80 -75
- data/lib/http/request/body.rb +87 -6
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +76 -16
- data/lib/http/request.rb +214 -98
- data/lib/http/response/body.rb +103 -18
- data/lib/http/response/inflater.rb +35 -7
- data/lib/http/response/parser.rb +98 -4
- data/lib/http/response/status/reasons.rb +2 -4
- data/lib/http/response/status.rb +141 -31
- data/lib/http/response.rb +219 -61
- data/lib/http/retriable/delay_calculator.rb +38 -11
- data/lib/http/retriable/errors.rb +21 -0
- data/lib/http/retriable/performer.rb +82 -38
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +147 -34
- data/lib/http/timeout/null.rb +155 -9
- data/lib/http/timeout/per_operation.rb +139 -18
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +289 -124
- data/lib/http/version.rb +2 -1
- data/lib/http.rb +11 -2
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/{spec → test}/support/fakeio.rb +2 -2
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/{spec → test}/support/simplecov.rb +11 -2
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +108 -168
- data/.github/workflows/ci.yml +0 -67
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.rubocop/layout.yml +0 -8
- data/.rubocop/metrics.yml +0 -4
- data/.rubocop/rspec.yml +0 -9
- data/.rubocop/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -219
- data/.yardopts +0 -2
- data/CHANGES_OLD.md +0 -1002
- data/Gemfile +0 -51
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/lib/http/headers/mixin.rb +0 -34
- data/lib/http/retriable/client.rb +0 -37
- data/logo.png +0 -0
- data/spec/lib/http/client_spec.rb +0 -556
- data/spec/lib/http/connection_spec.rb +0 -88
- data/spec/lib/http/content_type_spec.rb +0 -47
- data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
- data/spec/lib/http/features/instrumentation_spec.rb +0 -81
- data/spec/lib/http/features/logging_spec.rb +0 -65
- data/spec/lib/http/features/raise_error_spec.rb +0 -62
- data/spec/lib/http/headers/mixin_spec.rb +0 -36
- data/spec/lib/http/headers/normalizer_spec.rb +0 -52
- data/spec/lib/http/headers_spec.rb +0 -527
- data/spec/lib/http/options/body_spec.rb +0 -15
- data/spec/lib/http/options/features_spec.rb +0 -33
- data/spec/lib/http/options/form_spec.rb +0 -15
- data/spec/lib/http/options/headers_spec.rb +0 -24
- data/spec/lib/http/options/json_spec.rb +0 -15
- data/spec/lib/http/options/merge_spec.rb +0 -68
- data/spec/lib/http/options/new_spec.rb +0 -30
- data/spec/lib/http/options/proxy_spec.rb +0 -20
- data/spec/lib/http/options_spec.rb +0 -13
- data/spec/lib/http/redirector_spec.rb +0 -530
- data/spec/lib/http/request/body_spec.rb +0 -211
- data/spec/lib/http/request/writer_spec.rb +0 -121
- data/spec/lib/http/request_spec.rb +0 -234
- data/spec/lib/http/response/body_spec.rb +0 -85
- data/spec/lib/http/response/parser_spec.rb +0 -74
- data/spec/lib/http/response/status_spec.rb +0 -253
- data/spec/lib/http/response_spec.rb +0 -262
- data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
- data/spec/lib/http/retriable/performer_spec.rb +0 -302
- data/spec/lib/http/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -535
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -89
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/dummy_server/servlet.rb +0 -203
- data/spec/support/dummy_server.rb +0 -44
- data/spec/support/fuubar.rb +0 -21
- data/spec/support/http_handling_shared.rb +0 -190
- data/spec/support/proxy_server.rb +0 -39
- data/spec/support/servers/config.rb +0 -11
- data/spec/support/servers/runner.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
- /data/{spec → test}/support/capture_warning.rb +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP
|
|
4
|
+
# URI normalization and dot-segment removal
|
|
5
|
+
class URI
|
|
6
|
+
# Default URI normalizer
|
|
7
|
+
# @private
|
|
8
|
+
NORMALIZER = lambda do |uri|
|
|
9
|
+
uri = HTTP::URI.parse uri
|
|
10
|
+
scheme = uri.scheme&.downcase
|
|
11
|
+
host = uri.normalized_host
|
|
12
|
+
host = "[#{host}]" if host&.include?(":")
|
|
13
|
+
default_port = scheme == HTTPS_SCHEME ? 443 : 80
|
|
14
|
+
|
|
15
|
+
HTTP::URI.new(
|
|
16
|
+
scheme: scheme,
|
|
17
|
+
user: uri.user,
|
|
18
|
+
password: uri.password,
|
|
19
|
+
host: host,
|
|
20
|
+
port: (uri.port == default_port ? nil : uri.port),
|
|
21
|
+
path: uri.path.empty? ? "/" : percent_encode(remove_dot_segments(uri.path)),
|
|
22
|
+
query: percent_encode(uri.query),
|
|
23
|
+
fragment: uri.fragment
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Standalone dot segments that terminate the algorithm
|
|
28
|
+
# @private
|
|
29
|
+
DOT_SEGMENTS = %w[. ..].freeze
|
|
30
|
+
|
|
31
|
+
# Matches "/." followed by "/" or end-of-string
|
|
32
|
+
# @private
|
|
33
|
+
SINGLE_DOT_SEGMENT = %r{\A/\.(?:/|\z)}
|
|
34
|
+
|
|
35
|
+
# Matches "/.." followed by "/" or end-of-string
|
|
36
|
+
# @private
|
|
37
|
+
DOUBLE_DOT_SEGMENT = %r{\A/\.\.(?:/|\z)}
|
|
38
|
+
|
|
39
|
+
# Matches the last segment in a path (everything after the final "/")
|
|
40
|
+
# @private
|
|
41
|
+
LAST_SEGMENT = %r{/[^/]*\z}
|
|
42
|
+
|
|
43
|
+
# Matches the first path segment, with or without a leading "/"
|
|
44
|
+
# @private
|
|
45
|
+
FIRST_SEGMENT = %r{\A/?[^/]*}
|
|
46
|
+
|
|
47
|
+
# Remove dot segments from a URI path per RFC 3986 Section 5.2.4
|
|
48
|
+
#
|
|
49
|
+
# @param [String] path URI path to normalize
|
|
50
|
+
#
|
|
51
|
+
# @api private
|
|
52
|
+
# @return [String] path with dot segments removed
|
|
53
|
+
def self.remove_dot_segments(path)
|
|
54
|
+
input = path.dup
|
|
55
|
+
output = +""
|
|
56
|
+
until input.empty?
|
|
57
|
+
reduce_dot_segment(input, output) unless
|
|
58
|
+
input.delete_prefix!("../") || input.delete_prefix!("./") ||
|
|
59
|
+
input.sub!(SINGLE_DOT_SEGMENT, "/")
|
|
60
|
+
end
|
|
61
|
+
output
|
|
62
|
+
end
|
|
63
|
+
private_class_method :remove_dot_segments
|
|
64
|
+
|
|
65
|
+
# Process a single dot-segment removal step per RFC 3986 Section 5.2.4
|
|
66
|
+
#
|
|
67
|
+
# @param [String] input remaining path input (mutated)
|
|
68
|
+
# @param [String] output accumulated result (mutated)
|
|
69
|
+
#
|
|
70
|
+
# @api private
|
|
71
|
+
# @return [void]
|
|
72
|
+
private_class_method def self.reduce_dot_segment(input, output)
|
|
73
|
+
if input.sub!(DOUBLE_DOT_SEGMENT, "/")
|
|
74
|
+
output.sub!(LAST_SEGMENT, "")
|
|
75
|
+
elsif DOT_SEGMENTS.include?(input)
|
|
76
|
+
input.clear
|
|
77
|
+
else
|
|
78
|
+
output << input.slice!(FIRST_SEGMENT) # steep:ignore
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP
|
|
4
|
+
# Class methods and private helpers for URI parsing and host processing
|
|
5
|
+
class URI
|
|
6
|
+
# Parse the given URI string, returning an HTTP::URI object
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# HTTP::URI.parse("http://example.com/path")
|
|
10
|
+
#
|
|
11
|
+
# @param [HTTP::URI, String, #to_str] uri to parse
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
# @return [HTTP::URI] new URI instance
|
|
15
|
+
def self.parse(uri)
|
|
16
|
+
return uri if uri.is_a?(self)
|
|
17
|
+
raise InvalidError, "invalid URI: nil" if uri.nil?
|
|
18
|
+
|
|
19
|
+
uri_string = begin
|
|
20
|
+
String(uri)
|
|
21
|
+
rescue TypeError, NoMethodError
|
|
22
|
+
raise InvalidError, "invalid URI: #{uri.inspect}"
|
|
23
|
+
end
|
|
24
|
+
new(**parse_components(uri_string))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Encodes key/value pairs as application/x-www-form-urlencoded
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# HTTP::URI.form_encode(foo: "bar")
|
|
31
|
+
#
|
|
32
|
+
# @param [#to_hash, #to_ary] form_values to encode
|
|
33
|
+
# @param [TrueClass, FalseClass] sort should key/value pairs be sorted first?
|
|
34
|
+
#
|
|
35
|
+
# @api public
|
|
36
|
+
# @return [String] encoded value
|
|
37
|
+
def self.form_encode(form_values, sort: false)
|
|
38
|
+
return ::URI.encode_www_form(form_values) unless sort
|
|
39
|
+
|
|
40
|
+
::URI.encode_www_form(form_values.sort_by { |k, _| String(k) })
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Percent-encode matching characters in a string
|
|
44
|
+
#
|
|
45
|
+
# @param [String] string raw string
|
|
46
|
+
#
|
|
47
|
+
# @api private
|
|
48
|
+
# @return [String] encoded value
|
|
49
|
+
def self.percent_encode(string)
|
|
50
|
+
string&.gsub(PERCENT_ENCODE) do |substr|
|
|
51
|
+
substr.bytes.map { |c| format("%%%02X", c) }.join
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Loads the addressable gem on first use
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
# @return [void]
|
|
59
|
+
# @raise [LoadError] if addressable gem is not installed
|
|
60
|
+
def self.require_addressable
|
|
61
|
+
return if defined?(@addressable_loaded)
|
|
62
|
+
|
|
63
|
+
require "addressable/uri"
|
|
64
|
+
@addressable_loaded = true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Convert a hostname to ASCII via IDNA (requires addressable)
|
|
68
|
+
#
|
|
69
|
+
# @param [String] host hostname to encode
|
|
70
|
+
# @api private
|
|
71
|
+
# @return [String] ASCII-encoded hostname
|
|
72
|
+
def self.idna_to_ascii(host)
|
|
73
|
+
return host if host.ascii_only?
|
|
74
|
+
|
|
75
|
+
require_addressable
|
|
76
|
+
Addressable::IDNA.to_ascii(host) # steep:ignore
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Serialize the authority section of a URI (userinfo + host + port)
|
|
82
|
+
#
|
|
83
|
+
# @api private
|
|
84
|
+
# @return [String] authority component
|
|
85
|
+
def authority_string
|
|
86
|
+
str = +"//"
|
|
87
|
+
if (user = @user)
|
|
88
|
+
str << user
|
|
89
|
+
str << ":#{@password}" if @password
|
|
90
|
+
str << "@"
|
|
91
|
+
end
|
|
92
|
+
str << @raw_host # steep:ignore
|
|
93
|
+
str << ":#{@port}" if @port
|
|
94
|
+
str
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Adds or removes IPv6 brackets from a host
|
|
98
|
+
#
|
|
99
|
+
# @param [String] raw_host
|
|
100
|
+
# @param [Boolean] brackets
|
|
101
|
+
# @api private
|
|
102
|
+
# @return [String] Host with IPv6 address brackets added or removed
|
|
103
|
+
def process_ipv6_brackets(raw_host, brackets: false)
|
|
104
|
+
return unless raw_host
|
|
105
|
+
|
|
106
|
+
stripped = raw_host.delete_prefix("[").delete_suffix("]")
|
|
107
|
+
ip = IPAddr.new(stripped)
|
|
108
|
+
|
|
109
|
+
if ip.ipv6?
|
|
110
|
+
brackets ? "[#{ip}]" : ip.to_s
|
|
111
|
+
else
|
|
112
|
+
raw_host
|
|
113
|
+
end
|
|
114
|
+
rescue IPAddr::Error
|
|
115
|
+
raw_host
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Normalize a host for comparison and lookup
|
|
119
|
+
#
|
|
120
|
+
# Percent-decodes, strips trailing dot, lowercases, and IDN-encodes
|
|
121
|
+
# non-ASCII hostnames.
|
|
122
|
+
#
|
|
123
|
+
# @param [String, nil] host the host to normalize
|
|
124
|
+
# @api private
|
|
125
|
+
# @return [String, nil] normalized host
|
|
126
|
+
def normalize_host(host)
|
|
127
|
+
return nil unless host
|
|
128
|
+
|
|
129
|
+
h = host.gsub(/%\h{2}/) { |match| match.delete_prefix("%").to_i(16).chr }
|
|
130
|
+
h = h.delete_suffix(".")
|
|
131
|
+
h = h.downcase
|
|
132
|
+
self.class.idna_to_ascii(h)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Parse a URI string into component parts
|
|
136
|
+
#
|
|
137
|
+
# Uses stdlib for printable-ASCII URIs (faster), falling back to
|
|
138
|
+
# Addressable for non-ASCII or when stdlib rejects the input.
|
|
139
|
+
#
|
|
140
|
+
# @param [String] uri_string the URI to parse
|
|
141
|
+
# @api private
|
|
142
|
+
# @return [Hash] URI components
|
|
143
|
+
private_class_method def self.parse_components(uri_string)
|
|
144
|
+
return parse_with_addressable(uri_string) if uri_string.match?(NEEDS_ADDRESSABLE)
|
|
145
|
+
|
|
146
|
+
parse_with_stdlib(uri_string) || parse_with_addressable(uri_string)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Parse an ASCII URI using stdlib
|
|
150
|
+
#
|
|
151
|
+
# @param [String] uri_string the URI to parse
|
|
152
|
+
# @api private
|
|
153
|
+
# @return [Hash, nil] URI components, or nil if stdlib rejects the input
|
|
154
|
+
private_class_method def self.parse_with_stdlib(uri_string)
|
|
155
|
+
parsed = ::URI.parse(uri_string)
|
|
156
|
+
# stdlib always returns a port (defaulting to scheme's default);
|
|
157
|
+
# only store it when explicitly specified
|
|
158
|
+
port = parsed.port
|
|
159
|
+
port = nil if port.eql?(parsed.default_port)
|
|
160
|
+
{ scheme: parsed.scheme, user: parsed.user, password: parsed.password,
|
|
161
|
+
host: parsed.host, port: port, path: parsed.path,
|
|
162
|
+
query: parsed.query, fragment: parsed.fragment }
|
|
163
|
+
rescue ::URI::InvalidURIError
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Parse a non-ASCII URI using Addressable
|
|
168
|
+
#
|
|
169
|
+
# @param [String] uri_string the URI to parse
|
|
170
|
+
# @api private
|
|
171
|
+
# @return [Hash] URI components
|
|
172
|
+
private_class_method def self.parse_with_addressable(uri_string)
|
|
173
|
+
require_addressable
|
|
174
|
+
parsed = Addressable::URI.parse(uri_string) # steep:ignore
|
|
175
|
+
{ scheme: parsed.scheme, user: parsed.user, password: parsed.password,
|
|
176
|
+
host: parsed.host, port: parsed.port, path: parsed.path,
|
|
177
|
+
query: parsed.query, fragment: parsed.fragment }
|
|
178
|
+
rescue Addressable::URI::InvalidURIError # steep:ignore
|
|
179
|
+
raise InvalidError, "invalid URI: #{uri_string.inspect}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|