httpx 0.20.0 → 1.3.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 (250) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +0 -48
  3. data/README.md +54 -45
  4. data/doc/release_notes/0_10_0.md +2 -2
  5. data/doc/release_notes/0_11_0.md +3 -5
  6. data/doc/release_notes/0_12_0.md +5 -5
  7. data/doc/release_notes/0_13_0.md +5 -5
  8. data/doc/release_notes/0_14_0.md +2 -2
  9. data/doc/release_notes/0_16_0.md +3 -3
  10. data/doc/release_notes/0_17_0.md +1 -1
  11. data/doc/release_notes/0_18_0.md +4 -4
  12. data/doc/release_notes/0_18_2.md +1 -1
  13. data/doc/release_notes/0_19_0.md +1 -1
  14. data/doc/release_notes/0_19_8.md +1 -1
  15. data/doc/release_notes/0_20_0.md +2 -2
  16. data/doc/release_notes/0_20_1.md +5 -0
  17. data/doc/release_notes/0_20_2.md +7 -0
  18. data/doc/release_notes/0_20_3.md +6 -0
  19. data/doc/release_notes/0_20_4.md +17 -0
  20. data/doc/release_notes/0_20_5.md +3 -0
  21. data/doc/release_notes/0_21_0.md +96 -0
  22. data/doc/release_notes/0_21_1.md +12 -0
  23. data/doc/release_notes/0_22_0.md +13 -0
  24. data/doc/release_notes/0_22_1.md +11 -0
  25. data/doc/release_notes/0_22_2.md +5 -0
  26. data/doc/release_notes/0_22_3.md +55 -0
  27. data/doc/release_notes/0_22_4.md +6 -0
  28. data/doc/release_notes/0_22_5.md +6 -0
  29. data/doc/release_notes/0_23_0.md +42 -0
  30. data/doc/release_notes/0_23_1.md +5 -0
  31. data/doc/release_notes/0_23_2.md +5 -0
  32. data/doc/release_notes/0_23_3.md +6 -0
  33. data/doc/release_notes/0_23_4.md +5 -0
  34. data/doc/release_notes/0_24_0.md +48 -0
  35. data/doc/release_notes/0_24_1.md +12 -0
  36. data/doc/release_notes/0_24_2.md +12 -0
  37. data/doc/release_notes/0_24_3.md +12 -0
  38. data/doc/release_notes/0_24_4.md +18 -0
  39. data/doc/release_notes/0_24_5.md +6 -0
  40. data/doc/release_notes/0_24_6.md +5 -0
  41. data/doc/release_notes/0_24_7.md +10 -0
  42. data/doc/release_notes/1_0_0.md +60 -0
  43. data/doc/release_notes/1_0_1.md +5 -0
  44. data/doc/release_notes/1_0_2.md +7 -0
  45. data/doc/release_notes/1_1_0.md +32 -0
  46. data/doc/release_notes/1_1_1.md +17 -0
  47. data/doc/release_notes/1_1_2.md +12 -0
  48. data/doc/release_notes/1_1_3.md +18 -0
  49. data/doc/release_notes/1_1_4.md +6 -0
  50. data/doc/release_notes/1_1_5.md +12 -0
  51. data/doc/release_notes/1_2_0.md +49 -0
  52. data/doc/release_notes/1_2_1.md +6 -0
  53. data/doc/release_notes/1_2_2.md +10 -0
  54. data/doc/release_notes/1_2_3.md +16 -0
  55. data/doc/release_notes/1_2_4.md +8 -0
  56. data/doc/release_notes/1_2_5.md +7 -0
  57. data/doc/release_notes/1_2_6.md +13 -0
  58. data/doc/release_notes/1_3_0.md +18 -0
  59. data/doc/release_notes/1_3_1.md +17 -0
  60. data/lib/httpx/adapters/datadog.rb +215 -122
  61. data/lib/httpx/adapters/faraday.rb +145 -107
  62. data/lib/httpx/adapters/sentry.rb +26 -7
  63. data/lib/httpx/adapters/webmock.rb +34 -18
  64. data/lib/httpx/altsvc.rb +63 -26
  65. data/lib/httpx/base64.rb +27 -0
  66. data/lib/httpx/buffer.rb +12 -0
  67. data/lib/httpx/callbacks.rb +5 -3
  68. data/lib/httpx/chainable.rb +54 -39
  69. data/lib/httpx/connection/http1.rb +75 -44
  70. data/lib/httpx/connection/http2.rb +31 -38
  71. data/lib/httpx/connection.rb +287 -117
  72. data/lib/httpx/domain_name.rb +10 -13
  73. data/lib/httpx/errors.rb +52 -2
  74. data/lib/httpx/extensions.rb +24 -131
  75. data/lib/httpx/io/ssl.rb +83 -77
  76. data/lib/httpx/io/tcp.rb +48 -71
  77. data/lib/httpx/io/udp.rb +18 -52
  78. data/lib/httpx/io/unix.rb +10 -15
  79. data/lib/httpx/io.rb +3 -9
  80. data/lib/httpx/loggable.rb +4 -19
  81. data/lib/httpx/options.rb +176 -118
  82. data/lib/httpx/parser/http1.rb +4 -0
  83. data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
  84. data/lib/httpx/plugins/{authentication → auth}/digest.rb +14 -14
  85. data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
  86. data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
  87. data/lib/httpx/plugins/auth.rb +25 -0
  88. data/lib/httpx/plugins/aws_sdk_authentication.rb +4 -3
  89. data/lib/httpx/plugins/aws_sigv4.rb +12 -9
  90. data/lib/httpx/plugins/basic_auth.rb +29 -0
  91. data/lib/httpx/plugins/brotli.rb +50 -0
  92. data/lib/httpx/plugins/callbacks.rb +91 -0
  93. data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
  94. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
  95. data/lib/httpx/plugins/circuit_breaker.rb +148 -0
  96. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
  97. data/lib/httpx/plugins/cookies.rb +30 -17
  98. data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +14 -12
  99. data/lib/httpx/plugins/expect.rb +21 -14
  100. data/lib/httpx/plugins/follow_redirects.rb +140 -41
  101. data/lib/httpx/plugins/grpc/call.rb +2 -3
  102. data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
  103. data/lib/httpx/plugins/grpc/message.rb +7 -37
  104. data/lib/httpx/plugins/grpc.rb +36 -29
  105. data/lib/httpx/plugins/h2c.rb +26 -19
  106. data/lib/httpx/plugins/internal_telemetry.rb +16 -0
  107. data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
  108. data/lib/httpx/plugins/oauth.rb +175 -0
  109. data/lib/httpx/plugins/persistent.rb +1 -1
  110. data/lib/httpx/plugins/proxy/http.rb +23 -13
  111. data/lib/httpx/plugins/proxy/socks4.rb +9 -7
  112. data/lib/httpx/plugins/proxy/socks5.rb +11 -9
  113. data/lib/httpx/plugins/proxy.rb +80 -61
  114. data/lib/httpx/plugins/push_promise.rb +1 -1
  115. data/lib/httpx/plugins/rate_limiter.rb +5 -1
  116. data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
  117. data/lib/httpx/plugins/response_cache/store.rb +62 -25
  118. data/lib/httpx/plugins/response_cache.rb +105 -12
  119. data/lib/httpx/plugins/retries.rb +87 -17
  120. data/lib/httpx/plugins/ssrf_filter.rb +145 -0
  121. data/lib/httpx/plugins/stream.rb +27 -23
  122. data/lib/httpx/plugins/upgrade/h2.rb +4 -4
  123. data/lib/httpx/plugins/upgrade.rb +8 -10
  124. data/lib/httpx/plugins/webdav.rb +80 -0
  125. data/lib/httpx/pool/synch_pool.rb +93 -0
  126. data/lib/httpx/pool.rb +102 -27
  127. data/lib/httpx/punycode.rb +9 -291
  128. data/lib/httpx/request/body.rb +154 -0
  129. data/lib/httpx/request.rb +130 -146
  130. data/lib/httpx/resolver/https.rb +62 -27
  131. data/lib/httpx/resolver/multi.rb +9 -13
  132. data/lib/httpx/resolver/native.rb +192 -76
  133. data/lib/httpx/resolver/resolver.rb +34 -9
  134. data/lib/httpx/resolver/system.rb +16 -11
  135. data/lib/httpx/resolver.rb +38 -16
  136. data/lib/httpx/response/body.rb +242 -0
  137. data/lib/httpx/response/buffer.rb +96 -0
  138. data/lib/httpx/response.rb +159 -217
  139. data/lib/httpx/selector.rb +9 -4
  140. data/lib/httpx/session.rb +137 -89
  141. data/lib/httpx/session_extensions.rb +4 -1
  142. data/lib/httpx/timers.rb +34 -8
  143. data/lib/httpx/transcoder/body.rb +0 -2
  144. data/lib/httpx/transcoder/chunker.rb +0 -1
  145. data/lib/httpx/transcoder/deflate.rb +37 -0
  146. data/lib/httpx/transcoder/form.rb +52 -33
  147. data/lib/httpx/transcoder/gzip.rb +74 -0
  148. data/lib/httpx/transcoder/json.rb +21 -8
  149. data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
  150. data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +4 -4
  151. data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
  152. data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
  153. data/lib/httpx/transcoder/multipart.rb +17 -0
  154. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  155. data/lib/httpx/transcoder/utils/deflater.rb +72 -0
  156. data/lib/httpx/transcoder/utils/inflater.rb +19 -0
  157. data/lib/httpx/transcoder/xml.rb +52 -0
  158. data/lib/httpx/transcoder.rb +5 -6
  159. data/lib/httpx/utils.rb +36 -16
  160. data/lib/httpx/version.rb +1 -1
  161. data/lib/httpx.rb +12 -14
  162. data/sig/altsvc.rbs +33 -0
  163. data/sig/buffer.rbs +2 -1
  164. data/sig/callbacks.rbs +3 -3
  165. data/sig/chainable.rbs +11 -9
  166. data/sig/connection/http1.rbs +8 -7
  167. data/sig/connection/http2.rbs +19 -19
  168. data/sig/connection.rbs +64 -24
  169. data/sig/errors.rbs +22 -3
  170. data/sig/httpx.rbs +5 -4
  171. data/sig/io/ssl.rbs +27 -0
  172. data/sig/io/tcp.rbs +60 -0
  173. data/sig/io/udp.rbs +20 -0
  174. data/sig/io/unix.rbs +27 -0
  175. data/sig/io.rbs +6 -0
  176. data/sig/options.rbs +32 -22
  177. data/sig/parser/http1.rbs +1 -1
  178. data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
  179. data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
  180. data/sig/plugins/auth.rbs +13 -0
  181. data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
  182. data/sig/plugins/brotli.rbs +22 -0
  183. data/sig/plugins/callbacks.rbs +38 -0
  184. data/sig/plugins/circuit_breaker.rbs +71 -0
  185. data/sig/plugins/compression.rbs +7 -5
  186. data/sig/plugins/cookies/jar.rbs +2 -2
  187. data/sig/plugins/cookies.rbs +2 -0
  188. data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
  189. data/sig/plugins/follow_redirects.rbs +18 -4
  190. data/sig/plugins/grpc/call.rbs +19 -0
  191. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  192. data/sig/plugins/grpc/message.rbs +17 -0
  193. data/sig/plugins/grpc.rbs +7 -32
  194. data/sig/plugins/h2c.rbs +1 -1
  195. data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
  196. data/sig/plugins/oauth.rbs +54 -0
  197. data/sig/plugins/proxy/http.rbs +3 -0
  198. data/sig/plugins/proxy/socks4.rbs +9 -6
  199. data/sig/plugins/proxy/socks5.rbs +10 -6
  200. data/sig/plugins/proxy/ssh.rbs +1 -1
  201. data/sig/plugins/proxy.rbs +13 -5
  202. data/sig/plugins/push_promise.rbs +3 -3
  203. data/sig/plugins/rate_limiter.rbs +1 -1
  204. data/sig/plugins/response_cache.rbs +36 -7
  205. data/sig/plugins/retries.rbs +30 -8
  206. data/sig/plugins/stream.rbs +24 -17
  207. data/sig/plugins/upgrade.rbs +5 -3
  208. data/sig/pool.rbs +10 -7
  209. data/sig/request/body.rbs +38 -0
  210. data/sig/request.rbs +15 -24
  211. data/sig/resolver/https.rbs +8 -3
  212. data/sig/resolver/native.rbs +17 -4
  213. data/sig/resolver/resolver.rbs +8 -6
  214. data/sig/resolver/system.rbs +2 -0
  215. data/sig/resolver.rbs +9 -5
  216. data/sig/response/body.rbs +53 -0
  217. data/sig/response/buffer.rbs +24 -0
  218. data/sig/response.rbs +24 -39
  219. data/sig/selector.rbs +1 -1
  220. data/sig/session.rbs +29 -18
  221. data/sig/timers.rbs +18 -8
  222. data/sig/transcoder/body.rbs +4 -3
  223. data/sig/transcoder/deflate.rbs +11 -0
  224. data/sig/transcoder/form.rbs +5 -3
  225. data/sig/transcoder/gzip.rbs +24 -0
  226. data/sig/transcoder/json.rbs +8 -3
  227. data/sig/{plugins → transcoder}/multipart.rbs +15 -19
  228. data/sig/transcoder/utils/body_reader.rbs +15 -0
  229. data/sig/transcoder/utils/deflater.rbs +29 -0
  230. data/sig/transcoder/utils/inflater.rbs +12 -0
  231. data/sig/transcoder/xml.rbs +22 -0
  232. data/sig/transcoder.rbs +24 -9
  233. data/sig/utils.rbs +8 -2
  234. metadata +163 -41
  235. data/lib/httpx/plugins/authentication.rb +0 -20
  236. data/lib/httpx/plugins/basic_authentication.rb +0 -30
  237. data/lib/httpx/plugins/compression/brotli.rb +0 -54
  238. data/lib/httpx/plugins/compression/deflate.rb +0 -49
  239. data/lib/httpx/plugins/compression/gzip.rb +0 -88
  240. data/lib/httpx/plugins/compression.rb +0 -164
  241. data/lib/httpx/plugins/multipart/decoder.rb +0 -187
  242. data/lib/httpx/plugins/multipart.rb +0 -84
  243. data/lib/httpx/registry.rb +0 -85
  244. data/sig/plugins/authentication.rbs +0 -11
  245. data/sig/plugins/compression/brotli.rbs +0 -21
  246. data/sig/plugins/compression/deflate.rbs +0 -17
  247. data/sig/plugins/compression/gzip.rbs +0 -29
  248. data/sig/registry.rbs +0 -12
  249. /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
  250. /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "uri"
