httpx 0.7.0 → 0.10.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.
Files changed (137) 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_1_0.md +9 -0
  11. data/doc/release_notes/0_2_0.md +5 -0
  12. data/doc/release_notes/0_2_1.md +16 -0
  13. data/doc/release_notes/0_3_0.md +12 -0
  14. data/doc/release_notes/0_3_1.md +6 -0
  15. data/doc/release_notes/0_4_0.md +51 -0
  16. data/doc/release_notes/0_4_1.md +3 -0
  17. data/doc/release_notes/0_5_0.md +15 -0
  18. data/doc/release_notes/0_5_1.md +14 -0
  19. data/doc/release_notes/0_6_0.md +5 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_6_2.md +6 -0
  22. data/doc/release_notes/0_6_3.md +13 -0
  23. data/doc/release_notes/0_6_4.md +21 -0
  24. data/doc/release_notes/0_6_5.md +22 -0
  25. data/doc/release_notes/0_6_6.md +19 -0
  26. data/doc/release_notes/0_6_7.md +5 -0
  27. data/doc/release_notes/0_7_0.md +46 -0
  28. data/doc/release_notes/0_8_0.md +27 -0
  29. data/doc/release_notes/0_8_1.md +8 -0
  30. data/doc/release_notes/0_8_2.md +7 -0
  31. data/doc/release_notes/0_9_0.md +38 -0
  32. data/lib/httpx.rb +2 -0
  33. data/lib/httpx/adapters/faraday.rb +1 -1
  34. data/lib/httpx/altsvc.rb +18 -2
  35. data/lib/httpx/chainable.rb +9 -8
  36. data/lib/httpx/connection.rb +177 -72
  37. data/lib/httpx/connection/http1.rb +44 -13
  38. data/lib/httpx/connection/http2.rb +77 -34
  39. data/lib/httpx/domain_name.rb +440 -0
  40. data/lib/httpx/errors.rb +1 -0
  41. data/lib/httpx/extensions.rb +23 -3
  42. data/lib/httpx/headers.rb +2 -2
  43. data/lib/httpx/io/ssl.rb +11 -4
  44. data/lib/httpx/io/tcp.rb +16 -5
  45. data/lib/httpx/io/udp.rb +4 -1
  46. data/lib/httpx/loggable.rb +6 -6
  47. data/lib/httpx/options.rb +22 -15
  48. data/lib/httpx/parser/http1.rb +14 -17
  49. data/lib/httpx/plugins/compression.rb +49 -64
  50. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  51. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  52. data/lib/httpx/plugins/compression/gzip.rb +45 -17
  53. data/lib/httpx/plugins/cookies.rb +21 -60
  54. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  55. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  56. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  57. data/lib/httpx/plugins/digest_authentication.rb +2 -0
  58. data/lib/httpx/plugins/expect.rb +12 -1
  59. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  60. data/lib/httpx/plugins/h2c.rb +1 -1
  61. data/lib/httpx/plugins/multipart.rb +0 -8
  62. data/lib/httpx/plugins/persistent.rb +6 -1
  63. data/lib/httpx/plugins/proxy.rb +16 -12
  64. data/lib/httpx/plugins/proxy/http.rb +7 -2
  65. data/lib/httpx/plugins/proxy/socks4.rb +4 -2
  66. data/lib/httpx/plugins/proxy/socks5.rb +5 -1
  67. data/lib/httpx/plugins/push_promise.rb +2 -2
  68. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  69. data/lib/httpx/plugins/retries.rb +13 -6
  70. data/lib/httpx/plugins/stream.rb +109 -13
  71. data/lib/httpx/pool.rb +13 -15
  72. data/lib/httpx/registry.rb +2 -1
  73. data/lib/httpx/request.rb +14 -19
  74. data/lib/httpx/resolver.rb +7 -8
  75. data/lib/httpx/resolver/https.rb +22 -5
  76. data/lib/httpx/resolver/native.rb +27 -33
  77. data/lib/httpx/resolver/options.rb +2 -2
  78. data/lib/httpx/resolver/resolver_mixin.rb +1 -1
  79. data/lib/httpx/response.rb +22 -17
  80. data/lib/httpx/selector.rb +96 -97
  81. data/lib/httpx/session.rb +32 -24
  82. data/lib/httpx/timeout.rb +7 -1
  83. data/lib/httpx/transcoder/chunker.rb +0 -2
  84. data/lib/httpx/transcoder/form.rb +0 -6
  85. data/lib/httpx/transcoder/json.rb +0 -4
  86. data/lib/httpx/utils.rb +45 -0
  87. data/lib/httpx/version.rb +1 -1
  88. data/sig/buffer.rbs +24 -0
  89. data/sig/callbacks.rbs +14 -0
  90. data/sig/chainable.rbs +37 -0
  91. data/sig/connection.rbs +2 -0
  92. data/sig/connection/http2.rbs +4 -0
  93. data/sig/domain_name.rbs +17 -0
  94. data/sig/errors.rbs +3 -0
  95. data/sig/headers.rbs +42 -0
  96. data/sig/httpx.rbs +14 -0
  97. data/sig/loggable.rbs +11 -0
  98. data/sig/missing.rbs +12 -0
  99. data/sig/options.rbs +118 -0
  100. data/sig/parser/http1.rbs +50 -0
  101. data/sig/plugins/authentication.rbs +11 -0
  102. data/sig/plugins/basic_authentication.rbs +13 -0
  103. data/sig/plugins/compression.rbs +55 -0
  104. data/sig/plugins/compression/brotli.rbs +21 -0
  105. data/sig/plugins/compression/deflate.rbs +17 -0
  106. data/sig/plugins/compression/gzip.rbs +29 -0
  107. data/sig/plugins/cookies.rbs +26 -0
  108. data/sig/plugins/cookies/cookie.rbs +50 -0
  109. data/sig/plugins/cookies/jar.rbs +27 -0
  110. data/sig/plugins/digest_authentication.rbs +33 -0
  111. data/sig/plugins/expect.rbs +19 -0
  112. data/sig/plugins/follow_redirects.rbs +37 -0
  113. data/sig/plugins/h2c.rbs +26 -0
  114. data/sig/plugins/multipart.rbs +19 -0
  115. data/sig/plugins/persistent.rbs +17 -0
  116. data/sig/plugins/proxy.rbs +47 -0
  117. data/sig/plugins/proxy/http.rbs +14 -0
  118. data/sig/plugins/proxy/socks4.rbs +33 -0
  119. data/sig/plugins/proxy/socks5.rbs +36 -0
  120. data/sig/plugins/proxy/ssh.rbs +18 -0
  121. data/sig/plugins/push_promise.rbs +22 -0
  122. data/sig/plugins/rate_limiter.rbs +11 -0
  123. data/sig/plugins/retries.rbs +48 -0
  124. data/sig/plugins/stream.rbs +39 -0
  125. data/sig/pool.rbs +2 -0
  126. data/sig/registry.rbs +9 -0
  127. data/sig/request.rbs +61 -0
  128. data/sig/response.rbs +87 -0
  129. data/sig/session.rbs +49 -0
  130. data/sig/test.rbs +9 -0
  131. data/sig/timeout.rbs +29 -0
  132. data/sig/transcoder.rbs +16 -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 +120 -21
