httpx 0.8.0 → 0.10.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.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +48 -0
  3. data/README.md +9 -5
  4. data/doc/release_notes/0_0_1.md +7 -0
  5. data/doc/release_notes/0_0_2.md +9 -0
  6. data/doc/release_notes/0_0_3.md +9 -0
  7. data/doc/release_notes/0_0_4.md +7 -0
  8. data/doc/release_notes/0_0_5.md +5 -0
  9. data/doc/release_notes/0_10_0.md +66 -0
  10. data/doc/release_notes/0_10_1.md +39 -0
  11. data/doc/release_notes/0_1_0.md +9 -0
  12. data/doc/release_notes/0_2_0.md +5 -0
  13. data/doc/release_notes/0_2_1.md +16 -0
  14. data/doc/release_notes/0_3_0.md +12 -0
  15. data/doc/release_notes/0_3_1.md +6 -0
  16. data/doc/release_notes/0_4_0.md +51 -0
  17. data/doc/release_notes/0_4_1.md +3 -0
  18. data/doc/release_notes/0_5_0.md +15 -0
  19. data/doc/release_notes/0_5_1.md +14 -0
  20. data/doc/release_notes/0_6_0.md +5 -0
  21. data/doc/release_notes/0_6_1.md +6 -0
  22. data/doc/release_notes/0_6_2.md +6 -0
  23. data/doc/release_notes/0_6_3.md +13 -0
  24. data/doc/release_notes/0_6_4.md +21 -0
  25. data/doc/release_notes/0_6_5.md +22 -0
  26. data/doc/release_notes/0_6_6.md +19 -0
  27. data/doc/release_notes/0_6_7.md +5 -0
  28. data/doc/release_notes/0_7_0.md +46 -0
  29. data/doc/release_notes/0_8_0.md +27 -0
  30. data/doc/release_notes/0_8_1.md +8 -0
  31. data/doc/release_notes/0_8_2.md +7 -0
  32. data/doc/release_notes/0_9_0.md +38 -0
  33. data/lib/httpx.rb +2 -0
  34. data/lib/httpx/adapters/faraday.rb +1 -1
  35. data/lib/httpx/chainable.rb +11 -11
  36. data/lib/httpx/connection.rb +23 -31
  37. data/lib/httpx/connection/http1.rb +30 -4
  38. data/lib/httpx/connection/http2.rb +29 -10
  39. data/lib/httpx/domain_name.rb +440 -0
  40. data/lib/httpx/errors.rb +2 -1
  41. data/lib/httpx/extensions.rb +22 -2
  42. data/lib/httpx/headers.rb +2 -2
  43. data/lib/httpx/io/ssl.rb +0 -1
  44. data/lib/httpx/io/tcp.rb +6 -5
  45. data/lib/httpx/io/udp.rb +4 -1
  46. data/lib/httpx/options.rb +5 -1
  47. data/lib/httpx/parser/http1.rb +14 -17
  48. data/lib/httpx/plugins/compression.rb +46 -65
  49. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  50. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  51. data/lib/httpx/plugins/compression/gzip.rb +23 -5
  52. data/lib/httpx/plugins/cookies.rb +21 -60
  53. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  54. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  55. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  56. data/lib/httpx/plugins/expect.rb +12 -1
  57. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  58. data/lib/httpx/plugins/h2c.rb +1 -1
  59. data/lib/httpx/plugins/multipart.rb +12 -6
  60. data/lib/httpx/plugins/persistent.rb +6 -1
  61. data/lib/httpx/plugins/proxy.rb +16 -2
  62. data/lib/httpx/plugins/proxy/socks4.rb +14 -14
  63. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  64. data/lib/httpx/plugins/retries.rb +3 -2
  65. data/lib/httpx/plugins/stream.rb +109 -13
  66. data/lib/httpx/pool.rb +14 -17
  67. data/lib/httpx/request.rb +8 -20
  68. data/lib/httpx/resolver.rb +7 -10
  69. data/lib/httpx/resolver/https.rb +22 -24
  70. data/lib/httpx/resolver/native.rb +19 -16
  71. data/lib/httpx/resolver/resolver_mixin.rb +4 -2
  72. data/lib/httpx/resolver/system.rb +2 -2
  73. data/lib/httpx/response.rb +16 -25
  74. data/lib/httpx/selector.rb +11 -18
  75. data/lib/httpx/session.rb +40 -26
  76. data/lib/httpx/transcoder.rb +18 -0
  77. data/lib/httpx/transcoder/chunker.rb +0 -2
  78. data/lib/httpx/transcoder/form.rb +9 -7
  79. data/lib/httpx/transcoder/json.rb +0 -4
  80. data/lib/httpx/utils.rb +45 -0
  81. data/lib/httpx/version.rb +1 -1
  82. data/sig/buffer.rbs +24 -0
  83. data/sig/callbacks.rbs +14 -0
  84. data/sig/chainable.rbs +37 -0
  85. data/sig/connection.rbs +85 -0
  86. data/sig/connection/http1.rbs +66 -0
  87. data/sig/connection/http2.rbs +78 -0
  88. data/sig/domain_name.rbs +17 -0
  89. data/sig/errors.rbs +3 -0
  90. data/sig/headers.rbs +42 -0
  91. data/sig/httpx.rbs +15 -0
  92. data/sig/loggable.rbs +11 -0
  93. data/sig/missing.rbs +12 -0
  94. data/sig/options.rbs +118 -0
  95. data/sig/parser/http1.rbs +50 -0
  96. data/sig/plugins/authentication.rbs +11 -0
  97. data/sig/plugins/basic_authentication.rbs +13 -0
  98. data/sig/plugins/compression.rbs +55 -0
  99. data/sig/plugins/compression/brotli.rbs +21 -0
  100. data/sig/plugins/compression/deflate.rbs +17 -0
  101. data/sig/plugins/compression/gzip.rbs +29 -0
  102. data/sig/plugins/cookies.rbs +26 -0
  103. data/sig/plugins/cookies/cookie.rbs +50 -0
  104. data/sig/plugins/cookies/jar.rbs +27 -0
  105. data/sig/plugins/digest_authentication.rbs +33 -0
  106. data/sig/plugins/expect.rbs +19 -0
  107. data/sig/plugins/follow_redirects.rbs +37 -0
  108. data/sig/plugins/h2c.rbs +26 -0
  109. data/sig/plugins/multipart.rbs +21 -0
  110. data/sig/plugins/persistent.rbs +17 -0
  111. data/sig/plugins/proxy.rbs +47 -0
  112. data/sig/plugins/proxy/http.rbs +14 -0
  113. data/sig/plugins/proxy/socks4.rbs +33 -0
  114. data/sig/plugins/proxy/socks5.rbs +36 -0
  115. data/sig/plugins/proxy/ssh.rbs +18 -0
  116. data/sig/plugins/push_promise.rbs +22 -0
  117. data/sig/plugins/rate_limiter.rbs +11 -0
  118. data/sig/plugins/retries.rbs +48 -0
  119. data/sig/plugins/stream.rbs +39 -0
  120. data/sig/pool.rbs +36 -0
  121. data/sig/registry.rbs +9 -0
  122. data/sig/request.rbs +61 -0
  123. data/sig/resolver.rbs +26 -0
  124. data/sig/resolver/https.rbs +49 -0
  125. data/sig/resolver/native.rbs +60 -0
  126. data/sig/resolver/resolver_mixin.rbs +27 -0
  127. data/sig/resolver/system.rbs +17 -0
  128. data/sig/response.rbs +87 -0
  129. data/sig/selector.rbs +20 -0
  130. data/sig/session.rbs +49 -0
  131. data/sig/timeout.rbs +29 -0
  132. data/sig/transcoder.rbs +18 -0
  133. data/sig/transcoder/body.rbs +18 -0
  134. data/sig/transcoder/chunker.rbs +32 -0
  135. data/sig/transcoder/form.rbs +16 -0
  136. data/sig/transcoder/json.rbs +14 -0
  137. metadata +128 -22
  138. data/lib/httpx/resolver/options.rb +0 -25
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+ require "time"
5
+
6
+ module HTTPX
7
+ module Plugins::Cookies
8
+ module SetCookieParser
9
+ using(RegexpExtensions) unless Regexp.method_defined?(:match?)
10
+
11
+ # Whitespace.
12
+ RE_WSP = /[ \t]+/.freeze
13
+
14
+ # A pattern that matches a cookie name or attribute name which may
15
+ # be empty, capturing trailing whitespace.
16
+ RE_NAME = /(?!#{RE_WSP})[^,;\\"=]*/.freeze
17
+
18
+ RE_BAD_CHAR = /([\x00-\x20\x7F",;\\])/.freeze
19
+
20
+ # A pattern that matches the comma in a (typically date) value.
21
+ RE_COOKIE_COMMA = /,(?=#{RE_WSP}?#{RE_NAME}=)/.freeze
22
+
23
+ module_function
24
+
25
+ def scan_dquoted(scanner)
26
+ s = +""
27
+
28
+ until scanner.eos?
29
+ break if scanner.skip(/"/)
30
+
31
+ if scanner.skip(/\\/)
32
+ s << scanner.getch
33
+ elsif scanner.scan(/[^"\\]+/)
34
+ s << scanner.matched
35
+ end
36
+ end
37
+
38
+ s
39
+ end
40
+
41
+ def scan_value(scanner, comma_as_separator = false)
42
+ value = +""
43
+
44
+ until scanner.eos?
45
+ if scanner.scan(/[^,;"]+/)
46
+ value << scanner.matched
47
+ elsif scanner.skip(/"/)
48
+ # RFC 6265 2.2
49
+ # A cookie-value may be DQUOTE'd.
50
+ value << scan_dquoted(scanner)
51
+ elsif scanner.check(/;/)
52
+ break
53
+ elsif comma_as_separator && scanner.check(RE_COOKIE_COMMA)
54
+ break
55
+ else
56
+ value << scanner.getch
57
+ end
58
+ end
59
+
60
+ value.rstrip!
61
+ value
62
+ end
63
+
64
+ def scan_name_value(scanner, comma_as_separator = false)
65
+ name = scanner.scan(RE_NAME)
66
+ name.rstrip! if name
67
+
68
+ if scanner.skip(/=/)
69
+ value = scan_value(scanner, comma_as_separator)
70
+ else
71
+ scan_value(scanner, comma_as_separator)
72
+ value = nil
73
+ end
74
+ [name, value]
75
+ end
76
+
77
+ def call(set_cookie)
78
+ scanner = StringScanner.new(set_cookie)
79
+
80
+ # RFC 6265 4.1.1 & 5.2
81
+ until scanner.eos?
82
+ start = scanner.pos
83
+ len = nil
84
+
85
+ scanner.skip(RE_WSP)
86
+
87
+ name, value = scan_name_value(scanner, true)
88
+ value = nil if name.empty?
89
+
90
+ attrs = {}
91
+
92
+ until scanner.eos?
93
+ if scanner.skip(/,/)
94
+ # The comma is used as separator for concatenating multiple
95
+ # values of a header.
96
+ len = (scanner.pos - 1) - start
97
+ break
98
+ elsif scanner.skip(/;/)
99
+ scanner.skip(RE_WSP)
100
+
101
+ aname, avalue = scan_name_value(scanner, true)
102
+
103
+ next if aname.empty? || value.nil?
104
+
105
+ aname.downcase!
106
+
107
+ case aname
108
+ when "expires"
109
+ # RFC 6265 5.2.1
110
+ (avalue &&= Time.httpdate(avalue)) || next
111
+ when "max-age"
112
+ # RFC 6265 5.2.2
113
+ next unless /\A-?\d+\z/.match?(avalue)
114
+
115
+ avalue = Integer(avalue)
116
+ when "domain"
117
+ # RFC 6265 5.2.3
118
+ # An empty value SHOULD be ignored.
119
+ next if avalue.nil? || avalue.empty?
120
+ when "path"
121
+ # RFC 6265 5.2.4
122
+ # A relative path must be ignored rather than normalizing it
123
+ # to "/".
124
+ next unless avalue.start_with?("/")
125
+ when "secure", "httponly"
126
+ # RFC 6265 5.2.5, 5.2.6
127
+ avalue = true
128
+ end
129
+ attrs[aname] = avalue
130
+ end
131
+ end
132
+
133
+ len ||= scanner.pos - start
134
+
135
+ next if len > Cookie::MAX_LENGTH
136
+
137
+ yield(name, value, attrs) if name && !name.empty? && value
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -18,14 +18,24 @@ module HTTPX
18
18
 
19
19
  seconds
20
20
  end
21
+
22
+ def_option(:expect_threshold_size) do |bytes|
23
+ bytes = Integer(bytes)
24
+ raise Error, ":expect_threshold_size must be positive" unless bytes.positive?
25
+
26
+ bytes
27
+ end
21
28
  end.new(options).merge(expect_timeout: EXPECT_TIMEOUT)
22
29
  end
23
30
 
24
31
  module RequestBodyMethods
25
- def initialize(*)
32
+ def initialize(*, options)
26
33
  super
27
34
  return if @body.nil?
28
35
 
36
+ threshold = options.expect_threshold_size
37
+ return if threshold && !unbounded_body? && @body.bytesize < threshold
38
+
29
39
  @headers["expect"] = "100-continue"
30
40
  end
31
41
  end
@@ -50,6 +60,7 @@ module HTTPX
50
60
  return unless response
51
61
 
52
62
  if response.status == 417 && request.headers.key?("expect")
63
+ response.close
53
64
  request.headers.delete("expect")
54
65
  request.transition(:idle)
55
66
  connection = find_connection(request, connections, options)
@@ -59,8 +59,26 @@ module HTTPX
59
59
  return ErrorResponse.new(request, error, options)
60
60
  end
61
61
 
62
- connection = find_connection(retry_request, connections, options)
63
- connection.send(retry_request)
62
+ retry_after = response.headers["retry-after"]
63
+
64
+ if retry_after
65
+ # Servers send the "Retry-After" header field to indicate how long the
66
+ # user agent ought to wait before making a follow-up request.
67
+ # When sent with any 3xx (Redirection) response, Retry-After indicates
68
+ # the minimum time that the user agent is asked to wait before issuing
69
+ # the redirected request.
70
+ #
71
+ retry_after = Utils.parse_retry_after(retry_after)
72
+
73
+ log { "redirecting after #{retry_after} secs..." }
74
+ pool.after(retry_after) do
75
+ connection = find_connection(retry_request, connections, options)
76
+ connection.send(retry_request)
77
+ end
78
+ else
79
+ connection = find_connection(retry_request, connections, options)
80
+ connection.send(retry_request)
81
+ end
64
82
  nil
65
83
  end
66
84
 
@@ -73,7 +73,7 @@ module HTTPX
73
73
 
74
74
  # clean up data left behind in the buffer, if the server started
75
75
  # sending frames
76
- data = response.to_s
76
+ data = response.read
77
77
  @connection << data
78
78
  end
79
79
  end
@@ -23,19 +23,25 @@ module HTTPX
23
23
  def_delegator :@raw, :read
24
24
 
25
25
  def initialize(form)
26
- @raw = HTTP::FormData.create(form)
26
+ @raw = if multipart?(form)
27
+ HTTP::FormData::Multipart.new(Hash[*form.map { |k, v| Transcoder.enum_for(:normalize_keys, k, v).to_a }])
28
+ else
29
+ HTTP::FormData::Urlencoded.new(form, :encoder => Transcoder::Form.method(:encode))
30
+ end
27
31
  end
28
32
 
29
33
  def bytesize
30
34
  @raw.content_length
31
35
  end
32
36
 
33
- def force_encoding(*args)
34
- @raw.to_s.force_encoding(*args)
35
- end
37
+ private
36
38
 
37
- def to_str
38
- @raw.to_s
39
+ def multipart?(data)
40
+ data.any? do |_, v|
41
+ v.is_a?(HTTP::FormData::Part) ||
42
+ (v.respond_to?(:to_ary) && v.to_ary.any? { |e| e.is_a?(HTTP::FormData::Part) }) ||
43
+ (v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| e.is_a?(HTTP::FormData::Part) })
44
+ end
39
45
  end
40
46
  end
41
47
 
@@ -19,7 +19,12 @@ module HTTPX
19
19
  #
20
20
  module Persistent
21
21
  def self.load_dependencies(klass)
22
- klass.plugin(:retries, max_retries: 1, retry_change_requests: true)
22
+ max_retries = if klass.default_options.respond_to?(:max_retries)
23
+ [klass.default_options.max_retries, 1].max
24
+ else
25
+ 1
26
+ end
27
+ klass.plugin(:retries, max_retries: max_retries, retry_change_requests: true)
23
28
  end
24
29
 
25
30
  def self.extra_options(options)
@@ -121,8 +121,7 @@ module HTTPX
121
121
  def fetch_response(request, connections, options)
122
122
  response = super
123
123
  if response.is_a?(ErrorResponse) &&
124
- # either it was a timeout error connecting, or it was a proxy error
125
- PROXY_ERRORS.any? { |ex| response.error.is_a?(ex) } && !@_proxy_uris.empty?
124
+ __proxy_error?(response) && !@_proxy_uris.empty?
126
125
  @_proxy_uris.shift
127
126
  log { "failed connecting to proxy, trying next..." }
128
127
  request.transition(:idle)
@@ -139,6 +138,21 @@ module HTTPX
139
138
 
140
139
  super
141
140
  end
141
+
142
+ def __proxy_error?(response)
143
+ error = response.error
144
+ case error
145
+ when ResolveError
146
+ # failed resolving proxy domain
147
+ proxy_uri = error.connection.options.proxy.uri
148
+ proxy_uri.to_s == @_proxy_uris.first
149
+ when *PROXY_ERRORS
150
+ # timeout errors connecting to proxy
151
+ true
152
+ else
153
+ false
154
+ end
155
+ end
142
156
  end
143
157
 
144
158
  module ConnectionMethods
@@ -10,7 +10,7 @@ module HTTPX
10
10
  module Socks4
11
11
  VERSION = 4
12
12
  CONNECT = 1
13
- GRANTED = 90
13
+ GRANTED = 0x5A
14
14
  PROTOCOLS = %w[socks4 socks4a].freeze
15
15
 
16
16
  Error = Socks4Error
@@ -95,21 +95,21 @@ module HTTPX
95
95
 
96
96
  def connect(parameters, uri)
97
97
  packet = [VERSION, CONNECT, uri.port].pack("CCn")
98
- begin
99
- ip = IPAddr.new(uri.host)
100
- raise Error, "Socks4 connection to #{ip} not supported" unless ip.ipv4?
101
-
102
- packet << [ip.to_i].pack("N")
103
- rescue IPAddr::InvalidAddressError
104
- if parameters.uri.scheme =~ /^socks4a?$/
105
- # resolv defaults to IPv4, and socks4 doesn't support IPv6 otherwise
106
- ip = IPAddr.new(Resolv.getaddress(uri.host))
107
- packet << [ip.to_i].pack("N")
108
- else
109
- packet << "\x0\x0\x0\x1" << "\x7\x0" << uri.host
98
+
99
+ case parameters.uri.scheme
100
+ when "socks4"
101
+ socks_host = uri.host
102
+ begin
103
+ ip = IPAddr.new(socks_host)
104
+ packet << ip.hton
105
+ rescue IPAddr::InvalidAddressError
106
+ socks_host = Resolv.getaddress(socks_host)
107
+ retry
110
108
  end
109
+ packet << [parameters.username].pack("Z*")
110
+ when "socks4a"
111
+ packet << "\x0\x0\x0\x1" << [parameters.username].pack("Z*") << uri.host << "\x0"
111
112
  end
112
- packet << [parameters.username].pack("Z*")
113
113
  packet
114
114
  end
115
115
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds support for retrying requests when the request:
7
+ #
8
+ # * is rate limited;
9
+ # * when the server is unavailable (503);
10
+ # * when a 3xx request comes with a "retry-after" value
11
+ #
12
+ # https://gitlab.com/honeyryderchuck/httpx/wikis/RateLimiter
13
+ #
14
+ module RateLimiter
15
+ class << self
16
+ RATE_LIMIT_CODES = [429, 503].freeze
17
+
18
+ def load_dependencies(klass)
19
+ klass.plugin(:retries,
20
+ retry_change_requests: true,
21
+ retry_on: method(:retry_on_rate_limited_response),
22
+ retry_after: method(:retry_after_rate_limit))
23
+ end
24
+
25
+ def retry_on_rate_limited_response(response)
26
+ status = response.status
27
+
28
+ RATE_LIMIT_CODES.include?(status)
29
+ end
30
+
31
+ # Servers send the "Retry-After" header field to indicate how long the
32
+ # user agent ought to wait before making a follow-up request. When
33
+ # sent with a 503 (Service Unavailable) response, Retry-After indicates
34
+ # how long the service is expected to be unavailable to the client.
35
+ # When sent with any 3xx (Redirection) response, Retry-After indicates
36
+ # the minimum time that the user agent is asked to wait before issuing
37
+ # the redirected request.
38
+ #
39
+ def retry_after_rate_limit(_, response)
40
+ retry_after = response.headers["retry-after"]
41
+
42
+ return unless retry_after
43
+
44
+ Utils.parse_retry_after(retry_after)
45
+ end
46
+ end
47
+ end
48
+
49
+ register_plugin :rate_limiter, RateLimiter
50
+ end
51
+ end
@@ -75,14 +75,15 @@ module HTTPX
75
75
  )
76
76
  # rubocop:enable Style/MultilineTernaryOperator
77
77
  )
78
-
78
+ response.close if response.respond_to?(:close)
79
79
  request.retries -= 1
80
80
  log { "failed to get response, #{request.retries} tries to go..." }
81
81
  request.transition(:idle)
82
82
 
83
83
  retry_after = options.retry_after
84
+ retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
85
+
84
86
  if retry_after
85
- retry_after = retry_after.call(request) if retry_after.respond_to?(:call)
86
87
 
87
88
  log { "retrying after #{retry_after} secs..." }
88
89
  pool.after(retry_after) do
@@ -7,27 +7,123 @@ module HTTPX
7
7
  #
8
8
  module Stream
9
9
  module InstanceMethods
10
- def stream
11
- headers("accept" => "text/event-stream",
12
- "cache-control" => "no-cache")
10
+ private
11
+
12
+ def request(*args, stream: false, **options)
13
+ return super(*args, **options) unless stream
14
+
15
+ requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
16
+
17
+ raise Error, "only 1 response at a time is supported for streaming requests" unless requests.size == 1
18
+
19
+ StreamResponse.new(requests.first, self)
13
20
  end
14
21
  end
15
22
 
23
+ module RequestMethods
24
+ attr_accessor :stream
25
+ end
26
+
16
27
  module ResponseMethods
17
- def complete?
18
- super ||
19
- stream? &&
20
- @stream_complete
28
+ def stream
29
+ @request.stream
30
+ end
31
+ end
32
+
33
+ module ResponseBodyMethods
34
+ def initialize(*, **)
35
+ super
36
+ @stream = @response.stream
37
+ end
38
+
39
+ def write(chunk)
40
+ return super unless @stream
41
+
42
+ @stream.on_chunk(chunk.to_s.dup)
43
+ end
44
+
45
+ private
46
+
47
+ def transition(*)
48
+ return if @stream
49
+
50
+ super
51
+ end
52
+ end
53
+
54
+ class StreamResponse
55
+ def initialize(request, session)
56
+ @request = request
57
+ @session = session
58
+ @options = @request.options
59
+ end
60
+
61
+ def each(&block)
62
+ return enum_for(__method__) unless block_given?
63
+
64
+ raise Error, "response already streamed" if @response
65
+
66
+ @request.stream = self
67
+
68
+ begin
69
+ @on_chunk = block
70
+
71
+ response.raise_for_status
72
+ response.close
73
+ ensure
74
+ @on_chunk = nil
75
+ end
76
+ end
77
+
78
+ def each_line
79
+ return enum_for(__method__) unless block_given?
80
+
81
+ line = +""
82
+
83
+ each do |chunk|
84
+ line << chunk
85
+
86
+ while (idx = line.index("\n"))
87
+ yield line.byteslice(0..idx - 1)
88
+
89
+ line = line.byteslice(idx + 1..-1)
90
+ end
91
+ end
92
+ end
93
+
94
+ # This is a ghost method. It's to be used ONLY internally, when processing streams
95
+ def on_chunk(chunk)
96
+ raise NoMethodError unless @on_chunk
97
+
98
+ @on_chunk.call(chunk)
99
+ end
100
+
101
+ # :nocov:
102
+ def inspect
103
+ "#<StreamResponse:#{object_id}>"
104
+ end
105
+ # :nocov:
106
+
107
+ def to_s
108
+ response.to_s
109
+ end
110
+
111
+ private
112
+
113
+ def response
114
+ @response ||= @session.__send__(:send_requests, @request, @options).first
21
115
  end
22
116
 
23
- def stream?
24
- @headers["content-type"].start_with?("text/event-stream")
117
+ def respond_to_missing?(*args)
118
+ @options.response_class.respond_to?(*args) || super
25
119
  end
26
120
 
27
- def <<(data)
28
- res = super
29
- @stream_complete = true if String(data).end_with?("\n\n")
30
- res
121
+ def method_missing(meth, *args, &block)
122
+ if @options.response_class.public_method_defined?(meth)
123
+ response.__send__(meth, *args, &block)
124
+ else
125
+ super
126
+ end
31
127
  end
32
128
  end
33
129
  end