5
+ require "stringio"
6
+ require "zlib"
7
+
8
+ module HTTPX
9
+ module Transcoder
10
+ module GZIP
11
+ class Deflater < Transcoder::Deflater
12
+ def initialize(body)
13
+ @compressed_chunk = "".b
14
+ super
15
+ end
16
+
17
+ def deflate(chunk)
18
+ @deflater ||= Zlib::GzipWriter.new(self)
19
+
20
+ if chunk.nil?
21
+ unless @deflater.closed?
22
+ @deflater.flush
23
+ @deflater.close
24
+ compressed_chunk
25
+ end
26
+ else
27
+ @deflater.write(chunk)
28
+ compressed_chunk
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def write(chunk)
35
+ @compressed_chunk << chunk
36
+ end
37
+
38
+ def compressed_chunk
39
+ @compressed_chunk.dup
40
+ ensure
41
+ @compressed_chunk.clear
42
+ end
43
+ end
44
+
45
+ class Inflater
46
+ def initialize(bytesize)
47
+ @inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
48
+ @bytesize = bytesize
49
+ end
50
+
51
+ def call(chunk)
52
+ buffer = @inflater.inflate(chunk)
53
+ @bytesize -= chunk.bytesize
54
+ if @bytesize <= 0
55
+ buffer << @inflater.finish
56
+ @inflater.close
57
+ end
58
+ buffer
59
+ end
60
+ end
61
+
62
+ module_function
63
+
64
+ def encode(body)
65
+ Deflater.new(body)
66
+ end
67
+
68
+ def decode(response, bytesize: nil)
69
+ bytesize ||= response.headers.key?("content-length") ? response.headers["content-length"].to_i : Float::INFINITY
70
+ Inflater.new(bytesize)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,16 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
- require "json"
5
4
 
