httpx 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +191 -0
- data/README.md +119 -0
- data/lib/httpx.rb +50 -0
- data/lib/httpx/buffer.rb +34 -0
- data/lib/httpx/callbacks.rb +32 -0
- data/lib/httpx/chainable.rb +51 -0
- data/lib/httpx/channel.rb +222 -0
- data/lib/httpx/channel/http1.rb +220 -0
- data/lib/httpx/channel/http2.rb +224 -0
- data/lib/httpx/client.rb +173 -0
- data/lib/httpx/connection.rb +74 -0
- data/lib/httpx/errors.rb +7 -0
- data/lib/httpx/extensions.rb +52 -0
- data/lib/httpx/headers.rb +152 -0
- data/lib/httpx/io.rb +240 -0
- data/lib/httpx/loggable.rb +11 -0
- data/lib/httpx/options.rb +138 -0
- data/lib/httpx/plugins/authentication.rb +14 -0
- data/lib/httpx/plugins/basic_authentication.rb +20 -0
- data/lib/httpx/plugins/compression.rb +123 -0
- data/lib/httpx/plugins/compression/brotli.rb +55 -0
- data/lib/httpx/plugins/compression/deflate.rb +50 -0
- data/lib/httpx/plugins/compression/gzip.rb +59 -0
- data/lib/httpx/plugins/cookies.rb +63 -0
- data/lib/httpx/plugins/digest_authentication.rb +141 -0
- data/lib/httpx/plugins/follow_redirects.rb +72 -0
- data/lib/httpx/plugins/h2c.rb +85 -0
- data/lib/httpx/plugins/proxy.rb +108 -0
- data/lib/httpx/plugins/proxy/http.rb +115 -0
- data/lib/httpx/plugins/proxy/socks4.rb +110 -0
- data/lib/httpx/plugins/proxy/socks5.rb +152 -0
- data/lib/httpx/plugins/push_promise.rb +67 -0
- data/lib/httpx/plugins/stream.rb +33 -0
- data/lib/httpx/registry.rb +88 -0
- data/lib/httpx/request.rb +222 -0
- data/lib/httpx/response.rb +225 -0
- data/lib/httpx/selector.rb +155 -0
- data/lib/httpx/timeout.rb +68 -0
- data/lib/httpx/transcoder.rb +12 -0
- data/lib/httpx/transcoder/body.rb +56 -0
- data/lib/httpx/transcoder/chunker.rb +38 -0
- data/lib/httpx/transcoder/form.rb +41 -0
- data/lib/httpx/transcoder/json.rb +36 -0
- data/lib/httpx/version.rb +5 -0
- metadata +150 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
module PushPromise
|
6
|
+
PUSH_OPTIONS = { http2_settings: { settings_enable_push: 1 },
|
7
|
+
max_concurrent_requests: 1 }.freeze
|
8
|
+
|
9
|
+
module ResponseMethods
|
10
|
+
def pushed?
|
11
|
+
@__pushed
|
12
|
+
end
|
13
|
+
|
14
|
+
def mark_as_pushed!
|
15
|
+
@__pushed = true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
def initialize(opts = {})
|
21
|
+
super(PUSH_OPTIONS.merge(opts))
|
22
|
+
@promise_headers = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def on_promise(parser, stream)
|
28
|
+
stream.on(:promise_headers) do |h|
|
29
|
+
__on_promise_request(parser, stream, h)
|
30
|
+
end
|
31
|
+
stream.on(:headers) do |h|
|
32
|
+
__on_promise_response(parser, stream, h)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def __on_promise_request(parser, stream, h)
|
37
|
+
log(1, "#{stream.id}: ") do
|
38
|
+
h.map { |k, v| "-> PROMISE HEADER: #{k}: #{v}" }.join("\n")
|
39
|
+
end
|
40
|
+
headers = @options.headers_class.new(h)
|
41
|
+
path = headers[":path"]
|
42
|
+
authority = headers[":authority"]
|
43
|
+
request = parser.pending.find { |r| r.authority == authority && r.path == path }
|
44
|
+
if request
|
45
|
+
request.merge_headers(headers)
|
46
|
+
@promise_headers[stream] = request
|
47
|
+
parser.pending.delete(request)
|
48
|
+
else
|
49
|
+
stream.refuse
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def __on_promise_response(parser, stream, h)
|
54
|
+
request = @promise_headers.delete(stream)
|
55
|
+
return unless request
|
56
|
+
parser.__send__(:on_stream_headers, stream, request, h)
|
57
|
+
request.transition(:done)
|
58
|
+
response = request.response
|
59
|
+
response.mark_as_pushed!
|
60
|
+
stream.on(:data, &parser.method(:on_stream_data).curry[stream, request])
|
61
|
+
stream.on(:close, &parser.method(:on_stream_close).curry[stream, request])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
register_plugin(:push_promise, PushPromise)
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
module Stream
|
6
|
+
module InstanceMethods
|
7
|
+
def stream
|
8
|
+
headers("accept" => "text/event-stream",
|
9
|
+
"cache-control" => "no-cache")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ResponseMethods
|
14
|
+
def complete?
|
15
|
+
super ||
|
16
|
+
stream? &&
|
17
|
+
@stream_complete
|
18
|
+
end
|
19
|
+
|
20
|
+
def stream?
|
21
|
+
@headers["content-type"].start_with?("text/event-stream")
|
22
|
+
end
|
23
|
+
|
24
|
+
def <<(data)
|
25
|
+
res = super
|
26
|
+
@stream_complete = true if String(data).end_with?("\n\n")
|
27
|
+
res
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
register_plugin :stream, Stream
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
# Adds a general-purpose registry API to a class. It is designed to be a
|
5
|
+
# configuration-level API, i.e. the registry is global to the class and
|
6
|
+
# should be set on **boot time**.
|
7
|
+
#
|
8
|
+
# It is used internally to associate tags with handlers.
|
9
|
+
#
|
10
|
+
# ## Register/Fetch
|
11
|
+
#
|
12
|
+
# One is strongly advised to register handlers when creating the class.
|
13
|
+
#
|
14
|
+
# There is an instance-level method to retrieve from the registry based
|
15
|
+
# on the tag:
|
16
|
+
#
|
17
|
+
# class Server
|
18
|
+
# include HTTPX::Registry
|
19
|
+
#
|
20
|
+
# register "tcp", TCPHandler
|
21
|
+
# register "ssl", SSLHandlers
|
22
|
+
# ...
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# def handle(uri)
|
26
|
+
# scheme = uri.scheme
|
27
|
+
# handler = registry(scheme) #=> TCPHandler
|
28
|
+
# handler.handle
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
module Registry
|
33
|
+
# Base Registry Error
|
34
|
+
Error = Class.new(Error)
|
35
|
+
|
36
|
+
def self.extended(klass)
|
37
|
+
super
|
38
|
+
klass.extend(ClassMethods)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.included(klass)
|
42
|
+
super
|
43
|
+
klass.extend(ClassMethods)
|
44
|
+
klass.__send__(:include, InstanceMethods)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Class Methods
|
48
|
+
module ClassMethods
|
49
|
+
def inherited(klass)
|
50
|
+
super
|
51
|
+
klass.instance_variable_set(:@registry, @registry.dup)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param [Object] tag the handler identifier in the registry
|
55
|
+
# @return [Symbol, String, Object] the corresponding handler (if Symbol or String,
|
56
|
+
# will assume it referes to an autoloaded module, and will load-and-return it).
|
57
|
+
#
|
58
|
+
def registry(tag = nil)
|
59
|
+
@registry ||= {}
|
60
|
+
return @registry if tag.nil?
|
61
|
+
handler = @registry.fetch(tag)
|
62
|
+
raise(Error, "#{tag} is not registered in #{self}") unless handler
|
63
|
+
case handler
|
64
|
+
when Symbol, String
|
65
|
+
const_get(handler)
|
66
|
+
else
|
67
|
+
handler
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param [Object] tag the identifier for the handler in the registry
|
72
|
+
# @return [Symbol, String, Object] the handler (if Symbol or String, it is
|
73
|
+
# assumed to be an autoloaded module, to be loaded later)
|
74
|
+
#
|
75
|
+
def register(tag, handler)
|
76
|
+
registry[tag] = handler
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Instance Methods
|
81
|
+
module InstanceMethods
|
82
|
+
# delegates to HTTPX::Registry#registry
|
83
|
+
def registry(tag)
|
84
|
+
self.class.registry(tag)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module HTTPX
|
6
|
+
class Request
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
METHODS = [
|
10
|
+
# RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
|
11
|
+
:options, :get, :head, :post, :put, :delete, :trace, :connect,
|
12
|
+
|
13
|
+
# RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV
|
14
|
+
:propfind, :proppatch, :mkcol, :copy, :move, :lock, :unlock,
|
15
|
+
|
16
|
+
# RFC 3648: WebDAV Ordered Collections Protocol
|
17
|
+
:orderpatch,
|
18
|
+
|
19
|
+
# RFC 3744: WebDAV Access Control Protocol
|
20
|
+
:acl,
|
21
|
+
|
22
|
+
# RFC 6352: vCard Extensions to WebDAV -- CardDAV
|
23
|
+
:report,
|
24
|
+
|
25
|
+
# RFC 5789: PATCH Method for HTTP
|
26
|
+
:patch,
|
27
|
+
|
28
|
+
# draft-reschke-webdav-search: WebDAV Search
|
29
|
+
:search
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
USER_AGENT = "httpx.rb/#{VERSION}"
|
33
|
+
|
34
|
+
attr_reader :verb, :uri, :headers, :body, :state
|
35
|
+
|
36
|
+
attr_accessor :response
|
37
|
+
|
38
|
+
def_delegator :@body, :<<
|
39
|
+
|
40
|
+
def_delegator :@body, :empty?
|
41
|
+
|
42
|
+
def_delegator :@body, :chunk!
|
43
|
+
|
44
|
+
def initialize(verb, uri, options = {})
|
45
|
+
@verb = verb.to_s.downcase.to_sym
|
46
|
+
@uri = URI(uri)
|
47
|
+
@options = Options.new(options)
|
48
|
+
|
49
|
+
raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
|
50
|
+
|
51
|
+
@headers = @options.headers_class.new(@options.headers)
|
52
|
+
@headers["user-agent"] ||= USER_AGENT
|
53
|
+
@headers["accept"] ||= "*/*"
|
54
|
+
|
55
|
+
@body = @options.request_body_class.new(@headers, @options)
|
56
|
+
@state = :idle
|
57
|
+
end
|
58
|
+
|
59
|
+
def merge_headers(h)
|
60
|
+
@headers = @headers.merge(h)
|
61
|
+
end
|
62
|
+
|
63
|
+
def scheme
|
64
|
+
@uri.scheme
|
65
|
+
end
|
66
|
+
|
67
|
+
def path
|
68
|
+
path = uri.path
|
69
|
+
path << "/" if path.empty?
|
70
|
+
path << "?#{query}" unless query.empty?
|
71
|
+
path
|
72
|
+
end
|
73
|
+
|
74
|
+
def authority
|
75
|
+
host = @uri.host
|
76
|
+
port_string = @uri.port == @uri.default_port ? nil : ":#{@uri.port}"
|
77
|
+
"#{host}#{port_string}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def query
|
81
|
+
return @query if defined?(@query)
|
82
|
+
query = []
|
83
|
+
if (q = @options.params)
|
84
|
+
query << URI.encode_www_form(q)
|
85
|
+
end
|
86
|
+
query << @uri.query if @uri.query
|
87
|
+
@query = query.join("&")
|
88
|
+
end
|
89
|
+
|
90
|
+
def drain_body
|
91
|
+
return nil if @body.nil?
|
92
|
+
@drainer ||= @body.each
|
93
|
+
chunk = @drainer.next
|
94
|
+
chunk.dup
|
95
|
+
rescue StopIteration
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def inspect
|
100
|
+
"#<Request #{@verb.to_s.upcase} #{path} @headers=#{@headers.to_hash} @body=#{@body}>"
|
101
|
+
end
|
102
|
+
|
103
|
+
class Body
|
104
|
+
class << self
|
105
|
+
def new(*, options)
|
106
|
+
return options.body if options.body.is_a?(self)
|
107
|
+
super
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def initialize(headers, options)
|
112
|
+
@headers = headers
|
113
|
+
@body = if options.body
|
114
|
+
Transcoder.registry("body").encode(options.body)
|
115
|
+
elsif options.form
|
116
|
+
Transcoder.registry("form").encode(options.form)
|
117
|
+
elsif options.json
|
118
|
+
Transcoder.registry("json").encode(options.json)
|
119
|
+
end
|
120
|
+
return if @body.nil?
|
121
|
+
@headers["content-type"] ||= @body.content_type
|
122
|
+
@headers["content-length"] = @body.bytesize unless unbounded_body?
|
123
|
+
end
|
124
|
+
|
125
|
+
def each(&block)
|
126
|
+
return enum_for(__method__) unless block_given?
|
127
|
+
return if @body.nil?
|
128
|
+
body = stream(@body)
|
129
|
+
if body.respond_to?(:read)
|
130
|
+
::IO.copy_stream(body, ProcIO.new(block))
|
131
|
+
elsif body.respond_to?(:each)
|
132
|
+
body.each(&block)
|
133
|
+
else
|
134
|
+
block[body.to_s]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def empty?
|
139
|
+
return true if @body.nil?
|
140
|
+
return false if chunked?
|
141
|
+
bytesize.zero?
|
142
|
+
end
|
143
|
+
|
144
|
+
def bytesize
|
145
|
+
return 0 if @body.nil?
|
146
|
+
if @body.respond_to?(:bytesize)
|
147
|
+
@body.bytesize
|
148
|
+
elsif @body.respond_to?(:size)
|
149
|
+
@body.size
|
150
|
+
else
|
151
|
+
raise Error, "cannot determine size of body: #{@body.inspect}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def stream(body)
|
156
|
+
encoded = body
|
157
|
+
encoded = Transcoder.registry("chunker").encode(body) if chunked?
|
158
|
+
encoded
|
159
|
+
end
|
160
|
+
|
161
|
+
def unbounded_body?
|
162
|
+
chunked? || @body.bytesize == Float::INFINITY
|
163
|
+
end
|
164
|
+
|
165
|
+
def chunked?
|
166
|
+
@headers["transfer-encoding"] == "chunked"
|
167
|
+
end
|
168
|
+
|
169
|
+
def chunk!
|
170
|
+
@headers.add("transfer-encoding", "chunked")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def transition(nextstate)
|
175
|
+
case nextstate
|
176
|
+
when :idle
|
177
|
+
@response = nil
|
178
|
+
when :headers
|
179
|
+
return unless @state == :idle
|
180
|
+
when :body
|
181
|
+
return unless @state == :headers ||
|
182
|
+
@state == :expect
|
183
|
+
|
184
|
+
if @headers.key?("expect")
|
185
|
+
unless @response
|
186
|
+
@state = :expect
|
187
|
+
return
|
188
|
+
end
|
189
|
+
|
190
|
+
case @response.status
|
191
|
+
when 100
|
192
|
+
# deallocate
|
193
|
+
@response = nil
|
194
|
+
when 417
|
195
|
+
@response = ErrorResponse.new("Expectation Failed", 0)
|
196
|
+
return
|
197
|
+
end
|
198
|
+
end
|
199
|
+
when :done
|
200
|
+
return if @state == :expect
|
201
|
+
end
|
202
|
+
@state = nextstate
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
|
206
|
+
def expects?
|
207
|
+
@headers["expect"] == "100-continue" &&
|
208
|
+
@response && @response.status == 100
|
209
|
+
end
|
210
|
+
|
211
|
+
class ProcIO
|
212
|
+
def initialize(block)
|
213
|
+
@block = block
|
214
|
+
end
|
215
|
+
|
216
|
+
def write(data)
|
217
|
+
@block.call(data)
|
218
|
+
data.bytesize
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
require "tempfile"
|
5
|
+
require "fileutils"
|
6
|
+
require "forwardable"
|
7
|
+
|
8
|
+
module HTTPX
|
9
|
+
class Response
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_reader :status, :headers, :body, :version
|
13
|
+
|
14
|
+
def_delegator :@body, :to_s
|
15
|
+
|
16
|
+
def_delegator :@body, :read
|
17
|
+
|
18
|
+
def_delegator :@body, :copy_to
|
19
|
+
|
20
|
+
def_delegator :@body, :close
|
21
|
+
|
22
|
+
def_delegator :@request, :uri
|
23
|
+
|
24
|
+
def initialize(request, status, version, headers, options = {})
|
25
|
+
@options = Options.new(options)
|
26
|
+
@version = version
|
27
|
+
@request = request
|
28
|
+
@status = Integer(status)
|
29
|
+
@headers = @options.headers_class.new(headers)
|
30
|
+
@body = @options.response_body_class.new(self, threshold_size: @options.body_threshold_size,
|
31
|
+
window_size: @options.window_size)
|
32
|
+
end
|
33
|
+
|
34
|
+
def merge_headers(h)
|
35
|
+
@headers = @headers.merge(h)
|
36
|
+
end
|
37
|
+
|
38
|
+
def <<(data)
|
39
|
+
@body.write(data)
|
40
|
+
end
|
41
|
+
|
42
|
+
def bodyless?
|
43
|
+
@request.verb == :head ||
|
44
|
+
@status < 200 ||
|
45
|
+
@status == 201 ||
|
46
|
+
@status == 204 ||
|
47
|
+
@status == 205 ||
|
48
|
+
@status == 304
|
49
|
+
end
|
50
|
+
|
51
|
+
def content_type
|
52
|
+
ContentType.parse(@headers["content-type"])
|
53
|
+
end
|
54
|
+
|
55
|
+
def complete?
|
56
|
+
bodyless? || (@request.verb == :connect && @status == 200)
|
57
|
+
end
|
58
|
+
|
59
|
+
def inspect
|
60
|
+
"#<Response:#{object_id} @status=#{@status} @headers=#{@headers}>"
|
61
|
+
end
|
62
|
+
|
63
|
+
class Body
|
64
|
+
def initialize(response, threshold_size:, window_size: 1 << 14)
|
65
|
+
@response = response
|
66
|
+
@headers = response.headers
|
67
|
+
@threshold_size = threshold_size
|
68
|
+
@window_size = window_size
|
69
|
+
@encoding = response.content_type.charset || Encoding::BINARY
|
70
|
+
@length = 0
|
71
|
+
@buffer = nil
|
72
|
+
@state = :idle
|
73
|
+
end
|
74
|
+
|
75
|
+
def write(chunk)
|
76
|
+
@length += chunk.bytesize
|
77
|
+
transition
|
78
|
+
@buffer.write(chunk)
|
79
|
+
end
|
80
|
+
|
81
|
+
def read(*args)
|
82
|
+
return unless @buffer
|
83
|
+
@buffer.read(*args)
|
84
|
+
end
|
85
|
+
|
86
|
+
def bytesize
|
87
|
+
@length
|
88
|
+
end
|
89
|
+
|
90
|
+
def each
|
91
|
+
return enum_for(__method__) unless block_given?
|
92
|
+
begin
|
93
|
+
unless @state == :idle
|
94
|
+
rewind
|
95
|
+
while (chunk = @buffer.read(@window_size))
|
96
|
+
yield(chunk)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
ensure
|
100
|
+
close
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_s
|
105
|
+
rewind
|
106
|
+
return @buffer.read.force_encoding(@encoding) if @buffer
|
107
|
+
""
|
108
|
+
ensure
|
109
|
+
close
|
110
|
+
end
|
111
|
+
alias_method :to_str, :to_s
|
112
|
+
|
113
|
+
def empty?
|
114
|
+
@length.zero?
|
115
|
+
end
|
116
|
+
|
117
|
+
def copy_to(dest)
|
118
|
+
return unless @buffer
|
119
|
+
if dest.respond_to?(:path) && @buffer.respond_to?(:path)
|
120
|
+
FileUtils.mv(@buffer.path, dest.path)
|
121
|
+
else
|
122
|
+
@buffer.rewind
|
123
|
+
::IO.copy_stream(@buffer, dest)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# closes/cleans the buffer, resets everything
|
128
|
+
def close
|
129
|
+
return if @state == :idle
|
130
|
+
@buffer.close
|
131
|
+
@buffer.unlink if @buffer.respond_to?(:unlink)
|
132
|
+
@buffer = nil
|
133
|
+
@length = 0
|
134
|
+
@state = :idle
|
135
|
+
end
|
136
|
+
|
137
|
+
def ==(other)
|
138
|
+
to_s == other.to_s
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def rewind
|
144
|
+
return if @state == :idle
|
145
|
+
@buffer.rewind
|
146
|
+
end
|
147
|
+
|
148
|
+
def transition
|
149
|
+
case @state
|
150
|
+
when :idle
|
151
|
+
if @length > @threshold_size
|
152
|
+
@state = :buffer
|
153
|
+
@buffer = Tempfile.new("httpx", encoding: @encoding, mode: File::RDWR)
|
154
|
+
else
|
155
|
+
@state = :memory
|
156
|
+
@buffer = StringIO.new("".b, File::RDWR)
|
157
|
+
end
|
158
|
+
when :memory
|
159
|
+
if @length > @threshold_size
|
160
|
+
aux = @buffer
|
161
|
+
@buffer = Tempfile.new("palanca", encoding: @encoding, mode: File::RDWR)
|
162
|
+
aux.rewind
|
163
|
+
::IO.copy_stream(aux, @buffer)
|
164
|
+
# TODO: remove this if/when minor ruby is 2.3
|
165
|
+
# (this looks like a bug from older versions)
|
166
|
+
@buffer.pos = aux.pos #######################
|
167
|
+
#############################################
|
168
|
+
aux.close
|
169
|
+
@state = :buffer
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
return unless %i[memory buffer].include?(@state)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class ContentType
|
179
|
+
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
|
180
|
+
CHARSET_RE = /;\s*charset=([^;]+)/i
|
181
|
+
|
182
|
+
attr_reader :mime_type, :charset
|
183
|
+
|
184
|
+
def initialize(mime_type, charset)
|
185
|
+
@mime_type = mime_type
|
186
|
+
@charset = charset
|
187
|
+
end
|
188
|
+
|
189
|
+
class << self
|
190
|
+
# Parse string and return ContentType struct
|
191
|
+
def parse(str)
|
192
|
+
new(mime_type(str), charset(str))
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
# :nodoc:
|
198
|
+
def mime_type(str)
|
199
|
+
m = str.to_s[MIME_TYPE_RE, 1]
|
200
|
+
m && m.strip.downcase
|
201
|
+
end
|
202
|
+
|
203
|
+
# :nodoc:
|
204
|
+
def charset(str)
|
205
|
+
m = str.to_s[CHARSET_RE, 1]
|
206
|
+
m && m.strip.delete('"')
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
class ErrorResponse
|
212
|
+
attr_reader :error, :retries
|
213
|
+
|
214
|
+
alias_method :status, :error
|
215
|
+
|
216
|
+
def initialize(error, retries)
|
217
|
+
@error = error
|
218
|
+
@retries = retries
|
219
|
+
end
|
220
|
+
|
221
|
+
def retryable?
|
222
|
+
@retries.positive?
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|