@@ -28,6 +28,7 @@ module HTTPX
28
28
 
29
29
  NativeResolveError = Class.new(ResolveError) do
30
30
  attr_reader :connection, :host
31
+
31
32
  def initialize(connection, host, message = "Can't resolve #{host}")
32
33
  @connection = connection
33
34
  @host = host
@@ -11,7 +11,7 @@ module HTTPX
11
11
  #
12
12
  # Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
13
13
  #
14
- module CurryMethods # :nodoc:
14
+ module CurryMethods
15
15
  # Backport for the Method#curry method, which is part of ruby core since 2.2 .
16
16
  #
17
17
  def curry(*args)
@@ -54,11 +54,31 @@ module HTTPX
54
54
  Numeric.__send__(:include, NegMethods)
55
55
  end
56
56
 
57
+ module RegexpExtensions
58
+ # If you wonder why this is there: the oauth feature uses a refinement to enhance the
59
+ # Regexp class locally with #match? , but this is never tested, because ActiveSupport
60
+ # monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
61
+ # :nocov:
62
+ refine(Regexp) do
63
+ def match?(*args)
64
+ !match(*args).nil?
65
+ end
66
+ end
67
+ end
68
+
57
69
  module URIExtensions
58
70
  refine URI::Generic do
71
+ def non_ascii_hostname
72
+ @non_ascii_hostname
73
+ end
74
+
75
+ def non_ascii_hostname=(hostname)
76
+ @non_ascii_hostname = hostname
77
+ end
78
+
59
79
  def authority