6
5
  module HTTPX::Transcoder
7
6
  module JSON
8
- JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
9
-
10
- using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
11
-
12
7
  module_function
13
8
 
9
+ JSON_REGEX = %r{\bapplication/(?:vnd\.api\+|hal\+)?json\b}i.freeze
10
+
14
11
  class Encoder
15
12
  extend Forwardable
16
13
 
@@ -19,7 +16,7 @@ module HTTPX::Transcoder
19
16
  def_delegator :@raw, :bytesize
20
17
 
21
18
  def initialize(json)
22
- @raw = ::JSON.dump(json)
19
+ @raw = JSON.json_dump(json)
23
20
  @charset = @raw.encoding.name.downcase
24
21
  end
25
22
 
@@ -37,8 +34,24 @@ module HTTPX::Transcoder
37
34
 
38
35
  raise HTTPX::Error, "invalid json mime type (#{content_type})" unless JSON_REGEX.match?(content_type)
39
36
 
40
- ::JSON.method(:parse)
37
+ method(:json_load)
38
+ end
39
+
40
+ # rubocop:disable Style/SingleLineMethods
41
+ if defined?(MultiJson)
42
+ def json_load(*args); MultiJson.load(*args); end
43
+ def json_dump(*args); MultiJson.dump(*args); end
44
+ elsif defined?(Oj)
45
+ def json_load(response, *args); Oj.load(response.to_s, *args); end
46
+ def json_dump(*args); Oj.dump(*args); end
47
+ elsif defined?(Yajl)
48
+ def json_load(response, *args); Yajl::Parser.new(*args).parse(response.to_s); end
49
+ def json_dump(*args); Yajl::Encoder.encode(*args); end
50
+ else
51
+ require "json"
52
+ def json_load(*args); ::JSON.parse(*args); end
53
+ def json_dump(*args); ::JSON.dump(*args); end
41
54
  end
