http 0.7.4 → 0.8.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rubocop.yml +5 -2
- data/CHANGES.md +24 -7
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +24 -22
- data/Guardfile +2 -2
- data/README.md +34 -4
- data/Rakefile +7 -7
- data/examples/parallel_requests_with_celluloid.rb +2 -2
- data/http.gemspec +12 -12
- data/lib/http.rb +11 -10
- data/lib/http/cache.rb +146 -0
- data/lib/http/cache/headers.rb +100 -0
- data/lib/http/cache/null_cache.rb +13 -0
- data/lib/http/chainable.rb +14 -3
- data/lib/http/client.rb +64 -80
- data/lib/http/connection.rb +139 -0
- data/lib/http/content_type.rb +2 -2
- data/lib/http/errors.rb +7 -1
- data/lib/http/headers.rb +21 -8
- data/lib/http/headers/mixin.rb +1 -1
- data/lib/http/mime_type.rb +2 -2
- data/lib/http/mime_type/adapter.rb +2 -2
- data/lib/http/mime_type/json.rb +4 -4
- data/lib/http/options.rb +65 -74
- data/lib/http/redirector.rb +3 -3
- data/lib/http/request.rb +20 -13
- data/lib/http/request/caching.rb +95 -0
- data/lib/http/request/writer.rb +5 -5
- data/lib/http/response.rb +15 -9
- data/lib/http/response/body.rb +21 -8
- data/lib/http/response/caching.rb +142 -0
- data/lib/http/response/io_body.rb +63 -0
- data/lib/http/response/parser.rb +1 -1
- data/lib/http/response/status.rb +4 -12
- data/lib/http/response/status/reasons.rb +53 -53
- data/lib/http/response/string_body.rb +53 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/cache/headers_spec.rb +77 -0
- data/spec/lib/http/cache_spec.rb +182 -0
- data/spec/lib/http/client_spec.rb +123 -95
- data/spec/lib/http/content_type_spec.rb +25 -25
- data/spec/lib/http/headers/mixin_spec.rb +8 -8
- data/spec/lib/http/headers_spec.rb +213 -173
- data/spec/lib/http/options/body_spec.rb +5 -5
- data/spec/lib/http/options/form_spec.rb +3 -3
- data/spec/lib/http/options/headers_spec.rb +7 -7
- data/spec/lib/http/options/json_spec.rb +3 -3
- data/spec/lib/http/options/merge_spec.rb +26 -22
- data/spec/lib/http/options/new_spec.rb +10 -10
- data/spec/lib/http/options/proxy_spec.rb +8 -8
- data/spec/lib/http/options_spec.rb +2 -2
- data/spec/lib/http/redirector_spec.rb +32 -32
- data/spec/lib/http/request/caching_spec.rb +133 -0
- data/spec/lib/http/request/writer_spec.rb +26 -26
- data/spec/lib/http/request_spec.rb +63 -58
- data/spec/lib/http/response/body_spec.rb +13 -13
- data/spec/lib/http/response/caching_spec.rb +201 -0
- data/spec/lib/http/response/io_body_spec.rb +35 -0
- data/spec/lib/http/response/status_spec.rb +25 -25
- data/spec/lib/http/response/string_body_spec.rb +35 -0
- data/spec/lib/http/response_spec.rb +64 -45
- data/spec/lib/http_spec.rb +103 -76
- data/spec/spec_helper.rb +10 -12
- data/spec/support/connection_reuse_shared.rb +100 -0
- data/spec/support/create_certs.rb +12 -12
- data/spec/support/dummy_server.rb +11 -11
- data/spec/support/dummy_server/servlet.rb +43 -31
- data/spec/support/proxy_server.rb +31 -25
- metadata +57 -8
- data/spec/support/example_server.rb +0 -30
- data/spec/support/example_server/servlet.rb +0 -102
data/lib/http/content_type.rb
CHANGED
@@ -9,7 +9,7 @@ module HTTP
|
|
9
9
|
new mime_type(str), charset(str)
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
private
|
13
13
|
|
14
14
|
# :nodoc:
|
15
15
|
def mime_type(str)
|
@@ -20,7 +20,7 @@ module HTTP
|
|
20
20
|
# :nodoc:
|
21
21
|
def charset(str)
|
22
22
|
md = str.to_s.match CHARSET_RE
|
23
|
-
md && md[1].to_s.strip.gsub(/^"|"$/,
|
23
|
+
md && md[1].to_s.strip.gsub(/^"|"$/, "")
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
data/lib/http/errors.rb
CHANGED
@@ -8,6 +8,12 @@ module HTTP
|
|
8
8
|
# Generic Response error
|
9
9
|
class ResponseError < Error; end
|
10
10
|
|
11
|
-
#
|
11
|
+
# Requested to do something when we're in the wrong state
|
12
12
|
class StateError < ResponseError; end
|
13
|
+
|
14
|
+
# Generic Cache error
|
15
|
+
class CacheError < Error; end
|
16
|
+
|
17
|
+
# Header name is invalid
|
18
|
+
class InvalidHeaderNameError < Error; end
|
13
19
|
end
|
data/lib/http/headers.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
require
|
1
|
+
require "forwardable"
|
2
2
|
|
3
|
-
require
|
3
|
+
require "http/errors"
|
4
|
+
require "http/headers/mixin"
|
4
5
|
|
5
6
|
module HTTP
|
6
7
|
# HTTP Headers container.
|
@@ -11,6 +12,10 @@ module HTTP
|
|
11
12
|
# Matches HTTP header names when in "Canonical-Http-Format"
|
12
13
|
CANONICAL_HEADER = /^[A-Z][a-z]*(-[A-Z][a-z]*)*$/
|
13
14
|
|
15
|
+
# Matches valid header field name according to RFC.
|
16
|
+
# @see http://tools.ietf.org/html/rfc7230#section-3.2
|
17
|
+
HEADER_NAME_RE = /^[A-Za-z0-9!#\$%&'*+\-.^_`|~]+$/
|
18
|
+
|
14
19
|
# Class constructor.
|
15
20
|
def initialize
|
16
21
|
@pile = []
|
@@ -31,7 +36,7 @@ module HTTP
|
|
31
36
|
# @param [#to_s] name header name
|
32
37
|
# @return [void]
|
33
38
|
def delete(name)
|
34
|
-
name =
|
39
|
+
name = normalize_header name.to_s
|
35
40
|
@pile.delete_if { |k, _| k == name }
|
36
41
|
end
|
37
42
|
|
@@ -41,7 +46,7 @@ module HTTP
|
|
41
46
|
# @param [Array<#to_s>, #to_s] value header value(s) to be appended
|
42
47
|
# @return [void]
|
43
48
|
def add(name, value)
|
44
|
-
name =
|
49
|
+
name = normalize_header name.to_s
|
45
50
|
Array(value).each { |v| @pile << [name, v.to_s] }
|
46
51
|
end
|
47
52
|
|
@@ -52,7 +57,7 @@ module HTTP
|
|
52
57
|
#
|
53
58
|
# @return [Array<String>]
|
54
59
|
def get(name)
|
55
|
-
name =
|
60
|
+
name = normalize_header name.to_s
|
56
61
|
@pile.select { |k, _| k == name }.map { |_, v| v }
|
57
62
|
end
|
58
63
|
|
@@ -77,6 +82,7 @@ module HTTP
|
|
77
82
|
def to_h
|
78
83
|
Hash[keys.map { |k| [k, self[k]] }]
|
79
84
|
end
|
85
|
+
alias_method :to_hash, :to_h
|
80
86
|
|
81
87
|
# Returns headers key/value pairs.
|
82
88
|
#
|
@@ -179,14 +185,21 @@ module HTTP
|
|
179
185
|
alias_method :[], :coerce
|
180
186
|
end
|
181
187
|
|
182
|
-
|
188
|
+
private
|
183
189
|
|
184
190
|
# Transforms `name` to canonical HTTP header capitalization
|
185
191
|
#
|
186
192
|
# @param [String] name
|
193
|
+
# @raise [InvalidHeaderNameError] if normalized name does not
|
194
|
+
# match {HEADER_NAME_RE}
|
187
195
|
# @return [String] canonical HTTP header name
|
188
|
-
def
|
189
|
-
name[CANONICAL_HEADER]
|
196
|
+
def normalize_header(name)
|
197
|
+
normalized = name[CANONICAL_HEADER]
|
198
|
+
normalized ||= name.split(/[\-_]/).map(&:capitalize).join("-")
|
199
|
+
|
200
|
+
return normalized if normalized =~ HEADER_NAME_RE
|
201
|
+
|
202
|
+
fail InvalidHeaderNameError, "Invalid HTTP header field name: #{name.inspect}"
|
190
203
|
end
|
191
204
|
end
|
192
205
|
end
|
data/lib/http/headers/mixin.rb
CHANGED
data/lib/http/mime_type.rb
CHANGED
data/lib/http/mime_type/json.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "json"
|
2
|
+
require "http/mime_type/adapter"
|
3
3
|
|
4
4
|
module HTTP
|
5
5
|
module MimeType
|
@@ -17,7 +17,7 @@ module HTTP
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
register_adapter
|
21
|
-
register_alias
|
20
|
+
register_adapter "application/json", JSON
|
21
|
+
register_alias "application/json", :json
|
22
22
|
end
|
23
23
|
end
|
data/lib/http/options.rb
CHANGED
@@ -1,81 +1,78 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require "http/headers"
|
2
|
+
require "openssl"
|
3
|
+
require "socket"
|
4
|
+
require "http/cache/null_cache"
|
4
5
|
|
5
6
|
module HTTP
|
6
7
|
class Options
|
7
|
-
# How to format the response [:object, :body, :parse_body]
|
8
|
-
attr_accessor :response
|
9
|
-
|
10
|
-
# HTTP headers to include in the request
|
11
|
-
attr_accessor :headers
|
12
|
-
|
13
|
-
# Query string params to add to the url
|
14
|
-
attr_accessor :params
|
15
|
-
|
16
|
-
# Form data to embed in the request
|
17
|
-
attr_accessor :form
|
18
|
-
|
19
|
-
# JSON data to embed in the request
|
20
|
-
attr_accessor :json
|
21
|
-
|
22
|
-
# Explicit request body of the request
|
23
|
-
attr_accessor :body
|
24
|
-
|
25
|
-
# HTTP proxy to route request
|
26
|
-
attr_accessor :proxy
|
27
|
-
|
28
|
-
# Socket classes
|
29
|
-
attr_accessor :socket_class, :ssl_socket_class
|
30
|
-
|
31
|
-
# SSL context
|
32
|
-
attr_accessor :ssl_context
|
33
|
-
|
34
|
-
# Follow redirects
|
35
|
-
attr_accessor :follow
|
36
|
-
|
37
|
-
protected :response=, :headers=, :proxy=, :params=, :form=, :json=, :follow=
|
38
|
-
|
39
8
|
@default_socket_class = TCPSocket
|
40
9
|
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
41
10
|
|
11
|
+
@default_cache = Http::Cache::NullCache.new
|
12
|
+
|
42
13
|
class << self
|
43
14
|
attr_accessor :default_socket_class, :default_ssl_socket_class
|
15
|
+
attr_accessor :default_cache
|
44
16
|
|
45
17
|
def new(options = {})
|
46
18
|
return options if options.is_a?(self)
|
47
19
|
super
|
48
20
|
end
|
21
|
+
|
22
|
+
def defined_options
|
23
|
+
@defined_options ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def def_option(name, &interpreter)
|
29
|
+
defined_options << name.to_sym
|
30
|
+
interpreter ||= ->(v) { v }
|
31
|
+
|
32
|
+
attr_accessor name
|
33
|
+
protected :"#{name}="
|
34
|
+
|
35
|
+
define_method(:"with_#{name}") do |value|
|
36
|
+
dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) }
|
37
|
+
end
|
38
|
+
end
|
49
39
|
end
|
50
40
|
|
51
41
|
def initialize(options = {})
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
42
|
+
defaults = {:response => :auto,
|
43
|
+
:proxy => {},
|
44
|
+
:socket_class => self.class.default_socket_class,
|
45
|
+
:ssl_socket_class => self.class.default_ssl_socket_class,
|
46
|
+
:cache => self.class.default_cache,
|
47
|
+
:headers => {}}
|
48
|
+
|
49
|
+
opts_w_defaults = defaults.merge(options)
|
50
|
+
opts_w_defaults[:headers] = HTTP::Headers.coerce(opts_w_defaults[:headers])
|
51
|
+
|
52
|
+
opts_w_defaults.each do |(opt_name, opt_val)|
|
53
|
+
self[opt_name] = opt_val
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def_option :headers do |headers|
|
58
|
+
self.headers.merge(headers)
|
59
|
+
end
|
60
|
+
|
61
|
+
%w(proxy params form json body follow response socket_class ssl_socket_class ssl_context persistent).each do |method_name|
|
62
|
+
def_option method_name
|
65
63
|
end
|
66
64
|
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
def_option :cache do |cache_or_cache_options|
|
66
|
+
if cache_or_cache_options.respond_to? :perform
|
67
|
+
cache_or_cache_options
|
68
|
+
else
|
69
|
+
require "http/cache"
|
70
|
+
HTTP::Cache.new(cache_or_cache_options)
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
|
-
|
74
|
-
|
75
|
-
def with_#{method_name}(value)
|
76
|
-
dup { |opts| opts.#{method_name} = value }
|
77
|
-
end
|
78
|
-
RUBY
|
74
|
+
def persistent?
|
75
|
+
!persistent.nil? && persistent != ""
|
79
76
|
end
|
80
77
|
|
81
78
|
def [](option)
|
@@ -97,22 +94,10 @@ module HTTP
|
|
97
94
|
end
|
98
95
|
|
99
96
|
def to_hash
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
:response => response,
|
105
|
-
:headers => headers.to_h,
|
106
|
-
:proxy => proxy,
|
107
|
-
:params => params,
|
108
|
-
:form => form,
|
109
|
-
:json => json,
|
110
|
-
:body => body,
|
111
|
-
:follow => follow,
|
112
|
-
:socket_class => socket_class,
|
113
|
-
:ssl_socket_class => ssl_socket_class,
|
114
|
-
:ssl_context => ssl_context
|
115
|
-
}
|
97
|
+
hash_pairs = self.class
|
98
|
+
.defined_options
|
99
|
+
.flat_map { |opt_name| [opt_name, self[opt_name]] }
|
100
|
+
Hash[*hash_pairs]
|
116
101
|
end
|
117
102
|
|
118
103
|
def dup
|
@@ -121,7 +106,13 @@ module HTTP
|
|
121
106
|
dupped
|
122
107
|
end
|
123
108
|
|
124
|
-
|
109
|
+
protected
|
110
|
+
|
111
|
+
def []=(option, val)
|
112
|
+
send(:"#{option}=", val)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
125
116
|
|
126
117
|
def argument_error!(message)
|
127
118
|
fail(Error, message, caller[1..-1])
|
data/lib/http/redirector.rb
CHANGED
@@ -22,7 +22,7 @@ module HTTP
|
|
22
22
|
follow(&block)
|
23
23
|
end
|
24
24
|
|
25
|
-
|
25
|
+
private
|
26
26
|
|
27
27
|
# Reset redirector state
|
28
28
|
def reset(request, response)
|
@@ -38,8 +38,8 @@ module HTTP
|
|
38
38
|
fail TooManyRedirectsError if too_many_hops?
|
39
39
|
fail EndlessRedirectError if endless_loop?
|
40
40
|
|
41
|
-
uri = @response.headers[
|
42
|
-
fail StateError,
|
41
|
+
uri = @response.headers["Location"]
|
42
|
+
fail StateError, "no Location header in redirect" unless uri
|
43
43
|
|
44
44
|
if 303 == @response.code
|
45
45
|
@request = @request.redirect uri, :get
|
data/lib/http/request.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
1
|
+
require "http/errors"
|
2
|
+
require "http/headers"
|
3
|
+
require "http/request/caching"
|
4
|
+
require "http/request/writer"
|
5
|
+
require "http/version"
|
6
|
+
require "base64"
|
7
|
+
require "uri"
|
8
|
+
require "time"
|
7
9
|
|
8
10
|
module HTTP
|
9
11
|
class Request
|
@@ -66,7 +68,7 @@ module HTTP
|
|
66
68
|
attr_reader :proxy, :body, :version
|
67
69
|
|
68
70
|
# :nodoc:
|
69
|
-
def initialize(verb, uri, headers = {}, proxy = {}, body = nil, version =
|
71
|
+
def initialize(verb, uri, headers = {}, proxy = {}, body = nil, version = "1.1") # rubocop:disable ParameterLists
|
70
72
|
@verb = verb.to_s.downcase.to_sym
|
71
73
|
@uri = uri.is_a?(URI) ? uri : URI(uri.to_s)
|
72
74
|
@scheme = @uri.scheme && @uri.scheme.to_s.downcase.to_sym
|
@@ -78,15 +80,15 @@ module HTTP
|
|
78
80
|
|
79
81
|
@headers = HTTP::Headers.coerce(headers || {})
|
80
82
|
|
81
|
-
@headers[
|
82
|
-
@headers[
|
83
|
+
@headers["Host"] ||= default_host
|
84
|
+
@headers["User-Agent"] ||= USER_AGENT
|
83
85
|
end
|
84
86
|
|
85
87
|
# Returns new Request with updated uri
|
86
88
|
def redirect(uri, verb = @verb)
|
87
89
|
uri = @uri.merge uri.to_s
|
88
90
|
req = self.class.new(verb, uri, headers, proxy, body, version)
|
89
|
-
req[
|
91
|
+
req["Host"] = req.uri.host
|
90
92
|
req
|
91
93
|
end
|
92
94
|
|
@@ -109,7 +111,7 @@ module HTTP
|
|
109
111
|
# Compute and add the Proxy-Authorization header
|
110
112
|
def include_proxy_authorization_header
|
111
113
|
digest = Base64.encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}").chomp
|
112
|
-
headers[
|
114
|
+
headers["Proxy-Authorization"] = "Basic #{digest}"
|
113
115
|
end
|
114
116
|
|
115
117
|
# Compute HTTP request header for direct or proxy request
|
@@ -127,7 +129,12 @@ module HTTP
|
|
127
129
|
using_proxy? ? proxy[:proxy_port] : uri.port
|
128
130
|
end
|
129
131
|
|
130
|
-
|
132
|
+
# @return [HTTP::Request::Caching]
|
133
|
+
def caching
|
134
|
+
Caching.new self
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
131
138
|
|
132
139
|
def path_for_request_header
|
133
140
|
if using_proxy?
|
@@ -139,7 +146,7 @@ module HTTP
|
|
139
146
|
|
140
147
|
def uri_path_with_query
|
141
148
|
path = uri_has_query? ? "#{uri.path}?#{uri.query}" : uri.path
|
142
|
-
path.empty? ?
|
149
|
+
path.empty? ? "/" : path
|
143
150
|
end
|
144
151
|
|
145
152
|
def uri_has_query?
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "http/cache/headers"
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
class Request
|
5
|
+
# Decorator for requests to provide convenience methods related to caching.
|
6
|
+
class Caching < DelegateClass(HTTP::Request)
|
7
|
+
INVALIDATING_METHODS = [:post, :put, :delete, :patch].freeze
|
8
|
+
CACHEABLE_METHODS = [:get, :head].freeze
|
9
|
+
|
10
|
+
# When was this request sent to the server
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
attr_accessor :sent_at
|
14
|
+
|
15
|
+
# Inits a new instance
|
16
|
+
# @api private
|
17
|
+
def initialize(obj)
|
18
|
+
super
|
19
|
+
@requested_at = nil
|
20
|
+
@received_at = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [HTTP::Request::Caching]
|
24
|
+
def caching
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Boolean] true iff request demands the resources cache entry be invalidated
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def invalidates_cache?
|
32
|
+
INVALIDATING_METHODS.include?(verb) ||
|
33
|
+
cache_headers.no_store?
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean] true if request is cacheable
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
def cacheable?
|
40
|
+
CACHEABLE_METHODS.include?(verb) &&
|
41
|
+
!cache_headers.no_store?
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Boolean] true iff the cache control info of this
|
45
|
+
# request demands that the response be revalidated by the origin
|
46
|
+
# server.
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def skips_cache?
|
50
|
+
0 == cache_headers.max_age ||
|
51
|
+
cache_headers.must_revalidate? ||
|
52
|
+
cache_headers.no_cache?
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [HTTP::Request::Caching] new request based on this
|
56
|
+
# one but conditional on the resource having changed since
|
57
|
+
# `cached_response`
|
58
|
+
#
|
59
|
+
# @api public
|
60
|
+
def conditional_on_changes_to(cached_response)
|
61
|
+
self.class.new HTTP::Request.new(
|
62
|
+
verb, uri, headers.merge(conditional_headers_for(cached_response)),
|
63
|
+
proxy, body, version).caching
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [HTTP::Cache::Headers] cache control helper for this request
|
67
|
+
# @api public
|
68
|
+
def cache_headers
|
69
|
+
@cache_headers ||= HTTP::Cache::Headers.new headers
|
70
|
+
end
|
71
|
+
|
72
|
+
def env
|
73
|
+
{"rack-cache.cache_key" => ->(r) { r.uri.to_s }}
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# @return [Headers] conditional request headers
|
79
|
+
# @api private
|
80
|
+
def conditional_headers_for(cached_response)
|
81
|
+
headers = HTTP::Headers.new
|
82
|
+
|
83
|
+
cached_response.headers.get("Etag")
|
84
|
+
.each { |etag| headers.add("If-None-Match", etag) }
|
85
|
+
|
86
|
+
cached_response.headers.get("Last-Modified")
|
87
|
+
.each { |last_mod| headers.add("If-Modified-Since", last_mod) }
|
88
|
+
|
89
|
+
headers.add("Cache-Control", "max-age=0") if cache_headers.forces_revalidation?
|
90
|
+
|
91
|
+
headers
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|