60
80
  port_string = port == default_port ? nil : ":#{port}"
61
- "#{host}#{port_string}"
81
+ "#{@non_ascii_hostname || host}#{port_string}"
62
82
  end
63
83
 
64
84
  def origin
@@ -81,4 +101,4 @@ module HTTPX
81
101
  end
82
102
  end
83
103
  end
84
- end
104
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module HTTPX
4
4
  class Headers
5
- EMPTY = [].freeze # :nodoc:
5
+ EMPTY = [].freeze
6
6
 
7
7
  class << self
8
8
  def new(headers = nil)
@@ -67,7 +67,7 @@ module HTTPX
67
67
  #
68
68
  def [](field)
69
69
  a = @headers[downcased(field)] || return
70
- a.join(",")
70
+ a.join(", ")
71
71
  end
72
72
 
73
73
  # sets +value+ (if not nil) as single value for the +field+ header.
@@ -20,6 +20,10 @@ module HTTPX
20
20
  @state = :negotiated if @keep_open
21
21
  end
22
22
 
23
+ def interests
24
+ @interests || super
25
+ end
26
+
23
27
  def protocol
24
28
  @io.alpn_protocol || super
25
29
  rescue StandardError
@@ -38,7 +42,6 @@ module HTTPX
38
42
  # allow reconnections
39
43
  # connect only works if initial @io is a socket
40
44
  @io = @io.io if @io.respond_to?(:io)
41
- @negotiated = false
42
45
  end
43
46
 
44
47
  def connected?
@@ -62,15 +65,18 @@ module HTTPX
62
65
  @io.connect_nonblock