55
+ # rubocop:enable Style/SingleLineMethods
42
56
  end
43
- register "json", JSON
44
57
  end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require "delegate"
5
+
6
+ module HTTPX
7
+ module Transcoder
8
+ module Multipart
9
+ class FilePart < SimpleDelegator
10
+ attr_reader :original_filename, :content_type
11
+
12
+ def initialize(filename, content_type)
13
+ @original_filename = filename
14
+ @content_type = content_type
15
+ @file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
16
+ super(@file)
17
+ end
18
+ end
19
+
20
+ class Decoder
21
+ include HTTPX::Utils
22
+
23
+ CRLF = "\r\n"
24
+ BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
25
+ MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
26
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
27
+ MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
28
+ WINDOW_SIZE = 2 << 14
29
+
30
+ def initialize(response)
31
+ @boundary = begin
32
+ m = response.headers["content-type"].to_s[BOUNDARY_RE, 1]
33
+ raise Error, "no boundary declared in content-type header" unless m
34
+
35
+ m.strip
36
+ end
37
+ @buffer = "".b
38
+ @parts = {}
39
+ @intermediate_boundary = "--#{@boundary}"
40
+ @state = :idle
41
+ end
42
+
43
+ def call(response, *)
44
+ response.body.each do |chunk|
45
+ @buffer << chunk
46
+
47
+ parse
48
+ end
49
+
50
+ raise Error, "invalid or unsupported multipart format" unless @buffer.empty?
51
+
52
+ @parts
53
+ end
54
+
55
+ private
56
+
57
+ def parse
58
+ case @state
59
+ when :idle
60
+ raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
61
+
62
+ @buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
63
+
64
+ @state = :part_header
65
+ when :part_header
66
+ idx = @buffer.index("#{CRLF}#{CRLF}")
67
+
68
+ # raise Error, "couldn't parse part headers" unless idx
69
+ return unless idx
70
+
71
+ head = @buffer.byteslice(0..idx + 4 - 1)
72
+
73
+ @buffer = @buffer.byteslice(head.bytesize..-1)
74
+
75
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
76
+ if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
77
+ name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
78
+ name.gsub!(/\\(.)/, "\\1")
79
+ name
80
+ else
81
+ name = head[MULTIPART_CONTENT_ID, 1]
82
+ end
83
+
84
+ filename = HTTPX::Utils.get_filename(head)
85
+
86
+ name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
87
+
88
+ @current = name
89
+
90
+ @parts[name] = if filename
91
+ FilePart.new(filename, content_type)
92
+ else
93
+ "".b
94
+ end
95
+
96
+ @state = :part_body
97
+ when :part_body
98
+ part = @parts[@current]
99
+
100
+ body_separator = if part.is_a?(FilePart)
101
+ "#{CRLF}#{CRLF}"
102
+ else
103
+ CRLF
104
+ end
105
+ idx = @buffer.index(body_separator)
106
+
107
+ if idx
108
+ payload = @buffer.byteslice(0..idx - 1)
109
+ @buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
110
+ part << payload
111
+ part.rewind if part.respond_to?(:rewind)
112
+ @state = :parse_boundary
113
+ else
114
+ part << @buffer
115
+ @buffer.clear
116
+ end
117
+ when :parse_boundary
118
+ raise Error, "payload does not start with boundary" unless @buffer.start_with?(@intermediate_boundary)
119
+
120
+ @buffer = @buffer.byteslice(@intermediate_boundary.bytesize..-1)
121
+
122
+ if @buffer == "--"
123
+ @buffer.clear
124
+ @state = :done
125
+ return
126
+ elsif @buffer.start_with?(CRLF)
127
+ @buffer = @buffer.byteslice(2..-1)
128
+ @state = :part_header
129
+ else
130
+ return
131
+ end
132
+ when :done
133
+ raise Error, "parsing should have been over by now"
134
+ end until @buffer.empty?
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module HTTPX::Plugins
4
- module Multipart
3
+ module HTTPX
4
+ module Transcoder::Multipart
5
5
  class Encoder
