async-http 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9747cc8b9879031adf8d9e6086942e69955ff33ca04a598bac893f6a04588680
4
- data.tar.gz: d4dca36355247bfdd1c5162493813ffc2eaf815b1a9d8c6297fed08069a3c15e
3
+ metadata.gz: 4977069f03e3535e44a463522d754edc6c625e24af2e520f7707f8684467145c
4
+ data.tar.gz: 85ec5b67a4ac4aef7c0c04c8226ebb91fd986c1174e34fe90cf65b684eed967d
5
5
  SHA512:
6
- metadata.gz: d3060ef8cf5a1dc5524c8170437cfdc1a027446737ab199daed129d9d5cb8230e3df0b228c4a8259c2b2f70d0221419c1261707097d9f0a32104a13980716aef
7
- data.tar.gz: 9ef10b59d8e2b27295dccc8eef3db0b60052bcf01497d124e1eae16e4d2a9d129e29acd29de71f17ffedf0ae431b2f31b1fa1ba8ec404c88663fc2aecb92982c
6
+ metadata.gz: ce1d3da79cb8f4ded9270f3a48bbde841a4770c43636ef0d6336b61e587c80c93ce3ebe58711c912ffbe2c158ce2ac2c7a20d50203f17d1304bb406eafb41b98
7
+ data.tar.gz: 8f46167cd368f92067805a9d828a3ece309c5ecde97edf4966b26a41d60eb9ee21e09ec5b01a39aea415bb48ce1f42df66bcd9f3d232b7736610b52089939967
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency("async", "~> 1.6")
20
- spec.add_dependency("async-io", "~> 1.9")
20
+ spec.add_dependency("async-io", "~> 1.10")
21
21
 
22
22
  spec.add_dependency("http-2", "~> 0.9.0")
23
23
  # spec.add_dependency("openssl")
@@ -42,6 +42,11 @@ module Async
42
42
 
43
43
  # Read the next available chunk.
44
44
  def read
45
+ # I'm not sure if this is a good idea (*).
46
+ # if @stopped
47
+ # raise @stopped
48
+ # end
49
+
45
50
  return if @finished
46
51
 
47
52
  unless chunk = @queue.dequeue
@@ -58,6 +63,8 @@ module Async
58
63
 
59
64
  # Write a single chunk to the body. Signal completion by calling `#finish`.
60
65
  def write(chunk)
66
+ # If the reader breaks, the writer will break.
67
+ # The inverse of this is less obvious (*)
61
68
  if @stopped
62
69
  raise @stopped
63
70
  end
@@ -27,12 +27,13 @@ require_relative 'middleware'
27
27
  module Async
28
28
  module HTTP
29
29
  class Client
30
- def initialize(endpoint, protocol = nil, authority = nil, **options)
30
+ def initialize(endpoint, protocol = nil, authority = nil, retries: 3, **options)
31
31
  @endpoint = endpoint
32
32
 
33
33
  @protocol = protocol || endpoint.protocol
34
34
  @authority = authority || endpoint.hostname
35
35
 
36
+ @retries = retries
36
37
  @connections = connect(**options)
37
38
  end
38
39
 
@@ -60,21 +61,39 @@ module Async
60
61
 
61
62
  def call(request)
62
63
  request.authority ||= @authority
64
+ attempt = 0
63
65
 
64
- # As we cache connections, it's possible these connections go bad (e.g. closed by remote host). In this case, we need to try again. It's up to the caller to impose a timeout on this.
65
- while true
66
+ begin
67
+ attempt += 1
68
+
69
+ # As we cache connections, it's possible these connections go bad (e.g. closed by remote host). In this case, we need to try again. It's up to the caller to impose a timeout on this.
66
70
  connection = @connections.acquire
67
71
 
