httpx 0.2.1 → 0.3.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/lib/httpx.rb +1 -0
- data/lib/httpx/altsvc.rb +76 -0
- data/lib/httpx/chainable.rb +4 -0
- data/lib/httpx/channel.rb +72 -20
- data/lib/httpx/channel/http1.rb +71 -52
- data/lib/httpx/channel/http2.rb +43 -5
- data/lib/httpx/client.rb +32 -1
- data/lib/httpx/connection.rb +21 -8
- data/lib/httpx/errors.rb +18 -1
- data/lib/httpx/extensions.rb +72 -41
- data/lib/httpx/parser/http1.rb +171 -0
- data/lib/httpx/plugins/multipart.rb +50 -0
- data/lib/httpx/plugins/proxy.rb +6 -1
- data/lib/httpx/request.rb +8 -3
- data/lib/httpx/response.rb +15 -6
- data/lib/httpx/selector.rb +1 -1
- data/lib/httpx/timeout.rb +37 -8
- data/lib/httpx/transcoder/chunker.rb +76 -3
- data/lib/httpx/transcoder/form.rb +7 -11
- data/lib/httpx/version.rb +1 -1
- metadata +7 -18
data/lib/httpx/channel/http2.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "io/wait"
|
3
4
|
require "http/2"
|
4
5
|
|
5
6
|
module HTTPX
|
@@ -7,6 +8,22 @@ module HTTPX
|
|
7
8
|
include Callbacks
|
8
9
|
include Loggable
|
9
10
|
|
11
|
+
if HTTP2::VERSION < "0.10.1"
|
12
|
+
module HTTP2Extensions
|
13
|
+
refine ::HTTP2::Client do
|
14
|
+
def receive(*)
|
15
|
+
send_connection_preface
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def <<(*args)
|
20
|
+
receive(*args)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
using HTTP2Extensions
|
25
|
+
end
|
26
|
+
|
10
27
|
Error = Class.new(Error) do
|
11
28
|
def initialize(id, code)
|
12
29
|
super("stream #{id} closed with error: #{code}")
|
@@ -18,11 +35,12 @@ module HTTPX
|
|
18
35
|
def initialize(buffer, options)
|
19
36
|
@options = Options.new(options)
|
20
37
|
@max_concurrent_requests = @options.max_concurrent_requests
|
21
|
-
init_connection
|
22
38
|
@pending = []
|
23
39
|
@streams = {}
|
24
40
|
@drains = {}
|
25
41
|
@buffer = buffer
|
42
|
+
@handshake_completed = false
|
43
|
+
init_connection
|
26
44
|
end
|
27
45
|
|
28
46
|
def close
|
@@ -38,7 +56,8 @@ module HTTPX
|
|
38
56
|
end
|
39
57
|
|
40
58
|
def send(request, **)
|
41
|
-
if
|
59
|
+
if !@handshake_completed ||
|
60
|
+
@connection.active_stream_count >= @max_concurrent_requests
|
42
61
|
@pending << request
|
43
62
|
return
|
44
63
|
end
|
@@ -48,6 +67,7 @@ module HTTPX
|
|
48
67
|
@streams[request] = stream
|
49
68
|
end
|
50
69
|
handle(request, stream)
|
70
|
+
true
|
51
71
|
end
|
52
72
|
|
53
73
|
def reenqueue!
|
@@ -73,6 +93,12 @@ module HTTPX
|
|
73
93
|
|
74
94
|
private
|
75
95
|
|
96
|
+
def send_pending
|
97
|
+
while (request = @pending.shift)
|
98
|
+
break unless send(request)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
76
102
|
def headline_uri(request)
|
77
103
|
request.path
|
78
104
|
end
|
@@ -95,9 +121,16 @@ module HTTPX
|
|
95
121
|
@connection.on(:frame_sent, &method(:on_frame_sent))
|
96
122
|
@connection.on(:frame_received, &method(:on_frame_received))
|
97
123
|
@connection.on(:promise, &method(:on_promise))
|
98
|
-
@connection.on(:altsvc
|
124
|
+
@connection.on(:altsvc) { |frame| on_altsvc(frame[:origin], frame) }
|
99
125
|
@connection.on(:settings_ack, &method(:on_settings))
|
100
126
|
@connection.on(:goaway, &method(:on_close))
|
127
|
+
#
|
128
|
+
# Some servers initiate HTTP/2 negotiation right away, some don't.
|
129
|
+
# As such, we have to check the socket buffer. If there is something
|
130
|
+
# to read, the server initiated the negotiation. If not, we have to
|
131
|
+
# initiate it.
|
132
|
+
#
|
133
|
+
@connection.send_connection_preface
|
101
134
|
end
|
102
135
|
|
103
136
|
def handle_stream(stream, request)
|
@@ -105,7 +138,7 @@ module HTTPX
|
|
105
138
|
stream.on(:half_close) do
|
106
139
|
log(level: 2, label: "#{stream.id}: ") { "waiting for response..." }
|
107
140
|
end
|
108
|
-
|
141
|
+
stream.on(:altsvc, &method(:on_altsvc).curry[request.origin])
|
109
142
|
stream.on(:headers, &method(:on_stream_headers).curry[stream, request])
|
110
143
|
stream.on(:data, &method(:on_stream_data).curry[stream, request])
|
111
144
|
end
|
@@ -187,8 +220,10 @@ module HTTPX
|
|
187
220
|
end
|
188
221
|
|
189
222
|
def on_settings(*)
|
223
|
+
@handshake_completed = true
|
190
224
|
@max_concurrent_requests = [@max_concurrent_requests,
|
191
225
|
@connection.remote_settings[:settings_max_concurrent_streams]].min
|
226
|
+
send_pending
|
192
227
|
end
|
193
228
|
|
194
229
|
def on_close(_last_frame, error, _payload)
|
@@ -225,9 +260,12 @@ module HTTPX
|
|
225
260
|
end
|
226
261
|
end
|
227
262
|
|
228
|
-
def on_altsvc(frame)
|
263
|
+
def on_altsvc(origin, frame)
|
229
264
|
log(level: 2, label: "#{frame[:stream]}: ") { "altsvc frame was received" }
|
230
265
|
log(level: 2, label: "#{frame[:stream]}: ") { frame.inspect }
|
266
|
+
alt_origin = URI.parse("#{frame[:proto]}://#{frame[:host]}:#{frame[:port]}")
|
267
|
+
params = { "ma" => frame[:max_age] }
|
268
|
+
emit(:altsvc, origin, alt_origin, origin, params)
|
231
269
|
end
|
232
270
|
|
233
271
|
def on_promise(stream)
|
data/lib/httpx/client.rb
CHANGED
@@ -47,7 +47,6 @@ module HTTPX
|
|
47
47
|
def on_promise(_, stream)
|
48
48
|
log(level: 2, label: "#{stream.id}: ") { "refusing stream!" }
|
49
49
|
stream.refuse
|
50
|
-
# TODO: policy for handling promises
|
51
50
|
end
|
52
51
|
|
53
52
|
def fetch_response(request)
|
@@ -66,6 +65,9 @@ module HTTPX
|
|
66
65
|
other_channel = build_channel(uncoalesced_uri, options)
|
67
66
|
channel.unmerge(other_channel)
|
68
67
|
end
|
68
|
+
channel.on(:altsvc) do |alt_origin, origin, alt_params|
|
69
|
+
build_altsvc_channel(channel, alt_origin, origin, alt_params, options)
|
70
|
+
end
|
69
71
|
end
|
70
72
|
|
71
73
|
def build_channel(uri, options)
|
@@ -74,6 +76,35 @@ module HTTPX
|
|
74
76
|
channel
|
75
77
|
end
|
76
78
|
|
79
|
+
def build_altsvc_channel(existing_channel, alt_origin, origin, alt_params, options)
|
80
|
+
altsvc = AltSvc.cached_altsvc_set(origin, alt_params.merge("origin" => alt_origin))
|
81
|
+
|
82
|
+
# altsvc already exists, somehow it wasn't advertised, probably noop
|
83
|
+
return unless altsvc
|
84
|
+
|
85
|
+
channel = @connection.find_channel(alt_origin) || build_channel(alt_origin, options)
|
86
|
+
# advertised altsvc is the same origin being used, ignore
|
87
|
+
return if channel == existing_channel
|
88
|
+
|
89
|
+
log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
|
90
|
+
|
91
|
+
# get uninitialized requests
|
92
|
+
# incidentally, all requests will be re-routed to the first
|
93
|
+
# advertised alt-svc, which incidentally follows the spec.
|
94
|
+
existing_channel.purge_pending do |request, args|
|
95
|
+
is_idle = request.origin == origin &&
|
96
|
+
request.state == :idle &&
|
97
|
+
!request.headers.key?("alt-used")
|
98
|
+
if is_idle
|
99
|
+
log(level: 1) { "#{origin} alt-svc: sending #{request.uri} to #{alt_origin}" }
|
100
|
+
channel.send(request, args)
|
101
|
+
end
|
102
|
+
is_idle
|
103
|
+
end
|
104
|
+
rescue UnsupportedSchemeError
|
105
|
+
altsvc["noop"] = true
|
106
|
+
end
|
107
|
+
|
77
108
|
def __build_reqs(*args, **options)
|
78
109
|
requests = case args.size
|
79
110
|
when 1
|
data/lib/httpx/connection.rb
CHANGED
@@ -13,6 +13,7 @@ module HTTPX
|
|
13
13
|
resolver_type = Resolver.registry(resolver_type) if resolver_type.is_a?(Symbol)
|
14
14
|
@selector = Selector.new
|
15
15
|
@channels = []
|
16
|
+
@connected_channels = 0
|
16
17
|
@resolver = resolver_type.new(self, @options)
|
17
18
|
@resolver.on(:resolve, &method(:on_resolver_channel))
|
18
19
|
@resolver.on(:error, &method(:on_resolver_error))
|
@@ -32,8 +33,11 @@ module HTTPX
|
|
32
33
|
monitor.interests = channel.interests
|
33
34
|
end
|
34
35
|
end
|
35
|
-
rescue TimeoutError
|
36
|
-
|
36
|
+
rescue TimeoutError => timeout_error
|
37
|
+
@channels.each do |ch|
|
38
|
+
ch.handle_timeout_error(timeout_error)
|
39
|
+
end
|
40
|
+
rescue Errno::ECONNRESET,
|
37
41
|
Errno::ECONNABORTED,
|
38
42
|
Errno::EPIPE => ex
|
39
43
|
@channels.each do |ch|
|
@@ -50,6 +54,13 @@ module HTTPX
|
|
50
54
|
def build_channel(uri, **options)
|
51
55
|
channel = Channel.by(uri, @options.merge(options))
|
52
56
|
resolve_channel(channel)
|
57
|
+
channel.on(:open) do
|
58
|
+
@connected_channels += 1
|
59
|
+
@timeout.transition(:open) if @channels.size == @connected_channels
|
60
|
+
end
|
61
|
+
channel.on(:reset) do
|
62
|
+
@timeout.transition(:idle)
|
63
|
+
end
|
53
64
|
channel.once(:unreachable) do
|
54
65
|
@resolver.uncache(channel)
|
55
66
|
resolve_channel(channel)
|
@@ -107,6 +118,7 @@ module HTTPX
|
|
107
118
|
end
|
108
119
|
|
109
120
|
def register_channel(channel)
|
121
|
+
@timeout.transition(:idle)
|
110
122
|
monitor = @selector.register(channel, :w)
|
111
123
|
monitor.value = channel
|
112
124
|
channel.on(:close) do
|
@@ -117,12 +129,7 @@ module HTTPX
|
|
117
129
|
def unregister_channel(channel)
|
118
130
|
@channels.delete(channel)
|
119
131
|
@selector.deregister(channel)
|
120
|
-
|
121
|
-
|
122
|
-
def next_timeout
|
123
|
-
timeout = @timeout.timeout # force log time
|
124
|
-
return (@resolver.timeout || timeout) unless @resolver.closed?
|
125
|
-
timeout
|
132
|
+
@connected_channels -= 1
|
126
133
|
end
|
127
134
|
|
128
135
|
def coalesce_channels(ch1, ch2)
|
@@ -133,5 +140,11 @@ module HTTPX
|
|
133
140
|
register_channel(ch2)
|
134
141
|
end
|
135
142
|
end
|
143
|
+
|
144
|
+
def next_timeout
|
145
|
+
timeout = @timeout.timeout
|
146
|
+
return (@resolver.timeout || timeout) unless @resolver.closed?
|
147
|
+
timeout
|
148
|
+
end
|
136
149
|
end
|
137
150
|
end
|
data/lib/httpx/errors.rb
CHANGED
@@ -3,7 +3,24 @@
|
|
3
3
|
module HTTPX
|
4
4
|
Error = Class.new(StandardError)
|
5
5
|
|
6
|
-
|
6
|
+
UnsupportedSchemeError = Class.new(Error)
|
7
|
+
|
8
|
+
TimeoutError = Class.new(Error) do
|
9
|
+
attr_reader :timeout
|
10
|
+
|
11
|
+
def initialize(timeout, message)
|
12
|
+
@timeout = timeout
|
13
|
+
super(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_connection_error
|
17
|
+
ex = ConnectTimeoutError.new(@timeout, message)
|
18
|
+
ex.set_backtrace(backtrace)
|
19
|
+
ex
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
ConnectTimeoutError = Class.new(TimeoutError)
|
7
24
|
|
8
25
|
ResolveError = Class.new(Error)
|
9
26
|
|
data/lib/httpx/extensions.rb
CHANGED
@@ -1,52 +1,83 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
#
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module HTTPX
|
6
|
+
unless Method.method_defined?(:curry)
|
7
|
+
|
8
|
+
# Backport
|
9
|
+
#
|
10
|
+
# Ruby 2.1 and lower implement curry only for Procs.
|
11
|
+
#
|
12
|
+
# Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
|
13
13
|
#
|
14
|
-
|
15
|
-
|
14
|
+
module CurryMethods # :nodoc:
|
15
|
+
# Backport for the Method#curry method, which is part of ruby core since 2.2 .
|
16
|
+
#
|
17
|
+
def curry(*args)
|
18
|
+
to_proc.curry(*args)
|
19
|
+
end
|
16
20
|
end
|
21
|
+
Method.__send__(:include, CurryMethods)
|
17
22
|
end
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
frozen? ? dup : self
|
23
|
+
|
24
|
+
unless String.method_defined?(:+@)
|
25
|
+
# Backport for +"", to initialize unfrozen strings from the string literal.
|
26
|
+
#
|
27
|
+
module LiteralStringExtensions
|
28
|
+
def +@
|
29
|
+
frozen? ? dup : self
|
30
|
+
end
|
27
31
|
end
|
32
|
+
String.__send__(:include, LiteralStringExtensions)
|
28
33
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
self > 0
|
34
|
+
|
35
|
+
unless Numeric.method_defined?(:positive?)
|
36
|
+
# Ruby 2.3 Backport (Numeric#positive?)
|
37
|
+
#
|
38
|
+
module PosMethods
|
39
|
+
def positive?
|
40
|
+
self > 0
|
41
|
+
end
|
38
42
|
end
|
43
|
+
Numeric.__send__(:include, PosMethods)
|
39
44
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
45
|
+
|
46
|
+
unless Numeric.method_defined?(:negative?)
|
47
|
+
# Ruby 2.3 Backport (Numeric#negative?)
|
48
|
+
#
|
49
|
+
module NegMethods
|
50
|
+
def negative?
|
51
|
+
self < 0
|
52
|
+
end
|
53
|
+
end
|
54
|
+
Numeric.__send__(:include, NegMethods)
|
55
|
+
end
|
56
|
+
|
57
|
+
module URIExtensions
|
58
|
+
refine URI::Generic do
|
59
|
+
def authority
|
60
|
+
port_string = port == default_port ? nil : ":#{port}"
|
61
|
+
"#{host}#{port_string}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def origin
|
65
|
+
"#{scheme}://#{authority}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def altsvc_match?(uri)
|
69
|
+
uri = URI.parse(uri)
|
70
|
+
self == uri || begin
|
71
|
+
case scheme
|
72
|
+
when 'h2'
|
73
|
+
uri.scheme == "https" &&
|
74
|
+
host == uri.host &&
|
75
|
+
(port || default_port) == (uri.port || uri.default_port)
|
76
|
+
else
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
49
81
|
end
|
50
82
|
end
|
51
|
-
|
52
|
-
end
|
83
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Parser
|
5
|
+
Error = Class.new(Error)
|
6
|
+
|
7
|
+
class HTTP1
|
8
|
+
VERSIONS = %w[1.0 1.1].freeze
|
9
|
+
|
10
|
+
attr_reader :status_code, :http_version, :headers
|
11
|
+
|
12
|
+
def initialize(observer, header_separator: ":")
|
13
|
+
@observer = observer
|
14
|
+
@state = :idle
|
15
|
+
@header_separator = header_separator
|
16
|
+
@buffer = "".b
|
17
|
+
@headers = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(chunk)
|
21
|
+
@buffer << chunk
|
22
|
+
parse
|
23
|
+
end
|
24
|
+
|
25
|
+
def reset!
|
26
|
+
@state = :idle
|
27
|
+
@headers.clear
|
28
|
+
@content_length = nil
|
29
|
+
@_has_trailers = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def upgrade?
|
33
|
+
@upgrade
|
34
|
+
end
|
35
|
+
|
36
|
+
def upgrade_data
|
37
|
+
@buffer
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def parse
|
43
|
+
state = @state
|
44
|
+
case @state
|
45
|
+
when :idle
|
46
|
+
parse_headline
|
47
|
+
when :headers
|
48
|
+
parse_headers
|
49
|
+
when :trailers
|
50
|
+
parse_headers
|
51
|
+
when :data
|
52
|
+
parse_data
|
53
|
+
end
|
54
|
+
parse if !@buffer.empty? && state != @state
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_headline
|
58
|
+
idx = @buffer.index("\n")
|
59
|
+
return unless idx
|
60
|
+
(m = %r{\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?}in.match(@buffer)) ||
|
61
|
+
raise(Error, "wrong head line format")
|
62
|
+
version, code, _ = m.captures
|
63
|
+
raise(Error, "unsupported HTTP version (HTTP/#{version})") unless VERSIONS.include?(version)
|
64
|
+
@http_version = version.split(".").map(&:to_i)
|
65
|
+
@status_code = code.to_i
|
66
|
+
raise(Error, "wrong status code (#{@status_code})") unless (100..599).cover?(@status_code)
|
67
|
+
@buffer.slice!(0, idx + 1)
|
68
|
+
nextstate(:headers)
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_headers
|
72
|
+
headers = @headers
|
73
|
+
while (idx = @buffer.index("\n"))
|
74
|
+
line = @buffer.slice!(0, idx + 1).sub(/\s+\z/, "")
|
75
|
+
if line.empty?
|
76
|
+
case @state
|
77
|
+
when :headers
|
78
|
+
prepare_data(headers)
|
79
|
+
@observer.on_headers(headers)
|
80
|
+
return unless @state == :headers
|
81
|
+
# state might have been reset
|
82
|
+
# in the :headers callback
|
83
|
+
nextstate(:data)
|
84
|
+
headers.clear
|
85
|
+
when :trailers
|
86
|
+
@observer.on_trailers(headers)
|
87
|
+
headers.clear
|
88
|
+
nextstate(:complete)
|
89
|
+
else
|
90
|
+
raise Error, "wrong header format"
|
91
|
+
end
|
92
|
+
return
|
93
|
+
end
|
94
|
+
separator_index = line.index(@header_separator)
|
95
|
+
raise Error, "wrong header format" unless separator_index
|
96
|
+
key = line[0..separator_index - 1]
|
97
|
+
raise Error, "wrong header format" if key.start_with?("\s", "\t")
|
98
|
+
key.strip!
|
99
|
+
value = line[separator_index + 1..-1]
|
100
|
+
value.strip!
|
101
|
+
raise Error, "wrong header format" if value.nil?
|
102
|
+
(headers[key.downcase] ||= []) << value
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse_data
|
107
|
+
if @buffer.respond_to?(:each)
|
108
|
+
@buffer.each do |chunk|
|
109
|
+
@observer.on_data(chunk)
|
110
|
+
end
|
111
|
+
elsif @content_length
|
112
|
+
data = @buffer.slice!(0, @content_length)
|
113
|
+
@content_length -= data.bytesize
|
114
|
+
@observer.on_data(data)
|
115
|
+
data.clear
|
116
|
+
else
|
117
|
+
@observer.on_data(@buffer)
|
118
|
+
@buffer.clear
|
119
|
+
end
|
120
|
+
return unless no_more_data?
|
121
|
+
@buffer = @buffer.to_s
|
122
|
+
if @_has_trailers
|
123
|
+
nextstate(:trailers)
|
124
|
+
else
|
125
|
+
nextstate(:complete)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def prepare_data(headers)
|
130
|
+
@upgrade = headers.key?("upgrade")
|
131
|
+
|
132
|
+
@_has_trailers = headers.key?("trailer")
|
133
|
+
|
134
|
+
if (tr_encodings = headers["transfer-encoding"])
|
135
|
+
tr_encodings.reverse_each do |tr_encoding|
|
136
|
+
tr_encoding.split(/ *, */).each do |encoding|
|
137
|
+
case encoding
|
138
|
+
when "chunked"
|
139
|
+
@buffer = Transcoder::Chunker::Decoder.new(@buffer, @_has_trailers)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
else
|
144
|
+
@content_length = headers["content-length"][0].to_i if headers.key?("content-length")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def no_more_data?
|
149
|
+
if @content_length
|
150
|
+
@content_length <= 0
|
151
|
+
elsif @buffer.respond_to?(:finished?)
|
152
|
+
@buffer.finished?
|
153
|
+
else
|
154
|
+
false
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def nextstate(state)
|
159
|
+
@state = state
|
160
|
+
case state
|
161
|
+
when :headers
|
162
|
+
@observer.on_start
|
163
|
+
when :complete
|
164
|
+
@observer.on_complete
|
165
|
+
reset!
|
166
|
+
nextstate(:idle) unless @buffer.empty?
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|