6
6
  attr_reader :bytesize
7
7
 
@@ -19,7 +19,7 @@ module HTTPX::Plugins
19
19
  end
20
20
 
21
21
  def read(length = nil, outbuf = nil)
22
- data = outbuf.clear.force_encoding(Encoding::BINARY) if outbuf
22
+ data = String(outbuf).clear.force_encoding(Encoding::BINARY) if outbuf
23
23
  data ||= "".b
24
24
 
25
25
  read_chunks(data, length)
@@ -43,7 +43,7 @@ module HTTPX::Plugins
43
43
  def to_parts(form)
44
44
  @bytesize = 0
45
45
  params = form.each_with_object([]) do |(key, val), aux|
46
- Multipart.normalize_keys(key, val) do |k, v|
46
+ Transcoder.normalize_keys(key, val, MULTIPART_VALUE_COND) do |k, v|
47
47
  next if v.nil?
48
48
 
49
49
  value, content_type, filename = Part.call(v)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- module Plugins::Multipart
4
+ module Transcoder::Multipart
5
5
  module MimeTypeDetector
6
6
  module_function
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- module Plugins::Multipart
4
+ module Transcoder::Multipart
5
5
  module Part
6
6
  module_function
7
7
 
@@ -21,7 +21,8 @@ module HTTPX
21
21
 
