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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +267 -0
- data/CONTRIBUTING.md +26 -0
- data/LICENSE.txt +20 -0
- data/README.md +263 -0
- data/SECURITY.md +17 -0
- data/UPGRADING.md +491 -0
- data/http.gemspec +48 -0
- data/lib/http/base64.rb +22 -0
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +377 -0
- data/lib/http/client.rb +230 -0
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +265 -0
- data/lib/http/content_type.rb +89 -0
- data/lib/http/errors.rb +67 -0
- data/lib/http/feature.rb +86 -0
- data/lib/http/features/auto_deflate.rb +230 -0
- data/lib/http/features/auto_inflate.rb +64 -0
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +149 -0
- data/lib/http/features/logging.rb +231 -0
- data/lib/http/features/normalize_uri.rb +34 -0
- data/lib/http/features/raise_error.rb +37 -0
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +90 -0
- data/lib/http/headers/normalizer.rb +50 -0
- data/lib/http/headers.rb +343 -0
- data/lib/http/mime_type/adapter.rb +43 -0
- data/lib/http/mime_type/json.rb +41 -0
- data/lib/http/mime_type.rb +96 -0
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +241 -0
- data/lib/http/redirector.rb +157 -0
- data/lib/http/request/body.rb +181 -0
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +186 -0
- data/lib/http/request.rb +375 -0
- data/lib/http/response/body.rb +172 -0
- data/lib/http/response/inflater.rb +60 -0
- data/lib/http/response/parser.rb +223 -0
- data/lib/http/response/status/reasons.rb +79 -0
- data/lib/http/response/status.rb +263 -0
- data/lib/http/response.rb +350 -0
- data/lib/http/retriable/delay_calculator.rb +91 -0
- data/lib/http/retriable/errors.rb +35 -0
- data/lib/http/retriable/performer.rb +197 -0
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +229 -0
- data/lib/http/timeout/null.rb +225 -0
- data/lib/http/timeout/per_operation.rb +197 -0
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +376 -0
- data/lib/http/version.rb +6 -0
- data/lib/http.rb +36 -0
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/capture_warning.rb +10 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/test/support/fakeio.rb +21 -0
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/test/support/simplecov.rb +28 -0
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +218 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
require "http/form_data"
|
|
6
|
+
require "http/headers"
|
|
7
|
+
require "http/connection"
|
|
8
|
+
require "http/uri"
|
|
9
|
+
|
|
10
|
+
module HTTP
|
|
11
|
+
class Request
|
|
12
|
+
# Builds HTTP::Request objects from resolved options
|
|
13
|
+
#
|
|
14
|
+
# @example Build a request from options
|
|
15
|
+
# options = HTTP::Options.new(headers: {"Accept" => "application/json"})
|
|
16
|
+
# builder = HTTP::Request::Builder.new(options)
|
|
17
|
+
# request = builder.build(:get, "https://example.com")
|
|
18
|
+
#
|
|
19
|
+
# @see Options
|
|
20
|
+
class Builder
|
|
21
|
+
# Pattern matching HTTP or HTTPS URI schemes
|
|
22
|
+
HTTP_OR_HTTPS_RE = %r{\Ahttps?://}i
|
|
23
|
+
|
|
24
|
+
# Initialize a new Request Builder
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# HTTP::Request::Builder.new(HTTP::Options.new)
|
|
28
|
+
#
|
|
29
|
+
# @param options [HTTP::Options] resolved request options
|
|
30
|
+
# @return [HTTP::Request::Builder]
|
|
31
|
+
# @api public
|
|
32
|
+
def initialize(options)
|
|
33
|
+
@options = options
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build an HTTP request
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# builder.build(:get, "https://example.com")
|
|
40
|
+
#
|
|
41
|
+
# @param verb [Symbol] the HTTP method
|
|
42
|
+
# @param uri [#to_s] the URI to request
|
|
43
|
+
# @return [HTTP::Request] the built request object
|
|
44
|
+
# @api public
|
|
45
|
+
def build(verb, uri)
|
|
46
|
+
uri = make_request_uri(uri)
|
|
47
|
+
headers = make_request_headers
|
|
48
|
+
body = make_request_body(headers)
|
|
49
|
+
|
|
50
|
+
req = HTTP::Request.new(
|
|
51
|
+
verb: verb,
|
|
52
|
+
uri: uri,
|
|
53
|
+
uri_normalizer: @options.feature(:normalize_uri)&.normalizer,
|
|
54
|
+
proxy: @options.proxy,
|
|
55
|
+
headers: headers,
|
|
56
|
+
body: body
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
wrap(req)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Wrap a request through feature middleware
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# builder.wrap(redirect_request)
|
|
66
|
+
#
|
|
67
|
+
# @param request [HTTP::Request] the request to wrap
|
|
68
|
+
# @return [HTTP::Request] the wrapped request
|
|
69
|
+
# @api public
|
|
70
|
+
def wrap(request)
|
|
71
|
+
@options.features.inject(request) do |req, (_name, feature)|
|
|
72
|
+
feature.wrap_request(req)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Merges query params if needed
|
|
79
|
+
#
|
|
80
|
+
# @param uri [#to_s] the URI to process
|
|
81
|
+
# @return [HTTP::URI] the constructed URI
|
|
82
|
+
# @api private
|
|
83
|
+
def make_request_uri(uri)
|
|
84
|
+
uri = uri.to_s
|
|
85
|
+
|
|
86
|
+
if @options.base_uri? && uri !~ HTTP_OR_HTTPS_RE
|
|
87
|
+
uri = resolve_against_base(uri)
|
|
88
|
+
elsif @options.persistent? && uri !~ HTTP_OR_HTTPS_RE
|
|
89
|
+
uri = "#{@options.persistent}#{uri}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
uri = HTTP::URI.parse uri
|
|
93
|
+
|
|
94
|
+
merge_query_params!(uri)
|
|
95
|
+
|
|
96
|
+
# Some proxies (seen on WEBrick) fail if URL has
|
|
97
|
+
# empty path (e.g. `http://example.com`) while it's RFC-compliant:
|
|
98
|
+
# http://tools.ietf.org/html/rfc1738#section-3.1
|
|
99
|
+
uri.path = "/" if uri.path.empty?
|
|
100
|
+
|
|
101
|
+
uri
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Resolve a relative URI against the configured base URI
|
|
105
|
+
#
|
|
106
|
+
# Ensures the base URI path has a trailing slash so that relative
|
|
107
|
+
# paths are appended rather than replacing the last path segment,
|
|
108
|
+
# per the convention described in RFC 3986 Section 5.
|
|
109
|
+
#
|
|
110
|
+
# @param uri [String] the relative URI to resolve
|
|
111
|
+
# @return [String] the resolved absolute URI
|
|
112
|
+
# @api private
|
|
113
|
+
def resolve_against_base(uri)
|
|
114
|
+
base = @options.base_uri or raise Error, "base_uri is not set"
|
|
115
|
+
|
|
116
|
+
unless base.path.end_with?("/")
|
|
117
|
+
base = base.dup
|
|
118
|
+
base.path = "#{base.path}/"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
String(base.join(uri))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Merge query parameters into URI
|
|
125
|
+
#
|
|
126
|
+
# @return [void]
|
|
127
|
+
# @api private
|
|
128
|
+
def merge_query_params!(uri)
|
|
129
|
+
return unless @options.params && !@options.params.empty?
|
|
130
|
+
|
|
131
|
+
existing = ::URI.decode_www_form(uri.query || "")
|
|
132
|
+
uri.query = ::URI.encode_www_form(existing.concat(@options.params.to_a))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Creates request headers
|
|
136
|
+
#
|
|
137
|
+
# @return [HTTP::Headers] the constructed headers
|
|
138
|
+
# @api private
|
|
139
|
+
def make_request_headers
|
|
140
|
+
headers = @options.headers
|
|
141
|
+
|
|
142
|
+
# Tell the server to keep the conn open
|
|
143
|
+
headers[Headers::CONNECTION] = @options.persistent? ? Connection::KEEP_ALIVE : Connection::CLOSE
|
|
144
|
+
|
|
145
|
+
headers
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Create the request body object to send
|
|
149
|
+
#
|
|
150
|
+
# @return [String, HTTP::FormData, nil] the request body
|
|
151
|
+
# @api private
|
|
152
|
+
def make_request_body(headers)
|
|
153
|
+
if @options.body
|
|
154
|
+
@options.body
|
|
155
|
+
elsif @options.form
|
|
156
|
+
form = make_form_data(@options.form)
|
|
157
|
+
headers[Headers::CONTENT_TYPE] ||= form.content_type
|
|
158
|
+
form
|
|
159
|
+
elsif @options.json
|
|
160
|
+
make_json_body(@options.json, headers)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Encode JSON body and set content type header
|
|
165
|
+
# @return [String] the encoded JSON body
|
|
166
|
+
# @api private
|
|
167
|
+
def make_json_body(data, headers)
|
|
168
|
+
body = MimeType[:json].encode data
|
|
169
|
+
headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name.downcase}"
|
|
170
|
+
body
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Coerce form data into an HTTP::FormData object
|
|
174
|
+
# @return [HTTP::FormData::Multipart, HTTP::FormData::Urlencoded] form data
|
|
175
|
+
# @api private
|
|
176
|
+
def make_form_data(form)
|
|
177
|
+
return form if form.is_a? FormData::Multipart
|
|
178
|
+
return form if form.is_a? FormData::Urlencoded
|
|
179
|
+
|
|
180
|
+
FormData.create(form)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP
|
|
4
|
+
class Request
|
|
5
|
+
# Proxy-related methods for HTTP requests
|
|
6
|
+
module Proxy
|
|
7
|
+
# Merges proxy headers into the request headers
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# request.include_proxy_headers
|
|
11
|
+
#
|
|
12
|
+
# @return [void]
|
|
13
|
+
# @api public
|
|
14
|
+
def include_proxy_headers
|
|
15
|
+
headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
|
|
16
|
+
include_proxy_authorization_header if using_authenticated_proxy?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Compute and add the Proxy-Authorization header
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# request.include_proxy_authorization_header
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
# @api public
|
|
26
|
+
def include_proxy_authorization_header
|
|
27
|
+
headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Build the Proxy-Authorization header value
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# request.proxy_authorization_header
|
|
34
|
+
#
|
|
35
|
+
# @return [String]
|
|
36
|
+
# @api public
|
|
37
|
+
def proxy_authorization_header
|
|
38
|
+
digest = encode64(format("%s:%s", proxy.fetch(:proxy_username), proxy.fetch(:proxy_password)))
|
|
39
|
+
"Basic #{digest}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Setup tunnel through proxy for SSL request
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# request.connect_using_proxy(socket)
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
# @api public
|
|
49
|
+
def connect_using_proxy(socket)
|
|
50
|
+
Writer.new(socket, nil, proxy_connect_headers, proxy_connect_header).connect_through_proxy
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Compute HTTP request header SSL proxy connection
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# request.proxy_connect_header
|
|
57
|
+
#
|
|
58
|
+
# @return [String]
|
|
59
|
+
# @api public
|
|
60
|
+
def proxy_connect_header
|
|
61
|
+
"CONNECT #{host}:#{port} HTTP/#{version}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Headers to send with proxy connect request
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# request.proxy_connect_headers
|
|
68
|
+
#
|
|
69
|
+
# @return [HTTP::Headers]
|
|
70
|
+
# @api public
|
|
71
|
+
def proxy_connect_headers
|
|
72
|
+
connect_headers = Headers.coerce(
|
|
73
|
+
Headers::HOST => headers[Headers::HOST],
|
|
74
|
+
Headers::USER_AGENT => headers[Headers::USER_AGENT]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy?
|
|
78
|
+
connect_headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
|
|
79
|
+
connect_headers
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "http/headers"
|
|
4
|
+
|
|
5
|
+
module HTTP
|
|
6
|
+
class Request
|
|
7
|
+
# Streams HTTP requests to a socket
|
|
8
|
+
class Writer
|
|
9
|
+
# CRLF is the universal HTTP delimiter
|
|
10
|
+
CRLF = "\r\n"
|
|
11
|
+
|
|
12
|
+
# Chunked data terminator
|
|
13
|
+
ZERO = "0"
|
|
14
|
+
|
|
15
|
+
# End of a chunked transfer
|
|
16
|
+
CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}".freeze
|
|
17
|
+
|
|
18
|
+
# Initialize a new request writer
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# Writer.new(socket, body, headers, "GET / HTTP/1.1")
|
|
22
|
+
#
|
|
23
|
+
# @return [HTTP::Request::Writer]
|
|
24
|
+
# @api public
|
|
25
|
+
def initialize(socket, body, headers, headline)
|
|
26
|
+
@body = body
|
|
27
|
+
@socket = socket
|
|
28
|
+
@headers = headers
|
|
29
|
+
@request_header = [headline]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Adds headers to the request header array
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# writer.add_headers
|
|
36
|
+
#
|
|
37
|
+
# @return [void]
|
|
38
|
+
# @api public
|
|
39
|
+
def add_headers
|
|
40
|
+
@headers.each do |field, value|
|
|
41
|
+
@request_header << "#{field}: #{value}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Stream the request to a socket
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# writer.stream
|
|
49
|
+
#
|
|
50
|
+
# @return [void]
|
|
51
|
+
# @api public
|
|
52
|
+
def stream
|
|
53
|
+
add_headers
|
|
54
|
+
add_body_type_headers
|
|
55
|
+
send_request
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Send headers needed to connect through proxy
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# writer.connect_through_proxy
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
64
|
+
# @api public
|
|
65
|
+
def connect_through_proxy
|
|
66
|
+
add_headers
|
|
67
|
+
write(join_headers)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Adds content length or transfer encoding headers
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# writer.add_body_type_headers
|
|
74
|
+
#
|
|
75
|
+
# @return [void]
|
|
76
|
+
# @api public
|
|
77
|
+
def add_body_type_headers
|
|
78
|
+
return if @headers[Headers::CONTENT_LENGTH] || chunked? || (
|
|
79
|
+
@body.source.nil? && %w[GET HEAD DELETE CONNECT].any? do |method|
|
|
80
|
+
@request_header.fetch(0).start_with?("#{method} ")
|
|
81
|
+
end
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Joins headers into an HTTP request header string
|
|
88
|
+
#
|
|
89
|
+
# @example
|
|
90
|
+
# writer.join_headers
|
|
91
|
+
#
|
|
92
|
+
# @return [String]
|
|
93
|
+
# @api public
|
|
94
|
+
def join_headers
|
|
95
|
+
# join the headers array with crlfs, stick two on the end because
|
|
96
|
+
# that ends the request header
|
|
97
|
+
@request_header.join(CRLF) + (CRLF * 2)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Writes HTTP request data into the socket
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# writer.send_request
|
|
104
|
+
#
|
|
105
|
+
# @return [void]
|
|
106
|
+
# @api public
|
|
107
|
+
def send_request
|
|
108
|
+
each_chunk { |chunk| write chunk }
|
|
109
|
+
rescue Errno::EPIPE
|
|
110
|
+
# server doesn't need any more data
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Yields chunks of request data for the socket
|
|
115
|
+
#
|
|
116
|
+
# It's important to send the request in a single write call when possible
|
|
117
|
+
# in order to play nicely with Nagle's algorithm. Making two writes in a
|
|
118
|
+
# row triggers a pathological case where Nagle is expecting a third write
|
|
119
|
+
# that never happens.
|
|
120
|
+
#
|
|
121
|
+
# @example
|
|
122
|
+
# writer.each_chunk { |chunk| socket.write(chunk) }
|
|
123
|
+
#
|
|
124
|
+
# @return [void]
|
|
125
|
+
# @api public
|
|
126
|
+
def each_chunk
|
|
127
|
+
data = join_headers
|
|
128
|
+
|
|
129
|
+
@body.each do |chunk|
|
|
130
|
+
data << encode_chunk(chunk)
|
|
131
|
+
yield data
|
|
132
|
+
data.clear
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
yield data unless data.empty?
|
|
136
|
+
|
|
137
|
+
yield CHUNKED_END if chunked?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Returns chunk encoded per Transfer-Encoding header
|
|
141
|
+
#
|
|
142
|
+
# @example
|
|
143
|
+
# writer.encode_chunk("hello")
|
|
144
|
+
#
|
|
145
|
+
# @return [String]
|
|
146
|
+
# @api public
|
|
147
|
+
def encode_chunk(chunk)
|
|
148
|
+
if chunked?
|
|
149
|
+
chunk.bytesize.to_s(16) << CRLF << chunk << CRLF
|
|
150
|
+
else
|
|
151
|
+
chunk
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Returns true if using chunked transfer encoding
|
|
156
|
+
#
|
|
157
|
+
# @example
|
|
158
|
+
# writer.chunked?
|
|
159
|
+
#
|
|
160
|
+
# @return [Boolean]
|
|
161
|
+
# @api public
|
|
162
|
+
def chunked?
|
|
163
|
+
@headers[Headers::TRANSFER_ENCODING].eql?(Headers::CHUNKED)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
# Write data to the underlying socket
|
|
169
|
+
# @return [void]
|
|
170
|
+
# @raise [SocketWriteError] when unable to write to socket
|
|
171
|
+
# @api private
|
|
172
|
+
def write(data)
|
|
173
|
+
until data.empty?
|
|
174
|
+
length = @socket.write(data)
|
|
175
|
+
break unless data.bytesize > length
|
|
176
|
+
|
|
177
|
+
data = data.byteslice(length..-1)
|
|
178
|
+
end
|
|
179
|
+
rescue Errno::EPIPE
|
|
180
|
+
raise
|
|
181
|
+
rescue IOError, SocketError, SystemCallError => e
|
|
182
|
+
raise SocketWriteError, "error writing to socket: #{e}", e.backtrace
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|