68
- if response = connection.call(request)
69
- # The connection won't be released until the body is completely read/released.
70
- Body::Streamable.wrap(response) do
71
- @connections.release(connection)
72
- end
73
-
74
- return response
72
+ response = connection.call(request)
73
+
74
+ # The connection won't be released until the body is completely read/released.
75
+ Body::Streamable.wrap(response) do
76
+ @connections.release(connection)
77
+ end
78
+
79
+ return response
80
+ rescue Protocol::RequestFailed
81
+ # This is a specific case where the entire request wasn't sent before a failure occurred. So, we can even resend non-idempotent requests.
82
+ @connections.release(connection)
83
+
84
+ attempt += 1
85
+ if attempt < @retries
86
+ retry
87
+ else
88
+ raise
89
+ end
90
+ rescue
91
+ @connections.release(connection)
92
+
93
+ if request.idempotent? and attempt < @retries
94
+ retry
75
95
  else
76
- # The connection failed for some reason, we close it.
77
- connection.close
96
+ raise
78
97
  end
79
98
  end
80
99
  end
@@ -25,34 +25,40 @@ module Async
25
25
  module Protocol
26
26
  # Implements basic HTTP/1.1 request/response.
27
27
  class HTTP10 < HTTP11
28
- VERSION = "HTTP/1.0".freeze
29
28
  KEEP_ALIVE = 'keep-alive'.freeze
30
29
 
30
+ VERSION = "HTTP/1.0".freeze
31
+
31
32
  def version
32
33
  VERSION
33
34
  end
34
35
 
35
36
  def persistent?(headers)
36
- headers['connection'] == KEEP_ALIVE
37
+ headers.delete(CONNECTION) == KEEP_ALIVE
37
38
  end
38
39
 
39
40
  # Server loop.
40
- def receive_requests
41
- while request = Request.new(*self.read_request)
41
+ def receive_requests(task: Task.current)
42
+ while @persistent
43
+ request = Request.new(*self.read_request)
44
+
45
+ unless persistent?(request.headers)
46
+ @persistent = false
47
+ end
48
+
42
49
  response = yield request
43
50
 
44
51
  response.version ||= request.version
45
52
 
46
53
  write_response(response.version, response.status, response.headers, response.body)
47
54
 
48
- unless persistent?(request.headers) && persistent?(headers)
49
- @persistent = false
50
-
51
- break
52
- end
55
+ # This ensures we yield at least once every iteration of the loop and allow other fibers to execute.
56
+ task.yield
53
57
  end
54
-
55
- return false
58
+ end
59
+
60
+ def write_persistent_header
61
+ @stream.write("Connection: keep-alive\r\n") if @persistent
56
62
  end
57
63
 
58
64
  def write_body(body, chunked = false)
@@ -66,7 +72,7 @@ module Async
66
72
  end
67
73
 
68
74
  # Technically, with HTTP/1.0, if no content-length is specified, we just need to read everything until the connection is closed.
69
- if !persistent?(headers)
75
+ unless @persistent
70
76
  return Body::Remainder.new(@stream)
71
77
  end
72
78
  end
@@ -20,6 +20,8 @@
20
20
 
21
21
  require 'async/io/protocol/line'
22
22
 
23
+ require_relative 'request_failed'
24
+
23
25
  require_relative '../request'
24
26
  require_relative '../response'
25
27
  require_relative '../headers'
@@ -33,6 +35,11 @@ module Async
33
35
  # Implements basic HTTP/1.1 request/response.
34
36
  class HTTP11 < Async::IO::Protocol::Line
35
37
  CRLF = "\r\n".freeze
38
+ CONNECTION = 'connection'.freeze
39
+ HOST = 'host'.freeze
40
+ CLOSE = 'close'.freeze
41
+
42
+ VERSION = "HTTP/1.1".freeze
36
43
 
37
44
  def initialize(stream)
38
45
  super(stream, CRLF)
@@ -49,7 +56,7 @@ module Async
49
56
  end
50
57
 
51
58
  def reusable?
52
- @persistent
59
+ @persistent && !@stream.closed?
53
60
  end
54
61
 
55
62
  class << self
@@ -57,23 +64,22 @@ module Async
57
64
  alias client new
58
65
  end
59
66
 
60
- CLOSE = 'close'.freeze
61
-
62
- VERSION = "HTTP/1.1".freeze
63
-
64
67
  def version