22
22
  value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
23
23
 
24
- if value.is_a?(File)
24
+ if value.respond_to?(:path) && value.respond_to?(:read)
25
+ # either a File, a Tempfile, or something else which has to quack like a file
25
26
  filename ||= File.basename(value.path)
26
27
  content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream"
27
28
  [value, content_type, filename]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "multipart/encoder"
4
+ require_relative "multipart/decoder"
5
+ require_relative "multipart/part"
6
+ require_relative "multipart/mime_type_detector"
7
+
8
+ module HTTPX::Transcoder
9
+ module Multipart
10
+ MULTIPART_VALUE_COND = lambda do |value|
11
+ value.respond_to?(:read) ||
12
+ (value.respond_to?(:to_hash) &&
13
+ value.key?(:body) &&
14
+ (value.key?(:filename) || value.key?(:content_type)))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module HTTPX
6
+ module Transcoder
7
+ class BodyReader
8
+ def initialize(body)
9
+ @body = if body.respond_to?(:read)
10
+ body.rewind if body.respond_to?(:rewind)
11
+ body
12
+ elsif body.respond_to?(:each)
13
+ body.enum_for(:each)
14
+ else
15
+ StringIO.new(body.to_s)
16
+ end
17
+ end
18
+
19
+ def bytesize
20
+ return @body.bytesize if @body.respond_to?(:bytesize)
21
+
22
+ Float::INFINITY
23
+ end
24
+
25
+ def read(length = nil, outbuf = nil)
26
+ return @body.read(length, outbuf) if @body.respond_to?(:read)
27
+
28
+ begin
29
+ chunk = @body.next
30
+ if outbuf
31
+ outbuf.clear.force_encoding(Encoding::BINARY)
32
+ outbuf << chunk
33
+ else
34
+ outbuf = chunk
35
+ end
36
+ outbuf unless length && outbuf.empty?
37
+ rescue StopIteration
38
+ end
39
+ end
40
+
41
+ def close
42
+ @body.close if @body.respond_to?(:close)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require_relative "body_reader"
5
+
6
+ module HTTPX
7
+ module Transcoder
8
+ class Deflater
9
+ extend Forwardable
10
+
11
+ attr_reader :content_type
12
+
13
+ def initialize(body)
14
+ @content_type = body.content_type
15
+ @body = BodyReader.new(body)
16
+ @closed = false
17
+ end
18
+
19
+ def bytesize
20
+ buffer_deflate!
21
+
22
+ @buffer.size
23
+ end
24
+
25
+ def read(length = nil, outbuf = nil)
26
+ return @buffer.read(length, outbuf) if @buffer
27
+
28
+ return if @closed
29
+
30
+ chunk = @body.read(length)
31
+
32
+ compressed_chunk = deflate(chunk)
33
+
34
+ return unless compressed_chunk
35
+
36
+ if outbuf
37
+ outbuf.clear.force_encoding(Encoding::BINARY)
38
+ outbuf << compressed_chunk
39
+ else
40
+ compressed_chunk
41
+ end
42
+ end
43
+
44
+ def close
45
+ return if @closed
46
+
47
+ @buffer.close if @buffer
48
+
49
+ @body.close
50
+
51
+ @closed = true
52
+ end
53
+
54
+ private
55
+
56
+ # rubocop:disable Naming/MemoizedInstanceVariableName
57
+ def buffer_deflate!
58
+ return @buffer if defined?(@buffer)
59
+
60
+ buffer = Response::Buffer.new(
61
+ threshold_size: Options::MAX_BODY_THRESHOLD_SIZE
62
+ )
63
+ ::IO.copy_stream(self, buffer)
64
+
65
+ buffer.rewind
66
+
67
+ @buffer = buffer
68
+ end
69
+ # rubocop:enable Naming/MemoizedInstanceVariableName
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+ module HTTPX
2
+ module Transcoder
3
+ class Inflater
4
+ def initialize(bytesize)
5
+ @bytesize = bytesize
6
+ end
7
+
8
+ def call(chunk)
9
+ buffer = @inflater.inflate(chunk)
10
+ @bytesize -= chunk.bytesize
11
+ if @bytesize <= 0
12
+ buffer << @inflater.finish
13
+ @inflater.close
14
+ end
15
+ buffer
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "forwardable"
5
+ require "uri"
6
+
7
+ module HTTPX::Transcoder
8
+ module Xml
9
+ module_function
10
+
11
+ MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze
12
+
13
+ class Encoder
14
+ def initialize(xml)
15
+ @raw = xml
16
+ end
17
+
18
+ def content_type
19
+ charset = @raw.respond_to?(:encoding) ? @raw.encoding.to_s.downcase : "utf-8"
20
+ "application/xml; charset=#{charset}"
21
+ end
22
+
23
+ def bytesize
24
+ @raw.to_s.bytesize
25
+ end
26
+
27
+ def to_s
28
+ @raw.to_s
29
+ end
30
+ end
31
+
32
+ def encode(xml)
33
+ Encoder.new(xml)
34
+ end
35
+
36
+ begin
37
+ require "nokogiri"
38
+
39
+ def decode(response)
40
+ content_type = response.content_type.mime_type
41
+
42
+ raise HTTPX::Error, "invalid form mime type (#{content_type})" unless MIME_TYPES.match?(content_type)
43
+
44
+ Nokogiri::XML.method(:parse)
45
+ end
46
+ rescue LoadError
47
+ def decode(_response)
48
+ raise HTTPX::Error, "\"nokogiri\" is required in order to decode XML"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -2,14 +2,10 @@
2
2
 