63
66
  @io.post_connection_check(@hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
64
67
  transition(:negotiated)
65
- rescue ::IO::WaitReadable,
66
- ::IO::WaitWritable
68
+ rescue ::IO::WaitReadable
69
+ @interests = :r
70
+ rescue ::IO::WaitWritable
71
+ @interests = :w
67
72
  end
68
73
 
69
74
  # :nocov:
70
75
  if RUBY_VERSION < "2.3"
71
- def read(*)
76
+ def read(_, buffer)
72
77
  super
73
78
  rescue ::IO::WaitWritable
79
+ buffer.clear
74
80
  0
75
81
  end
76
82
 
@@ -86,6 +92,7 @@ module HTTPX
86
92
  buffer.bytesize
87
93
  rescue ::IO::WaitReadable,
88
94
  ::IO::WaitWritable
95
+ buffer.clear
89
96
  0
90
97
  rescue EOFError
91
98
  nil
@@ -7,9 +7,7 @@ module HTTPX
7
7
  class TCP
8
8
  include Loggable
9
9
 
10
- attr_reader :ip, :port
11
-
12
- attr_reader :addresses
10
+ attr_reader :ip, :port, :addresses, :state
13
11
 
14
12
  alias_method :host, :ip
15
13
 
@@ -41,6 +39,10 @@ module HTTPX
41
39
  @io ||= build_socket
42
40
  end
43
41
 
42
+ def interests
43
+ :w
44
+ end
45
+
44
46
  def to_io
45
47
  @io.to_io
46
48
  end
@@ -67,7 +69,7 @@ module HTTPX
67
69
  @ip_index -= 1
68
70
  retry
69
71
  rescue Errno::ETIMEDOUT => e
70
- raise ConnectTimeout, e.message if @ip_index <= 0
72
+ raise ConnectTimeoutError, e.message if @ip_index <= 0
71
73
 
72
74
  @ip_index -= 1
73
75
  retry
@@ -80,8 +82,10 @@ module HTTPX
80
82
  # :nocov:
81
83
  def read(size, buffer)
82
84
  @io.read_nonblock(size, buffer)
85
+ log { "READ: #{buffer.bytesize} bytes..." }
83
86
  buffer.bytesize
84
87
  rescue ::IO::WaitReadable
88
+ buffer.clear
85
89
  0
86
90
  rescue EOFError
87
91
  nil
@@ -89,6 +93,7 @@ module HTTPX
89
93
 
90
94
  def write(buffer)
91
95
  siz = @io.write_nonblock(buffer)
96
+ log { "WRITE: #{siz} bytes..." }
92
97
  buffer.shift!(siz)
93
98
  siz
94
99
  rescue ::IO::WaitWritable
@@ -100,9 +105,13 @@ module HTTPX
100
105
  else
101
106
  def read(size, buffer)
102
107
  ret = @io.read_nonblock(size, buffer, exception: false)
103
- return 0 if ret == :wait_readable
108
+ if ret == :wait_readable
109
+ buffer.clear
110
+ return 0
111
+ end
104
112
  return if ret.nil?
105
113
 
114
+ log { "READ: #{buffer.bytesize} bytes..." }
106
115
  buffer.bytesize
107
116
  end
108
117
 
@@ -111,6 +120,8 @@ module HTTPX
111
120
  return 0 if siz == :wait_writable
112
121
  return if siz.nil?
113
122
 
123
+ log { "WRITE: #{siz} bytes..." }
124
+
114
125
  buffer.shift!(siz)
115
126
  siz
116
127
  end
@@ -7,11 +7,12 @@ module HTTPX
7
7
  class UDP
8
8
  include Loggable
9
9
 
10
- def initialize(uri, _, _)
10
+ def initialize(uri, _, options)
11
11
  ip = IPAddr.new(uri.host)
12
12
  @host = ip.to_s
13
13
  @port = uri.port
14
14
  @io = UDPSocket.new(ip.family)
15
+ @options = options
15
16
  end
16
17
 
17
18
  def to_io
@@ -40,6 +41,7 @@ module HTTPX
40
41
 
41
42
  def write(buffer)
42
43
  siz = @io.send(buffer, 0, @host, @port)
44
+ log { "WRITE: #{siz} bytes..." }
43
45
  buffer.shift!(siz)
44
46
  siz
45
47
  end
@@ -49,6 +51,7 @@ module HTTPX
49
51
  def read(size, buffer)
50
52
  data, _ = @io.recvfrom_nonblock(size)
51
53
  buffer.replace(data)
54
+ log { "READ: #{buffer.bytesize} bytes..." }
52
55
  buffer.bytesize
53
56
  rescue ::IO::WaitReadable
54
57
  0
@@ -13,35 +13,35 @@ module HTTPX
13
13
  white: 37,
14
14
  }.freeze
15
15
 
16
- def log(level: @options.debug_level, label: "", color: nil, &msg)
16
+ def log(level: @options.debug_level, color: nil, &msg)
17
17
  return unless @options.debug
18
18
  return unless @options.debug_level >= level
19
19
 
20
20
  debug_stream = @options.debug
21
21
 
22
- message = (+label << msg.call << "\n")
22
+ message = (+"" << msg.call << "\n")
23
23
  message = "\e[#{COLORS[color]}m#{message}\e[0m" if debug_stream.respond_to?(:isatty) && debug_stream.isatty
