raims 0.1.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.
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'timeout'
6
+ require 'zlib'
7
+
8
+ module Raims
9
+ class NetHttpClient
10
+ def execute(method:, uri:, headers:, body:, timeout:, connect_timeout:, max_response_bytes:)
11
+ # An explicit nil proxy disables environment proxy discovery.
12
+ http = Net::HTTP.new(uri.hostname, uri.port, nil)
13
+ http.use_ssl = uri.scheme == 'https'
14
+ if http.use_ssl?
15
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
16
+ http.min_version = OpenSSL::SSL::TLS1_2_VERSION
17
+ end
18
+ http.open_timeout = [timeout, connect_timeout].min
19
+ http.read_timeout = timeout
20
+ http.write_timeout = timeout
21
+ http.max_retries = 0
22
+ request = Net::HTTPGenericRequest.new(method, !body.nil?, method != 'HEAD', uri.request_uri, headers)
23
+ request.body = body unless body.nil?
24
+ result = nil
25
+ metadata = nil
26
+ Timeout.timeout(timeout) do
27
+ http.start do |connection|
28
+ connection.request(request) do |response|
29
+ metadata = ApiResponse.new(response.code.to_i, response.each_header.to_h)
30
+ length = content_length(response, metadata)
31
+ validate_transfer_encoding(response, length, metadata)
32
+ response.ignore_eof = false
33
+ response.content_length = length unless length.nil?
34
+ has_body = method != 'HEAD' && response.class.body_permitted?
35
+ if has_body && length && length > max_response_bytes
36
+ raise UnexpectedResponseError.new('The API response exceeds the supported size.', last_response: metadata)
37
+ end
38
+ bytes = String.new(encoding: Encoding::BINARY)
39
+ begin
40
+ response.read_body do |chunk|
41
+ if bytes.bytesize + chunk.bytesize > max_response_bytes
42
+ raise UnexpectedResponseError.new('The API response exceeds the supported size.', last_response: metadata)
43
+ end
44
+ bytes << chunk
45
+ end
46
+ rescue EOFError
47
+ raise UnexpectedResponseError.new('The API response ended before the HTTP body was complete.', last_response: metadata), cause: nil
48
+ rescue Net::HTTPBadResponse
49
+ raise UnexpectedResponseError.new('The API returned invalid HTTP body framing.', last_response: metadata), cause: nil
50
+ end
51
+ result = HttpResponse.new(status: metadata.status_code, headers: metadata.headers, body: bytes)
52
+ end
53
+ end
54
+ end
55
+ result
56
+ rescue Zlib::Error
57
+ raise UnexpectedResponseError.new('The API returned invalid compressed content.', last_response: metadata), cause: nil
58
+ rescue Net::HTTPHeaderSyntaxError
59
+ raise UnexpectedResponseError.new('The API returned invalid HTTP headers.', last_response: metadata), cause: nil
60
+ end
61
+
62
+ def inspect
63
+ "#<#{self.class}>"
64
+ end
65
+ alias to_s inspect
66
+
67
+ private
68
+
69
+ def content_length(response, metadata)
70
+ fields = response.get_fields('content-length')
71
+ return nil unless fields
72
+ values = fields.flat_map { |field| field.split(',', -1) }
73
+ unless !values.empty? && values.all? { |value| value.match?(/\A[ \t]*[0-9]+[ \t]*\z/) }
74
+ raise UnexpectedResponseError.new('The API returned an invalid Content-Length header.', last_response: metadata)
75
+ end
76
+ lengths = values.map(&:to_i).uniq
77
+ unless lengths.length == 1
78
+ raise UnexpectedResponseError.new('The API returned conflicting Content-Length headers.', last_response: metadata)
79
+ end
80
+ lengths.first
81
+ end
82
+
83
+ def validate_transfer_encoding(response, length, metadata)
84
+ fields = response.get_fields('transfer-encoding')
85
+ return unless fields
86
+ unless length.nil? && fields.length == 1 && fields.first.match?(/\A[ \t]*chunked[ \t]*\z/i)
87
+ raise UnexpectedResponseError.new('The API returned unsupported or ambiguous HTTP transfer framing.', last_response: metadata)
88
+ end
89
+ end
90
+ end
91
+ end