3
3
  module HTTPX
4
4
  module Transcoder
5
- extend Registry
6
-
7
- using RegexpExtensions unless Regexp.method_defined?(:match?)
8
-
9
5
  module_function
10
6
 
11
7
  def normalize_keys(key, value, cond = nil, &block)
12
- if (cond && cond.call(value))
8
+ if cond && cond.call(value)
13
9
  block.call(key.to_s, value)
14
10
  elsif value.respond_to?(:to_ary)
15
11
  if value.empty?
@@ -73,7 +69,7 @@ module HTTPX
73
69
  end
74
70
 
75
71
  def params_hash_has_key?(hash, key)
76
- return false if /\[\]/.match?(key)
72
+ return false if key.include?("[]")
77
73
 
78
74
  key.split(/[\[\]]+/).inject(hash) do |h, part|
79
75
  next h if part == ""
@@ -90,4 +86,7 @@ end
90
86
  require "httpx/transcoder/body"
91
87
  require "httpx/transcoder/form"
92
88
  require "httpx/transcoder/json"
89
+ require "httpx/transcoder/xml"
93
90
  require "httpx/transcoder/chunker"
91
+ require "httpx/transcoder/deflate"
92
+ require "httpx/transcoder/gzip"
data/lib/httpx/utils.rb CHANGED
@@ -4,6 +4,11 @@ module HTTPX
4
4
  module Utils
