httpx 0.0.1
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 +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
|