65
68
  VERSION
66
69
  end
67
70
 
68
71
  def persistent?(headers)
69
- headers['connection'] != CLOSE
72
+ headers.delete(CONNECTION) != CLOSE
70
73
  end
71
74
 
72
75
  # Server loop.
73
76
  def receive_requests(task: Task.current)
74
- while true
77
+ while @persistent
75
78
  request = Request.new(*read_request)
76
- @count += 1
79
+
80
+ unless persistent?(request.headers)
81
+ @persistent = false
82
+ end
77
83
 
78
84
  response = yield request
79
85
 
@@ -83,41 +89,41 @@ module Async
83
89
 
84
90
  request.finish
85
91
 
86
- unless persistent?(request.headers) and persistent?(response.headers)
87
- @persistent = false
88
-
89
- break
90
- end
91
-
92
92
  # This ensures we yield at least once every iteration of the loop and allow other fibers to execute.
93
93
  task.yield
94
94
  end
95
95
  end
96
96
 
97
97
  def call(request)
98
- @count += 1
99
-
100
98
  request.version ||= self.version
101
99
 
102
100
  Async.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"}
103
- write_request(request.authority, request.method, request.path, request.version, request.headers, request.body)
101
+
102
+ # We carefully interpret https://tools.ietf.org/html/rfc7230#section-6.3.1 to implement this correctly.
103
+ begin
104
+ write_request(request.authority, request.method, request.path, request.version, request.headers)
105
+ rescue
106
+ # If we fail to fully write the request and body, we can retry this request.
107
+ raise RequestFailed.new
108
+ end
109
+
110
+ # Once we start writing the body, we can't recover if the request fails. That's because the body might be generated dynamically, streaming, etc.
111
+ write_body(request.body)
104
112
 
105
113
  return Response.new(*read_response)
106
114
  rescue EOFError
107
- Async.logger.debug(self) {"Connection failed with EOFError after #{@count} requests."}
108
- return nil
115
+ # This will ensure that #reusable? returns false.
116
+ @stream.close
117
+
118
+ raise
109
119
  end
110
120
 
111
- def write_request(authority, method, path, version, headers, body)
121
+ def write_request(authority, method, path, version, headers)
112
122
  @stream.write("#{method} #{path} #{version}\r\n")
113
123
  @stream.write("Host: #{authority}\r\n")
114
-
115
124
  write_headers(headers)
116
- write_body(body)
117
125
 
118
126
  @stream.flush
119
-
120
- return true
121
127
  end
122
128
 
123
129
  def read_response
@@ -125,6 +131,8 @@ module Async
125
131
  headers = read_headers
126
132
  body = read_body(headers)
127
133
 
134
+ @count += 1
135
+
128
136
  @persistent = persistent?(headers)
129
137
 
130
138
  return version, Integer(status), reason, headers, body
@@ -135,7 +143,9 @@ module Async
135
143
  headers = read_headers
136
144
  body = read_body(headers)
137
145
 
138
- return headers.delete('host'), method, path, version, headers, body
146
+ @count += 1
147
+
148
+ return headers.delete(HOST), method, path, version, headers, body
139
149
  end
140
150
 
141
151
  def write_response(version, status, headers, body)
@@ -144,16 +154,20 @@ module Async
144
154
  write_body(body)
145
155
 
146
156
  @stream.flush
147
-
148
- return true
149
157
  end
150
158
 
151
159
  protected
152
160
 
161
+ def write_persistent_header
162
+ @stream.write("Connection: close\r\n") unless @persistent
163
+ end
164
+
153
165
  def write_headers(headers)
154
166
  headers.each do |name, value|
155
167
  @stream.write("#{name}: #{value}\r\n")
156
168
  end
169
+
170
+ write_persistent_header
157
171
  end
158
172
 
159
173
  def read_headers
@@ -196,6 +210,8 @@ module Async
196
210
  @stream.write(chunk)
197
211
  end
198
212
  end
213
+
214
+ @stream.flush
199
215
  end
200
216
 