5
5
  using URIExtensions
6
6
 
7
+ TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
8
+ VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
9
+ FILENAME_REGEX = /\s*filename=(#{VALUE})/.freeze
10
+ FILENAME_EXTENSION_REGEX = /\s*filename\*=(#{VALUE})/.freeze
11
+
7
12
  module_function
8
13
 
9
14
  def now
@@ -25,31 +30,46 @@ module HTTPX
25
30
  time - Time.now
26
31
  end
27
32
 
28
- if RUBY_VERSION < "2.3"
29
-
30
- def to_uri(uri)
31
- URI(uri)
33
+ def get_filename(header, _prefix_regex = nil)
34
+ filename = nil
35
+ case header
36
+ when FILENAME_REGEX
37
+ filename = Regexp.last_match(1)
38
+ filename = Regexp.last_match(1) if filename =~ /^"(.*)"$/
39
+ when FILENAME_EXTENSION_REGEX
40
+ filename = Regexp.last_match(1)
41
+ encoding, _, filename = filename.split("'", 3)
32
42
  end
33
43
 
34
- else
44
+ return unless filename
35
45
 
36
- URIParser = URI::RFC2396_Parser.new
46
+ filename = URI::DEFAULT_PARSER.unescape(filename) if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
37
47
 
38
- def to_uri(uri)
39
- return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
48
+ filename.scrub!
40
49
 
41
- uri = URI(URIParser.escape(uri))
50
+ filename = filename.gsub(/\\(.)/, '\1') unless /\\[^\\"]/.match?(filename)
42
51
 
43
- non_ascii_hostname = URIParser.unescape(uri.host)
52
+ filename.force_encoding ::Encoding.find(encoding) if encoding
44
53
 
45
- non_ascii_hostname.force_encoding(Encoding::UTF_8)
54
+ filename
55
+ end
46
56
 
47
- idna_hostname = Punycode.encode_hostname(non_ascii_hostname)
57
+ URIParser = URI::RFC2396_Parser.new
48
58
 
49
- uri.host = idna_hostname
50
- uri.non_ascii_hostname = non_ascii_hostname
51
- uri
52
- end
59
+ def to_uri(uri)
60
+ return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
61
+
62
+ uri = URI(URIParser.escape(uri))
63
+
64
+ non_ascii_hostname = URIParser.unescape(uri.host)
65
+
66
+ non_ascii_hostname.force_encoding(Encoding::UTF_8)
67
+
68
+ idna_hostname = Punycode.encode_hostname(non_ascii_hostname)
69
+
70
+ uri.host = idna_hostname
71
+ uri.non_ascii_hostname = non_ascii_hostname
72
+ uri
53
73
  end
54
74
  end
55
75
  end
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "0.20.0"
4
+ VERSION = "1.3.1"
5
5
  end