http 6.0.0-java

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 (142) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +267 -0
  3. data/CONTRIBUTING.md +26 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.md +263 -0
  6. data/SECURITY.md +17 -0
  7. data/UPGRADING.md +491 -0
  8. data/http.gemspec +48 -0
  9. data/lib/http/base64.rb +22 -0
  10. data/lib/http/chainable/helpers.rb +62 -0
  11. data/lib/http/chainable/verbs.rb +136 -0
  12. data/lib/http/chainable.rb +377 -0
  13. data/lib/http/client.rb +230 -0
  14. data/lib/http/connection/internals.rb +141 -0
  15. data/lib/http/connection.rb +265 -0
  16. data/lib/http/content_type.rb +89 -0
  17. data/lib/http/errors.rb +67 -0
  18. data/lib/http/feature.rb +86 -0
  19. data/lib/http/features/auto_deflate.rb +230 -0
  20. data/lib/http/features/auto_inflate.rb +64 -0
  21. data/lib/http/features/caching/entry.rb +178 -0
  22. data/lib/http/features/caching/in_memory_store.rb +63 -0
  23. data/lib/http/features/caching.rb +216 -0
  24. data/lib/http/features/digest_auth.rb +234 -0
  25. data/lib/http/features/instrumentation.rb +149 -0
  26. data/lib/http/features/logging.rb +231 -0
  27. data/lib/http/features/normalize_uri.rb +34 -0
  28. data/lib/http/features/raise_error.rb +37 -0
  29. data/lib/http/form_data/composite_io.rb +106 -0
  30. data/lib/http/form_data/file.rb +95 -0
  31. data/lib/http/form_data/multipart/param.rb +62 -0
  32. data/lib/http/form_data/multipart.rb +106 -0
  33. data/lib/http/form_data/part.rb +52 -0
  34. data/lib/http/form_data/readable.rb +58 -0
  35. data/lib/http/form_data/urlencoded.rb +175 -0
  36. data/lib/http/form_data/version.rb +8 -0
  37. data/lib/http/form_data.rb +102 -0
  38. data/lib/http/headers/known.rb +90 -0
  39. data/lib/http/headers/normalizer.rb +50 -0
  40. data/lib/http/headers.rb +343 -0
  41. data/lib/http/mime_type/adapter.rb +43 -0
  42. data/lib/http/mime_type/json.rb +41 -0
  43. data/lib/http/mime_type.rb +96 -0
  44. data/lib/http/options/definitions.rb +189 -0
  45. data/lib/http/options.rb +241 -0
  46. data/lib/http/redirector.rb +157 -0
  47. data/lib/http/request/body.rb +181 -0
  48. data/lib/http/request/builder.rb +184 -0
  49. data/lib/http/request/proxy.rb +83 -0
  50. data/lib/http/request/writer.rb +186 -0
  51. data/lib/http/request.rb +375 -0
  52. data/lib/http/response/body.rb +172 -0
  53. data/lib/http/response/inflater.rb +60 -0
  54. data/lib/http/response/parser.rb +223 -0
  55. data/lib/http/response/status/reasons.rb +79 -0
  56. data/lib/http/response/status.rb +263 -0
  57. data/lib/http/response.rb +350 -0
  58. data/lib/http/retriable/delay_calculator.rb +91 -0
  59. data/lib/http/retriable/errors.rb +35 -0
  60. data/lib/http/retriable/performer.rb +197 -0
  61. data/lib/http/session.rb +280 -0
  62. data/lib/http/timeout/global.rb +229 -0
  63. data/lib/http/timeout/null.rb +225 -0
  64. data/lib/http/timeout/per_operation.rb +197 -0
  65. data/lib/http/uri/normalizer.rb +82 -0
  66. data/lib/http/uri/parsing.rb +182 -0
  67. data/lib/http/uri.rb +376 -0
  68. data/lib/http/version.rb +6 -0
  69. data/lib/http.rb +36 -0
  70. data/sig/deps.rbs +122 -0
  71. data/sig/http.rbs +1619 -0
  72. data/test/http/base64_test.rb +28 -0
  73. data/test/http/client_test.rb +739 -0
  74. data/test/http/connection_test.rb +1533 -0
  75. data/test/http/content_type_test.rb +190 -0
  76. data/test/http/errors_test.rb +28 -0
  77. data/test/http/feature_test.rb +49 -0
  78. data/test/http/features/auto_deflate_test.rb +317 -0
  79. data/test/http/features/auto_inflate_test.rb +213 -0
  80. data/test/http/features/caching_test.rb +942 -0
  81. data/test/http/features/digest_auth_test.rb +996 -0
  82. data/test/http/features/instrumentation_test.rb +246 -0
  83. data/test/http/features/logging_test.rb +654 -0
  84. data/test/http/features/normalize_uri_test.rb +41 -0
  85. data/test/http/features/raise_error_test.rb +77 -0
  86. data/test/http/form_data/composite_io_test.rb +215 -0
  87. data/test/http/form_data/file_test.rb +255 -0
  88. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  89. data/test/http/form_data/multipart_test.rb +303 -0
  90. data/test/http/form_data/part_test.rb +90 -0
  91. data/test/http/form_data/urlencoded_test.rb +164 -0
  92. data/test/http/form_data_test.rb +232 -0
  93. data/test/http/headers/normalizer_test.rb +93 -0
  94. data/test/http/headers_test.rb +888 -0
  95. data/test/http/mime_type/json_test.rb +39 -0
  96. data/test/http/mime_type_test.rb +150 -0
  97. data/test/http/options/base_uri_test.rb +148 -0
  98. data/test/http/options/body_test.rb +21 -0
  99. data/test/http/options/features_test.rb +38 -0
  100. data/test/http/options/form_test.rb +21 -0
  101. data/test/http/options/headers_test.rb +32 -0
  102. data/test/http/options/json_test.rb +21 -0
  103. data/test/http/options/merge_test.rb +78 -0
  104. data/test/http/options/new_test.rb +37 -0
  105. data/test/http/options/proxy_test.rb +32 -0
  106. data/test/http/options_test.rb +575 -0
  107. data/test/http/redirector_test.rb +639 -0
  108. data/test/http/request/body_test.rb +318 -0
  109. data/test/http/request/builder_test.rb +623 -0
  110. data/test/http/request/writer_test.rb +391 -0
  111. data/test/http/request_test.rb +1733 -0
  112. data/test/http/response/body_test.rb +292 -0
  113. data/test/http/response/parser_test.rb +105 -0
  114. data/test/http/response/status_test.rb +322 -0
  115. data/test/http/response_test.rb +502 -0
  116. data/test/http/retriable/delay_calculator_test.rb +194 -0
  117. data/test/http/retriable/errors_test.rb +71 -0
  118. data/test/http/retriable/performer_test.rb +551 -0
  119. data/test/http/session_test.rb +424 -0
  120. data/test/http/timeout/global_test.rb +239 -0
  121. data/test/http/timeout/null_test.rb +218 -0
  122. data/test/http/timeout/per_operation_test.rb +220 -0
  123. data/test/http/uri/normalizer_test.rb +89 -0
  124. data/test/http/uri_test.rb +1140 -0
  125. data/test/http/version_test.rb +15 -0
  126. data/test/http_test.rb +818 -0
  127. data/test/regression_tests.rb +27 -0
  128. data/test/support/capture_warning.rb +10 -0
  129. data/test/support/dummy_server/encoding_routes.rb +47 -0
  130. data/test/support/dummy_server/routes.rb +201 -0
  131. data/test/support/dummy_server/servlet.rb +81 -0
  132. data/test/support/dummy_server.rb +200 -0
  133. data/test/support/fakeio.rb +21 -0
  134. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  135. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  136. data/test/support/http_handling_shared.rb +11 -0
  137. data/test/support/proxy_server.rb +207 -0
  138. data/test/support/servers/runner.rb +67 -0
  139. data/test/support/simplecov.rb +28 -0
  140. data/test/support/ssl_helper.rb +108 -0
  141. data/test/test_helper.rb +38 -0
  142. metadata +218 -0
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http/headers"
4
+ require "openssl"
5
+ require "socket"
6
+ require "http/uri"
7
+
8
+ module HTTP
9
+ # Configuration options for HTTP requests and clients
10
+ class Options
11
+ @default_socket_class = TCPSocket
12
+ @default_ssl_socket_class = OpenSSL::SSL::SSLSocket
13
+ @default_timeout_class = HTTP::Timeout::Null
14
+ @available_features = {}
15
+
16
+ class << self
17
+ # Default TCP socket class
18
+ #
19
+ # @example
20
+ # HTTP::Options.default_socket_class # => TCPSocket
21
+ #
22
+ # @return [Class] default socket class
23
+ # @api public
24
+ attr_accessor :default_socket_class
25
+
26
+ # Default SSL socket class
27
+ #
28
+ # @example
29
+ # HTTP::Options.default_ssl_socket_class
30
+ #
31
+ # @return [Class] default SSL socket class
32
+ # @api public
33
+ attr_accessor :default_ssl_socket_class
34
+
35
+ # Default timeout handler class
36
+ #
37
+ # @example
38
+ # HTTP::Options.default_timeout_class
39
+ #
40
+ # @return [Class] default timeout class
41
+ # @api public
42
+ attr_accessor :default_timeout_class
43
+
44
+ # Registered feature implementations
45
+ #
46
+ # @example
47
+ # HTTP::Options.available_features
48
+ #
49
+ # @return [Hash] registered feature implementations
50
+ # @api public
51
+ attr_reader :available_features
52
+
53
+ # Returns existing Options or creates new one
54
+ #
55
+ # @example
56
+ # HTTP::Options.new(response: :auto)
57
+ #
58
+ # @param [HTTP::Options, Hash, nil] options existing Options or Hash to convert
59
+ # @api public
60
+ # @return [HTTP::Options]
61
+ def new(options = nil, **kwargs)
62
+ return options if options.is_a?(self)
63
+
64
+ super(**(options || kwargs)) # steep:ignore
65
+ end
66
+
67
+ # Returns list of defined option names
68
+ #
69
+ # @example
70
+ # HTTP::Options.defined_options
71
+ #
72
+ # @api semipublic
73
+ # @return [Array<Symbol>]
74
+ def defined_options
75
+ @defined_options ||= []
76
+ end
77
+
78
+ # Registers a feature by name and implementation
79
+ #
80
+ # @example
81
+ # HTTP::Options.register_feature(:auto_inflate, AutoInflate)
82
+ #
83
+ # @param [Symbol] name
84
+ # @param [Class] impl
85
+ # @api public
86
+ # @return [Class]
87
+ def register_feature(name, impl)
88
+ @available_features[name] = impl
89
+ end
90
+
91
+ protected
92
+
93
+ # Defines an option with accessor and with_ method
94
+ #
95
+ # @param [Symbol] name
96
+ # @param [Boolean] reader_only
97
+ # @api private
98
+ # @return [void]
99
+ def def_option(name, reader_only: false, &interpreter)
100
+ defined_options << name.to_sym
101
+ interpreter ||= ->(v) { v }
102
+
103
+ def_option_accessor(name, reader_only: reader_only)
104
+
105
+ define_method(:"with_#{name}") do |value|
106
+ dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) } # steep:ignore
107
+ end
108
+ end
109
+
110
+ # Define accessor methods for an option
111
+ #
112
+ # @example
113
+ # def_option_accessor(:timeout, reader_only: false)
114
+ #
115
+ # @return [void]
116
+ # @api private
117
+ def def_option_accessor(name, reader_only:)
118
+ if reader_only
119
+ attr_reader name
120
+ else
121
+ attr_accessor name
122
+ protected :"#{name}="
123
+ end
124
+ end
125
+ end
126
+
127
+ # Initializes options with keyword arguments
128
+ #
129
+ # @example
130
+ # HTTP::Options.new(response: :auto, follow: true)
131
+ #
132
+ # @api public
133
+ # @return [HTTP::Options]
134
+ def initialize(
135
+ response: :auto,
136
+ encoding: nil,
137
+ nodelay: false,
138
+ keep_alive_timeout: 5,
139
+ proxy: {},
140
+ ssl: {},
141
+ headers: {},
142
+ features: {},
143
+ timeout_class: self.class.default_timeout_class,
144
+ timeout_options: {},
145
+ socket_class: self.class.default_socket_class,
146
+ ssl_socket_class: self.class.default_ssl_socket_class,
147
+ params: nil,
148
+ form: nil,
149
+ json: nil,
150
+ body: nil,
151
+ follow: nil,
152
+ retriable: nil,
153
+ base_uri: nil,
154
+ persistent: nil,
155
+ ssl_context: nil
156
+ )
157
+ assign_options(binding)
158
+ end
159
+
160
+ # Merges two Options objects
161
+ #
162
+ # @example
163
+ # opts = HTTP::Options.new.merge(HTTP::Options.new(response: :body))
164
+ #
165
+ # @param [HTTP::Options, Hash] other
166
+ # @api public
167
+ # @return [HTTP::Options]
168
+ def merge(other)
169
+ merged = to_hash.merge(other.to_hash) do |k, v1, v2|
170
+ k == :headers ? v1.merge(v2) : v2
171
+ end
172
+
173
+ self.class.new(**merged)
174
+ end
175
+
176
+ # Converts options to a Hash
177
+ #
178
+ # @example
179
+ # HTTP::Options.new.to_hash
180
+ #
181
+ # @api public
182
+ # @return [Hash]
183
+ def to_hash
184
+ self.class.defined_options.to_h { |opt_name| [opt_name, public_send(opt_name)] }
185
+ end
186
+
187
+ # Duplicates the options object
188
+ #
189
+ # @example
190
+ # opts = HTTP::Options.new
191
+ # opts.dup
192
+ #
193
+ # @api public
194
+ # @return [HTTP::Options]
195
+ def dup
196
+ dupped = super
197
+ yield(dupped) if block_given?
198
+ dupped
199
+ end
200
+
201
+ # Returns a feature by name
202
+ #
203
+ # @example
204
+ # opts = HTTP::Options.new
205
+ # opts.feature(:auto_inflate)
206
+ #
207
+ # @param [Symbol] name
208
+ # @api public
209
+ # @return [Feature, nil]
210
+ def feature(name)
211
+ features[name]
212
+ end
213
+
214
+ private
215
+
216
+ # Assigns all option values from the initialize binding
217
+ #
218
+ # @param [Binding] env binding from initialize with keyword argument values
219
+ # @api private
220
+ # @return [void]
221
+ def assign_options(env)
222
+ self.class.defined_options.each do |name|
223
+ value = env.local_variable_get(name)
224
+ value = Headers.coerce(value) if name.eql?(:headers)
225
+ __send__(:"#{name}=", value)
226
+ end
227
+ end
228
+
229
+ # Raises an argument error with adjusted backtrace
230
+ #
231
+ # @api private
232
+ # @return [void]
233
+ def argument_error!(message)
234
+ error = Error.new(message)
235
+ error.set_backtrace(caller(1) || [])
236
+ raise error
237
+ end
238
+ end
239
+ end
240
+
241
+ require "http/options/definitions"
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http/headers"
4
+
5
+ module HTTP
6
+ # Follows HTTP redirects according to configured policy
7
+ class Redirector
8
+ # Notifies that we reached max allowed redirect hops
9
+ class TooManyRedirectsError < ResponseError; end
10
+
11
+ # Notifies that following redirects got into an endless loop
12
+ class EndlessRedirectError < TooManyRedirectsError; end
13
+
14
+ # HTTP status codes which indicate redirects
15
+ REDIRECT_CODES = [300, 301, 302, 303, 307, 308].to_set.freeze
16
+
17
+ # Codes which which should raise StateError in strict mode if original
18
+ # request was any of {UNSAFE_VERBS}
19
+ STRICT_SENSITIVE_CODES = [300, 301, 302].to_set.freeze
20
+
21
+ # Insecure http verbs, which should trigger StateError in strict mode
22
+ # upon {STRICT_SENSITIVE_CODES}
23
+ UNSAFE_VERBS = %i[put delete post].to_set.freeze
24
+
25
+ # Verbs which will remain unchanged upon See Other response.
26
+ SEE_OTHER_ALLOWED_VERBS = %i[get head].to_set.freeze
27
+
28
+ # Returns redirector policy
29
+ #
30
+ # @example
31
+ # redirector.strict # => true
32
+ #
33
+ # @return [Boolean]
34
+ # @api public
35
+ attr_reader :strict
36
+
37
+ # Returns maximum allowed hops
38
+ #
39
+ # @example
40
+ # redirector.max_hops # => 5
41
+ #
42
+ # @return [Fixnum]
43
+ # @api public
44
+ attr_reader :max_hops
45
+
46
+ # Initializes a new Redirector
47
+ #
48
+ # @example
49
+ # HTTP::Redirector.new(strict: true, max_hops: 5)
50
+ #
51
+ # @param [Boolean] strict (true) redirector hops policy
52
+ # @param [#to_i] max_hops (5) maximum allowed amount of hops
53
+ # @param [#call, nil] on_redirect optional redirect callback
54
+ # @api public
55
+ # @return [HTTP::Redirector]
56
+ def initialize(strict: true, max_hops: 5, on_redirect: nil)
57
+ @strict = strict
58
+ @max_hops = Integer(max_hops)
59
+ @on_redirect = on_redirect
60
+ end
61
+
62
+ # Follows redirects until non-redirect response found
63
+ #
64
+ # @example
65
+ # redirector.perform(request, response) { |req| client.perform(req) }
66
+ #
67
+ # @param [HTTP::Request] request
68
+ # @param [HTTP::Response] response
69
+ # @api public
70
+ # @return [HTTP::Response]
71
+ def perform(request, response, &)
72
+ @request = request
73
+ @response = response
74
+ @visited = []
75
+
76
+ follow_redirects(&) while REDIRECT_CODES.include?(@response.code)
77
+
78
+ @response
79
+ end
80
+
81
+ private
82
+
83
+ # Perform a single redirect step
84
+ #
85
+ # @api private
86
+ # @return [void]
87
+ def follow_redirects
88
+ @visited << visit_key
89
+
90
+ raise TooManyRedirectsError if too_many_hops?
91
+ raise EndlessRedirectError if endless_loop?
92
+
93
+ @response.flush
94
+
95
+ @request = redirect_to(redirect_uri)
96
+ @on_redirect&.call @response, @request
97
+ @response = yield @request
98
+ end
99
+
100
+ # Extracts the redirect URI from the Location header
101
+ #
102
+ # @api private
103
+ # @return [String, nil] URI string or nil if no Location header
104
+ def redirect_uri
105
+ location = @response.headers.get(Headers::LOCATION)
106
+ location.join unless location.empty?
107
+ end
108
+
109
+ # Check if we reached max amount of redirect hops
110
+ #
111
+ # @api private
112
+ # @return [Boolean]
113
+ def too_many_hops?
114
+ @max_hops.positive? && @visited.length > @max_hops
115
+ end
116
+
117
+ # Check if we got into an endless loop
118
+ #
119
+ # @api private
120
+ # @return [Boolean]
121
+ def endless_loop?
122
+ @visited.count(@visited.last) > 1
123
+ end
124
+
125
+ # Build a visit key for the current request
126
+ #
127
+ # Includes verb, URI, and Cookie header so that requests to the same URL
128
+ # with different cookies are not falsely detected as an endless loop.
129
+ #
130
+ # @api private
131
+ # @return [String]
132
+ def visit_key
133
+ "#{@request.verb} #{@request.uri} #{@request.headers[Headers::COOKIE]}"
134
+ end
135
+
136
+ # Redirect policy for follow
137
+ #
138
+ # @api private
139
+ # @return [Request]
140
+ def redirect_to(uri)
141
+ raise StateError, "no Location header in redirect" unless uri
142
+
143
+ verb = @request.verb
144
+ code = @response.code
145
+
146
+ if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
147
+ raise StateError, "can't follow #{@response.status} redirect" if @strict
148
+
149
+ verb = :get
150
+ end
151
+
152
+ verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && code.eql?(303)
153
+
154
+ @request.redirect(uri, verb)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ class Request
5
+ # Represents an HTTP request body with streaming support
6
+ class Body
7
+ # The source data for this body
8
+ #
9
+ # @example
10
+ # body.source # => "hello world"
11
+ #
12
+ # @return [String, Enumerable, IO, nil]
13
+ # @api public
14
+ attr_reader :source
15
+
16
+ # Initialize a new request body
17
+ #
18
+ # @example
19
+ # Body.new("hello world")
20
+ #
21
+ # @return [HTTP::Request::Body]
22
+ # @api public
23
+ def initialize(source)
24
+ @source = source
25
+
26
+ validate_source_type!
27
+ end
28
+
29
+ # Whether the body is empty
30
+ #
31
+ # @example
32
+ # body.empty? # => true
33
+ #
34
+ # @return [Boolean]
35
+ # @api public
36
+ def empty?
37
+ @source.nil?
38
+ end
39
+
40
+ # Whether the body content can be accessed for logging
41
+ #
42
+ # Returns true for String sources (the content can be inspected).
43
+ # Returns false for IO streams and Enumerables (which cannot be
44
+ # read without consuming them), and for nil bodies.
45
+ #
46
+ # The logging feature checks the string encoding separately to
47
+ # decide whether to log the content as text or format it as binary.
48
+ #
49
+ # @example
50
+ # body.loggable? # => true
51
+ #
52
+ # @return [Boolean]
53
+ # @api public
54
+ def loggable?
55
+ @source.is_a?(String)
56
+ end
57
+
58
+ # Returns size for the "Content-Length" header
59
+ #
60
+ # @example
61
+ # body.size
62
+ #
63
+ # @return [Integer]
64
+ # @api public
65
+ def size
66
+ if @source.is_a?(String)
67
+ @source.bytesize
68
+ elsif @source.respond_to?(:read)
69
+ raise RequestError, "IO object must respond to #size" unless @source.respond_to?(:size)
70
+
71
+ @source.size
72
+ elsif @source.nil?
73
+ 0
74
+ else
75
+ raise RequestError,
76
+ "cannot determine size of body: #{@source.inspect}; " \
77
+ "set the Content-Length header explicitly or use chunked Transfer-Encoding"
78
+ end
79
+ end
80
+
81
+ # Yields chunks of content to be streamed
82
+ #
83
+ # @example
84
+ # body.each { |chunk| socket.write(chunk) }
85
+ #
86
+ # @yieldparam [String]
87
+ # @return [self]
88
+ # @api public
89
+ def each(&block)
90
+ if @source.is_a?(String)
91
+ yield @source
92
+ elsif @source.respond_to?(:read)
93
+ IO.copy_stream(@source, ProcIO.new(block))
94
+ rewind(@source)
95
+ elsif @source
96
+ @source.each(&block)
97
+ end
98
+
99
+ self
100
+ end
101
+
102
+ # Check equality based on source
103
+ #
104
+ # @example
105
+ # body == other_body
106
+ #
107
+ # @return [Boolean]
108
+ # @api public
109
+ def ==(other)
110
+ other.is_a?(self.class) && source == other.source
111
+ end
112
+
113
+ private
114
+
115
+ # Rewind an IO source if possible
116
+ # @return [void]
117
+ # @api private
118
+ def rewind(io)
119
+ io.rewind if io.respond_to? :rewind
120
+ rescue Errno::ESPIPE, Errno::EPIPE
121
+ # Pipe IOs respond to `:rewind` but fail when you call it.
122
+ #
123
+ # Calling `IO#rewind` on a pipe, fails with *ESPIPE* on MRI,
124
+ # but *EPIPE* on jRuby.
125
+ #
126
+ # - **ESPIPE** -- "Illegal seek."
127
+ # Invalid seek operation (such as on a pipe).
128
+ #
129
+ # - **EPIPE** -- "Broken pipe."
130
+ # There is no process reading from the other end of a pipe. Every
131
+ # library function that returns this error code also generates
132
+ # a SIGPIPE signal; this signal terminates the program if not handled
133
+ # or blocked. Thus, your program will never actually see EPIPE unless
134
+ # it has handled or blocked SIGPIPE.
135
+ #
136
+ # See: https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
137
+ nil
138
+ end
139
+
140
+ # Validate that source is a supported type
141
+ # @return [void]
142
+ # @api private
143
+ def validate_source_type!
144
+ return if @source.is_a?(String)
145
+ return if @source.respond_to?(:read)
146
+ return if @source.is_a?(Enumerable)
147
+ return if @source.nil?
148
+
149
+ raise RequestError, "body of wrong type: #{@source.class}"
150
+ end
151
+
152
+ # This class provides a "writable IO" wrapper around a proc object, with
153
+ # #write simply calling the proc, which we can pass in as the
154
+ # "destination IO" in IO.copy_stream.
155
+ class ProcIO
156
+ # Initialize a new ProcIO wrapper
157
+ #
158
+ # @example
159
+ # ProcIO.new(block)
160
+ #
161
+ # @return [ProcIO]
162
+ # @api public
163
+ def initialize(block)
164
+ @block = block
165
+ end
166
+
167
+ # Write data by calling the wrapped proc
168
+ #
169
+ # @example
170
+ # proc_io.write("hello")
171
+ #
172
+ # @return [Integer]
173
+ # @api public
174
+ def write(data)
175
+ @block.call(data)
176
+ data.bytesize
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end