24
24
  debug_stream << message
25
25
  end
26
26
 
27
27
  if !Exception.instance_methods.include?(:full_message)
28
28
 
29
- def log_exception(ex, level: @options.debug_level, label: "", color: nil)
29
+ def log_exception(ex, level: @options.debug_level, color: nil)
30
30
  return unless @options.debug
31
31
  return unless @options.debug_level >= level
32
32
 
33
33
  message = +"#{ex.message} (#{ex.class})"
34
34
  message << "\n" << ex.backtrace.join("\n") unless ex.backtrace.nil?
35
- log(level: level, label: label, color: color) { message }
35
+ log(level: level, color: color) { message }
36
36
  end
37
37
 
38
38
  else
39
39
 
40
- def log_exception(ex, level: @options.debug_level, label: "", color: nil)
40
+ def log_exception(ex, level: @options.debug_level, color: nil)
41
41
  return unless @options.debug
42
42
  return unless @options.debug_level >= level
43
43
 
44
- log(level: level, label: label, color: color) { ex.full_message }
44
+ log(level: level, color: color) { ex.full_message }
45
45
  end
46
46
 
47
47
  end
@@ -25,23 +25,28 @@ module HTTPX
25
25
 
26
26
  def def_option(name, &interpreter)
27
27
  defined_options << name.to_sym
28
- interpreter ||= ->(v) { v }
29
28
 
30
29
  attr_reader name
31
30
 
32
- define_method(:"#{name}=") do |value|
33
- return if value.nil?
31
+ if interpreter
32
+ define_method(:"#{name}=") do |value|
33
+ return if value.nil?
34
34
 
35
- instance_variable_set(:"@#{name}", instance_exec(value, &interpreter))
36
- end
35
+ instance_variable_set(:"@#{name}", instance_exec(value, &interpreter))
36
+ end
37
37
 
38
- protected :"#{name}="
38
+ define_method(:"with_#{name}") do |value|
39
+ merge(name => instance_exec(value, &interpreter))
40
+ end
41
+ else
42
+ attr_writer name
39
43
 
40
- define_method(:"with_#{name}") do |value|
41
- other = dup
42
- other.send(:"#{name}=", other.instance_exec(value, &interpreter))
43
- other
44
+ define_method(:"with_#{name}") do |value|
45
+ merge(name => value)
46
+ end
44
47
  end
48
+
49
+ protected :"#{name}="
45
50
  end
46
51
  end
47
52
 
@@ -70,8 +75,9 @@ module HTTPX
70
75
  }
71
76
 
72
77
  defaults.merge!(options)
73
- defaults[:headers] = Headers.new(defaults[:headers])
74
78
  defaults.each do |(k, v)|
79
+ next if v.nil?
80
+
75
81
  __send__(:"#{k}=", v)
76
82
  end
77
83
  end
@@ -80,7 +86,7 @@ module HTTPX
80
86
  if self.headers
81
87
  self.headers.merge(headers)
82
88
  else
83
- headers
89
+ Headers.new(headers)
84
90
  end
85
91
  end
86
92
 
@@ -116,8 +122,7 @@ module HTTPX
116
122
  end
117
123
 