201
217
  def read_body(headers)
@@ -66,6 +66,13 @@ module Async
66
66
  Async.logger.debug(self) {"Received frame: #{frame.inspect}"}
67
67
  end
68
68
 
69
+ @controller.on(:goaway) do |payload|
70
+ Async.logger.error(self) {"goaway: #{payload.inspect}"}
71
+
72
+ @reader.stop
73
+ @stream.io.close
74
+ end
75
+
69
76
  @count = 0
70
77
  end
71
78
 
@@ -77,7 +84,7 @@ module Async
77
84
  end
78
85
 
79
86
  def reusable?
80
- @reader.alive?
87
+ !@stream.closed?
81
88
  end
82
89
 
83
90
  def version
@@ -102,15 +109,14 @@ module Async
102
109
 
103
110
  def close
104
111
  Async.logger.debug(self) {"Closing connection"}
105
- @reader.stop
112
+
113
+ @reader.stop if @reader
106
114
  @stream.close
107
115
  end
108
116
 
109
117
  def receive_requests(task: Task.current, &block)
110
118
  # emits new streams opened by the client
111
119
  @controller.on(:stream) do |stream|
112
- @count += 1
113
-
114
120
  request = Request.new
115
121
  request.version = self.version
116
122
  request.headers = Headers.new
@@ -136,33 +142,40 @@ module Async
136
142
  body.write(chunk.to_s) unless chunk.empty?
137
143
  end
138
144
 
145
+ stream.on(:close) do |error|
146
+ if error
147
+ body.stop(EOFError.new(error))
148
+ end
149
+ end
150
+
139
151
  stream.on(:half_close) do
140
- # puts "Generating response..."
141
- response = yield request
142
-
143
- # puts "Finishing body..."
144
- body.finish
145
-
146
- # puts "Sending response..."
147
- # send response
148
- headers = {STATUS => response.status.to_s}
149
- headers.update(response.headers)
150
-
151
- # puts "Sending headers #{headers}"
152
- if response.body.nil? or response.body.empty?
153
- stream.headers(headers, end_stream: true)
154
- response.body.read if response.body
155
- else
156
- stream.headers(headers, end_stream: false)
152
+ begin
153
+ # We are no longer receiving any more data frames:
154
+ body.finish
155
+
156
+ # Generate the response:
157
+ response = yield request
157
158
 
158
- # puts "Streaming body..."
159
- response.body.each do |chunk|
160
- # puts "Sending chunk #{chunk.inspect}"
161
- stream.data(chunk, end_stream: false)
159
+ headers = {STATUS => response.status.to_s}
160
+ headers.update(response.headers)
161
+
162
+ if response.body.nil? or response.body.empty?
163
+ stream.headers(headers, end_stream: true)
164
+ response.body.read if response.body
165
+ else
166
+ stream.headers(headers, end_stream: false)
167
+
168
+ response.body.each do |chunk|
169
+ stream.data(chunk, end_stream: false)
170
+ end
171
+
172
+ stream.data("", end_stream: true)
162
173
  end
174
+ rescue
175
+ Async.logger.error(self) {$!}
163
176
 
164
- # puts "Ending stream..."
165
- stream.data("", end_stream: true)
177
+ # Generating the response failed.
178
+ stream.close(:internal_error)
166
179
  end
167
180
  end
168
181
  end
@@ -172,11 +185,10 @@ module Async
172
185
  end
173
186
 
174
187
  def call(request)
175
- @count += 1
176
-
177
188
  request.version ||= self.version
178
189
 
179
190
  stream = @controller.new_stream
191
+ @count += 1
180
192
 