118
124
  %w[
119
- params form json body
120
- follow ssl http2_settings
125
+ params form json body ssl http2_settings
121
126
  request_class response_class headers_class request_body_class response_body_class connection_class
122
127
  io fallback_protocol debug debug_level transport_options resolver_class resolver_options
123
128
  persistent
@@ -143,8 +148,10 @@ module HTTPX
143
148
  end
144
149
 
145
150
  def merge(other)
146
- h1 = to_hash
147
151
  h2 = other.to_hash
152
+ return self if h2.empty?
153
+
154
+ h1 = to_hash
148
155
 
149
156
  merged = h1.merge(h2) do |k, v1, v2|
150
157
  case k
@@ -9,10 +9,9 @@ module HTTPX
9
9
 
10
10
  attr_reader :status_code, :http_version, :headers
11
11
 
12
- def initialize(observer, header_separator: ":")
12
+ def initialize(observer)
13
13
  @observer = observer
14
14
  @state = :idle
15
- @header_separator = header_separator
16
15
  @buffer = "".b
17
16
  @headers = {}
18
17
  end
@@ -40,25 +39,25 @@ module HTTPX
40
39
  private
41
40
 
42
41
  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
42
+ loop do
43
+ state = @state
44
+ case @state
45
+ when :idle
46
+ parse_headline
47
+ when :headers, :trailers
48
+ parse_headers
49
+ when :data
50
+ parse_data
51
+ end
52
+ return if @buffer.empty? || state == @state
53
53
  end
54
- parse if !@buffer.empty? && state != @state
55
54
  end
56
55
 
57
56
  def parse_headline
58
57
  idx = @buffer.index("\n")
59
58
  return unless idx
60
59
 
61
- (m = %r{\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?}in.match(@buffer)) ||
60
+ (m = %r{\AHTTP(?:/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?}in.match(@buffer)) ||
62
61
  raise(Error, "wrong head line format")
63
62
  version, code, _ = m.captures
64
63
  raise(Error, "unsupported HTTP version (HTTP/#{version})") unless VERSIONS.include?(version)
@@ -91,12 +90,10 @@ module HTTPX
91
90
  @observer.on_trailers(headers)
92
91
  headers.clear
93
92
  nextstate(:complete)
94
- else
95
- raise Error, "wrong header format"
96
93
  end
97
94
  return
98
95
  end
99
- separator_index = line.index(@header_separator)
96
+ separator_index = line.index(":")
100
97
  raise Error, "wrong header format" unless separator_index
101
98
 
102
99
  key = line[0..separator_index - 1]
@@ -14,13 +14,23 @@ module HTTPX
14
14
  #
15
15
  module Compression
16
16
  extend Registry
17
- def self.load_dependencies(klass)
18
- klass.plugin(:"compression/gzip")
19
- klass.plugin(:"compression/deflate")
20
- end
21
17
 
22
- def self.extra_options(options)
23
- options.merge(headers: { "accept-encoding" => Compression.registry.keys })
18
+ class << self
19
+ def load_dependencies(klass)
20
+ klass.plugin(:"compression/gzip")
21
+ klass.plugin(:"compression/deflate")
22
+ end
23
+
24
+ def extra_options(options)
25
+ Class.new(options.class) do
26
+ def_option(:compression_threshold_size) do |bytes|
27
+ bytes = Integer(bytes)
28
+ raise Error, ":expect_threshold_size must be positive" unless bytes.positive?
29
+
30
+ bytes
31
+ end
32
+ end.new(options).merge(headers: { "accept-encoding" => Compression.registry.keys })
33
+ end
24
34
  end
25
35
 
26
36
  module RequestMethods
@@ -32,12 +42,17 @@ module HTTPX
32
42
  end
33
43
 
34
44
  module RequestBodyMethods
35
- def initialize(*)
45
+ def initialize(*, options)
36
46
  super
37
47
  return if @body.nil?
38
48
 
49
+ threshold = options.compression_threshold_size
50
+ return if threshold && !unbounded_body? && @body.bytesize < threshold
51
+
39
52
  @headers.get("content-encoding").each do |encoding|
40
- @body = Encoder.new(@body, Compression.registry(encoding).encoder)
53
+ next if encoding == "identity"
54
+
55
+ @body = Encoder.new(@body, Compression.registry(encoding).deflater)
41
56
  end
42
57
  @headers["content-length"] = @body.bytesize unless chunked?
43
58
  end
@@ -53,55 +68,53 @@ module HTTPX
53
68
 
54
69
  return unless @headers.key?("content-encoding")
55
70
 
56
- @_decoders = @headers.get("content-encoding").map do |encoding|
57
- decoder = Compression.registry(encoding).decoder
58
- # do not uncompress if there is no decoder available. In fact, we can't reliably
59
- # continue decompressing beyond that, so ignore.
60
- break unless decoder
61
-
62
- @encodings << encoding
63
- decoder
64
- end
65
-
66
71
  # remove encodings that we are able to decode
67
72
  @headers["content-encoding"] = @headers.get("content-encoding") - @encodings
68
73
 
69
- @_compressed_length = if @headers.key?("content-length")
74
+ compressed_length = if @headers.key?("content-length")
70
75
  @headers["content-length"].to_i
71
76
  else
72
77
  Float::INFINITY
73
78
  end
79
+
80
+ @_inflaters = @headers.get("content-encoding").map do |encoding|
81
+ next if encoding == "identity"
82
+
83
+ inflater = Compression.registry(encoding).inflater(compressed_length)
84
+ # do not uncompress if there is no decoder available. In fact, we can't reliably
85
+ # continue decompressing beyond that, so ignore.
86
+ break unless inflater
87
+
88
+ @encodings << encoding
89
+ inflater
90
+ end.compact
91
+
92
+ # this can happen if the only declared encoding is "identity"
93
+ remove_instance_variable(:@_inflaters) if @_inflaters.empty?
74
94
  end
75
95
 
76
96
  def write(chunk)
77
- return super unless defined?(@_compressed_length)
97
+ return super unless defined?(@_inflaters)
78
98
 
79
- @_compressed_length -= chunk.bytesize
80
99
  chunk = decompress(chunk)
81
100
  super(chunk)
82
101
  end
83
102
 
84
- def close
85
- super
86
-
87
- return unless defined?(@_decoders)
88
-
89
- @_decoders.each(&:close)
90
- end
91
-
92
103
  private
93
104
 
94
105
  def decompress(buffer)
95
- @_decoders.reverse_each do |decoder|
96
- buffer = decoder.decode(buffer)
97
- buffer << decoder.finish if @_compressed_length <= 0
106
+ @_inflaters.reverse_each do |inflater|
107
+ buffer = inflater.inflate(buffer)
98
108
  end
99
109
  buffer
100
110
  end
101
111
  end
102
112
 
103
113
  class Encoder
114
+ attr_reader :content_type
115
+
104
116
  def initialize(body, deflater)
117
+ @content_type = body.content_type
105
118
  @body = body.respond_to?(:read) ? body : StringIO.new(body.to_s)
106
119
  @buffer = StringIO.new("".b, File::RDWR)
107
120
  @deflater = deflater
@@ -110,11 +123,10 @@ module HTTPX
110
123
  def each(&blk)
111
124
  return enum_for(__method__) unless block_given?
112
125
 
113
- unless @buffer.size.zero?
114
- @buffer.rewind
115
- return @buffer.each(&blk)
116
- end
117
- deflate(&blk)
126
+ return deflate(&blk) if @buffer.size.zero?
127
+
128
+ @buffer.rewind
129
+ @buffer.each(&blk)
118
130
  end
119
131
 
120
132
  def bytesize
@@ -122,17 +134,6 @@ module HTTPX
122
134
  @buffer.size
123
135
  end
124
136
 
125
- def to_s
126
- deflate
127
- @buffer.rewind
128
- @buffer.read
129
- end
130
-
131
- def close
132
- @buffer.close
133
- @body.close
134
- end
135
-
136
137
  private
137
138
 
138
139
  def deflate(&blk)
@@ -142,22 +143,6 @@ module HTTPX
142
143
  @deflater.deflate(@body, @buffer, chunk_size: 16_384, &blk)
143
144
  end
144
145
  end
145
-
146
- class Decoder
147
- extend Forwardable
148
-
149
- def_delegator :@inflater, :finish
150
-
151
- def_delegator :@inflater, :close
152
-
153
- def initialize(inflater)
154
- @inflater = inflater
155
- end
156
-
157
- def decode(chunk)
158
- @inflater.inflate(chunk)
159
- end
160
- end
161
146
  end
162
147
  register_plugin :compression, Compression
163
148
  end