181
193
  headers = {
182
194
  SCHEME => HTTPS,
@@ -187,6 +199,7 @@ module Async
187
199
 
188
200
  finished = Async::Notification.new
189
201
 
202
+ exception = nil
190
203
  response = Response.new
191
204
  response.version = self.version
192
205
  response.headers = {}
@@ -204,6 +217,16 @@ module Async
204
217
  end
205
218
  end
206
219
 
220
+ # At this point, we are now expecting two events: data and close.
221
+ stream.on(:close) do |error|
222
+ # If we receive close after this point, it's not a request error, but a failure we need to signal to the body.
223
+ if error
224
+ body.stop(EOFError.new(error))
225
+ else
226
+ body.finish
227
+ end
228
+ end
229
+
207
230
  finished.signal
208
231
  end
209
232
 
@@ -211,15 +234,23 @@ module Async
211
234
  body.write(chunk.to_s) unless chunk.empty?
212
235
  end
213
236
 
214
- stream.on(:close) do
215
- body.finish
237
+ stream.on(:close) do |error|
238
+ # The remote server has closed the connection while we were sending the request.
239
+ if error
240
+ exception = EOFError.new(error)
241
+ finished.signal
242
+ end
216
243
  end
217
244
 
218
245
  if request.body.nil? or request.body.empty?
219
246
  stream.headers(headers, end_stream: true)
220
247
  request.body.read if request.body
221
248
  else
222
- stream.headers(headers, end_stream: false)
249
+ begin
250
+ stream.headers(headers, end_stream: false)
251
+ rescue
252
+ raise RequestFailed.new
253
+ end
223
254
 
224
255
  request.body.each do |chunk|
225
256
  stream.data(chunk, end_stream: false)
@@ -231,10 +262,14 @@ module Async
231
262
  start_connection
232
263
  @stream.flush
233
264
 
234
- # Async.logger.debug(self) {"Stream flushed, waiting for signal."}
265
+ Async.logger.debug(self) {"Stream flushed, waiting for signal."}
235
266
  finished.wait
236
267
 
237
- # Async.logger.debug(self) {"Stream finished: #{response.inspect}"}
268
+ if exception
269
+ raise exception
270
+ end
271
+
272
+ Async.logger.debug(self) {"Stream finished: #{response.inspect}"}
238
273
  return response
239
274
  end
240
275
  end
@@ -0,0 +1,29 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Async
22
+ module HTTP
23
+ module Protocol
24
+ # Failed to send the request. The request body has NOT been consumed (i.e. #read) and you should retry the request.
25
+ class RequestFailed < StandardError
26
+ end
27
+ end
28
+ end
29
+ end
@@ -30,6 +30,10 @@ module Async
30
30
 
31
31
  self.new(nil, method, path, nil, headers, body)
32
32
  end
33
+
34
+ def idempotent?
35
+ method != 'POST' && (body.nil? || body.empty?)
36
+ end
33
37
  end
34
38
  end
35
39
  end
@@ -46,7 +46,7 @@ module Async
46
46
  stream = Async::IO::Stream.new(peer)
47
47
  protocol = @protocol_class.server(stream)
48
48
 
49
- # Async.logger.debug(self) {"Incoming connnection from #{address.inspect}"}
49
+ Async.logger.debug(self) {"Incoming connnection from #{address.inspect} to #{protocol}"}
50
50
 
51
51
  hijack = catch(:hijack) do
52
52
  protocol.receive_requests do |request|
@@ -62,7 +62,7 @@ module Async
62
62
  hijack.call(peer)
63
63
  end
64
64
  rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
65
- # Sometimes client will disconnect without completing a result or reading the entire buffer.
65
+ # Sometimes client will disconnect without completing a result or reading the entire buffer. That means we are done.
66
66
  end
67
67
 
68
68
  def run
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERSION = "0.22.0"
23
+ VERSION = "0.23.0"
24
24
  end
25
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-30 00:00:00.000000000 Z
11
+ date: 2018-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.9'
33
+ version: '1.10'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.9'
40
+ version: '1.10'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: http-2
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -146,6 +146,7 @@ files:
146
146
  - lib/async/http/protocol/http11.rb
147
147
  - lib/async/http/protocol/http2.rb
148
148
  - lib/async/http/protocol/https.rb
149
+ - lib/async/http/protocol/request_failed.rb
149
150
  - lib/async/http/reference.rb
150
151
  - lib/async/http/relative_location.rb
151
152
  - lib/async/http/request.rb