async-http 0.26.0 → 0.27.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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/Rakefile +2 -2
- data/lib/async/http/accept_encoding.rb +1 -1
- data/lib/async/http/body/buffered.rb +0 -12
- data/lib/async/http/body/file.rb +22 -4
- data/lib/async/http/body/fixed.rb +4 -1
- data/lib/async/http/body/readable.rb +1 -1
- data/lib/async/http/body/reader.rb +59 -0
- data/lib/async/http/body/streamable.rb +6 -0
- data/lib/async/http/body/writable.rb +5 -4
- data/lib/async/http/client.rb +11 -8
- data/lib/async/http/content_encoding.rb +1 -1
- data/lib/async/http/middleware.rb +6 -6
- data/lib/async/http/pool.rb +6 -0
- data/lib/async/http/protocol/http11.rb +19 -14
- data/lib/async/http/protocol/http2.rb +96 -57
- data/lib/async/http/protocol/{bad_request.rb → request.rb} +33 -1
- data/lib/async/http/protocol/{request_failed.rb → response.rb} +26 -2
- data/lib/async/http/request.rb +2 -1
- data/lib/async/http/response.rb +2 -1
- data/lib/async/http/server.rb +10 -11
- data/lib/async/http/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46c2f19cb8438e19ef4b02d5a35dc2916c316119682cd022d3eda40f8a0acf55
|
4
|
+
data.tar.gz: 6d9c903d8337af465cb0ce824fada879e83e90063cf8ebf2f99445c4e330b01e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae20e6866313108201bcf25af615131296725836d9bc94b7d816b00abd1416d7ffe098d2614df8c9d285b3bddce965fee0b0cfe9867f1d09b8ef4f4ca517ba8c
|
7
|
+
data.tar.gz: 2c9087a2300ecfed16f8a7977e549511ede35fc83eb283b4f9939219bac7e5f04640bcfe0d061780c1579f5e007eb7e545ac8c2c79504af5efae98e41408b56a
|
data/.travis.yml
CHANGED
data/Rakefile
CHANGED
@@ -21,7 +21,7 @@ task :server do
|
|
21
21
|
require 'async/container/forked'
|
22
22
|
require 'async/http/server'
|
23
23
|
|
24
|
-
server = Async::HTTP::Server.
|
24
|
+
server = Async::HTTP::Server.for(Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true), PROTOCOL) do |request|
|
25
25
|
return Async::HTTP::Response[200, {'content-type' => 'text/plain'}, ["Hello World"]]
|
26
26
|
end
|
27
27
|
|
@@ -58,7 +58,7 @@ task :wrk do
|
|
58
58
|
require 'async/http/server'
|
59
59
|
require 'async/container/forked'
|
60
60
|
|
61
|
-
server = Async::HTTP::Server.
|
61
|
+
server = Async::HTTP::Server.for(Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true), PROTOCOL) do |request|
|
62
62
|
return Async::HTTP::Response[200, {'content-type' => 'text/plain'}, ["Hello World"]]
|
63
63
|
end
|
64
64
|
|
@@ -89,18 +89,6 @@ module Async
|
|
89
89
|
def inspect
|
90
90
|
"\#<#{self.class} #{@chunks.count} chunks, #{self.length} bytes>"
|
91
91
|
end
|
92
|
-
|
93
|
-
module Reader
|
94
|
-
def read
|
95
|
-
self.body ? self.body.join : nil
|
96
|
-
end
|
97
|
-
|
98
|
-
def finish
|
99
|
-
return if self.body.nil?
|
100
|
-
|
101
|
-
self.body = self.body.close
|
102
|
-
end
|
103
|
-
end
|
104
92
|
end
|
105
93
|
end
|
106
94
|
end
|
data/lib/async/http/body/file.rb
CHANGED
@@ -24,13 +24,19 @@ module Async
|
|
24
24
|
module HTTP
|
25
25
|
module Body
|
26
26
|
class File < Readable
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
BLOCK_SIZE = Async::IO::Stream::BLOCK_SIZE
|
28
|
+
|
29
|
+
def self.open(path, *args)
|
30
|
+
self.new(::File.open(path), *args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(file, range = nil, block_size: BLOCK_SIZE)
|
34
|
+
@file = file
|
30
35
|
|
31
36
|
@block_size = block_size
|
32
37
|
|
33
38
|
if range
|
39
|
+
@file.seek(range.min)
|
34
40
|
@offset = range.min
|
35
41
|
@length = @remaining = range.size
|
36
42
|
else
|
@@ -39,12 +45,22 @@ module Async
|
|
39
45
|
end
|
40
46
|
end
|
41
47
|
|
48
|
+
attr :offset
|
42
49
|
attr :length
|
43
50
|
|
44
51
|
def empty?
|
45
52
|
@remaining == 0
|
46
53
|
end
|
47
54
|
|
55
|
+
def rewind
|
56
|
+
@file.seek(@offset)
|
57
|
+
end
|
58
|
+
|
59
|
+
def close
|
60
|
+
@file.close
|
61
|
+
@remaining = 0
|
62
|
+
end
|
63
|
+
|
48
64
|
def read
|
49
65
|
if @remaining > 0
|
50
66
|
amount = [@remaining, @block_size].min
|
@@ -60,6 +76,8 @@ module Async
|
|
60
76
|
end
|
61
77
|
|
62
78
|
def join
|
79
|
+
return "" if @remaining == 0
|
80
|
+
|
63
81
|
buffer = @file.read(@remaining)
|
64
82
|
|
65
83
|
@remaining = 0
|
@@ -68,7 +86,7 @@ module Async
|
|
68
86
|
end
|
69
87
|
|
70
88
|
def inspect
|
71
|
-
"\#<#{self.class}
|
89
|
+
"\#<#{self.class} file=#{@file.inspect} offset=#{@offset} remaining=#{@remaining}>"
|
72
90
|
end
|
73
91
|
end
|
74
92
|
end
|
@@ -38,7 +38,9 @@ module Async
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def stop(error)
|
41
|
-
@
|
41
|
+
if @remaining != 0
|
42
|
+
@stream.close
|
43
|
+
end
|
42
44
|
end
|
43
45
|
|
44
46
|
def read
|
@@ -74,6 +76,7 @@ module Async
|
|
74
76
|
end
|
75
77
|
|
76
78
|
def stop(error)
|
79
|
+
# We can't really do anything in this case except close the connection.
|
77
80
|
@stream.close
|
78
81
|
end
|
79
82
|
|
@@ -25,7 +25,7 @@ module Async
|
|
25
25
|
module Body
|
26
26
|
# A generic base class for wrapping body instances. Typically you'd override `#read`.
|
27
27
|
class Readable
|
28
|
-
#
|
28
|
+
# Read all remaining chunks into a buffered body.
|
29
29
|
def close
|
30
30
|
Buffered.for(self)
|
31
31
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# Copyright, 2018, 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 Body
|
24
|
+
module Reader
|
25
|
+
# Read chunks from the body.
|
26
|
+
def each(&block)
|
27
|
+
self.body.each(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Reads the entire request/response body.
|
31
|
+
def read
|
32
|
+
if self.body
|
33
|
+
self.body.join
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Immediately stop reading the body. May close the underlying connection. Discards body.
|
38
|
+
def stop(error = EOFError)
|
39
|
+
if self.body
|
40
|
+
self.body.stop(error)
|
41
|
+
self.body = nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Gracefully finish reading the body. This will buffer the remainder of the body.
|
46
|
+
def finish
|
47
|
+
if self.body
|
48
|
+
self.body = self.body.close
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Close the connection as quickly as possible. Discards body.
|
53
|
+
def close
|
54
|
+
self.stop
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -36,13 +36,14 @@ module Async
|
|
36
36
|
@stopped = nil
|
37
37
|
end
|
38
38
|
|
39
|
+
# Has the producer called #finish and has the reader consumed the nil token?
|
39
40
|
def empty?
|
40
41
|
@finished
|
41
42
|
end
|
42
43
|
|
43
44
|
# Read the next available chunk.
|
44
45
|
def read
|
45
|
-
# I'm not sure if this is a good idea
|
46
|
+
# I'm not sure if this is a good idea.
|
46
47
|
# if @stopped
|
47
48
|
# raise @stopped
|
48
49
|
# end
|
@@ -56,9 +57,9 @@ module Async
|
|
56
57
|
return chunk
|
57
58
|
end
|
58
59
|
|
59
|
-
#
|
60
|
+
# Stop generating output; cause the next call to write to fail with the given error.
|
60
61
|
def stop(error)
|
61
|
-
@stopped
|
62
|
+
@stopped ||= error
|
62
63
|
end
|
63
64
|
|
64
65
|
# Write a single chunk to the body. Signal completion by calling `#finish`.
|
@@ -77,7 +78,7 @@ module Async
|
|
77
78
|
|
78
79
|
alias << write
|
79
80
|
|
80
|
-
# Signal that output has finished.
|
81
|
+
# Signal that output has finished. This must be called at least once.
|
81
82
|
def finish
|
82
83
|
@queue.enqueue(nil)
|
83
84
|
end
|
data/lib/async/http/client.rb
CHANGED
@@ -34,13 +34,16 @@ module Async
|
|
34
34
|
@authority = authority || endpoint.hostname
|
35
35
|
|
36
36
|
@retries = retries
|
37
|
-
@
|
37
|
+
@pool = connect(**options)
|
38
38
|
end
|
39
39
|
|
40
40
|
attr :endpoint
|
41
41
|
attr :protocol
|
42
42
|
attr :authority
|
43
43
|
|
44
|
+
attr :retries
|
45
|
+
attr :pool
|
46
|
+
|
44
47
|
def self.open(*args, &block)
|
45
48
|
client = self.new(*args)
|
46
49
|
|
@@ -54,10 +57,10 @@ module Async
|
|
54
57
|
end
|
55
58
|
|
56
59
|
def close
|
57
|
-
@
|
60
|
+
@pool.close
|
58
61
|
end
|
59
62
|
|
60
|
-
include
|
63
|
+
include Methods
|
61
64
|
|
62
65
|
def call(request)
|
63
66
|
request.authority ||= @authority
|
@@ -67,20 +70,20 @@ module Async
|
|
67
70
|
begin
|
68
71
|
attempt += 1
|
69
72
|
|
70
|
-
# As we cache
|
71
|
-
connection = @
|
73
|
+
# As we cache pool, it's possible these pool 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. If this is the last attempt, we force a new connection.
|
74
|
+
connection = @pool.acquire
|
72
75
|
|
73
76
|
response = connection.call(request)
|
74
77
|
|
75
78
|
# The connection won't be released until the body is completely read/released.
|
76
79
|
Body::Streamable.wrap(response) do
|
77
|
-
@
|
80
|
+
@pool.release(connection)
|
78
81
|
end
|
79
82
|
|
80
83
|
return response
|
81
84
|
rescue Protocol::RequestFailed
|
82
85
|
# This is a specific case where the entire request wasn't sent before a failure occurred. So, we can even resend non-idempotent requests.
|
83
|
-
@
|
86
|
+
@pool.release(connection)
|
84
87
|
|
85
88
|
attempt += 1
|
86
89
|
if attempt < @retries
|
@@ -89,7 +92,7 @@ module Async
|
|
89
92
|
raise
|
90
93
|
end
|
91
94
|
rescue
|
92
|
-
@
|
95
|
+
@pool.release(connection)
|
93
96
|
|
94
97
|
if request.idempotent? and attempt < @retries
|
95
98
|
retry
|
@@ -30,7 +30,7 @@ module Async
|
|
30
30
|
|
31
31
|
VERBS = [GET, HEAD, POST, PUT, PATCH, DELETE, CONNECT].freeze
|
32
32
|
|
33
|
-
module
|
33
|
+
module Methods
|
34
34
|
VERBS.each do |verb|
|
35
35
|
define_method(verb.downcase) do |location, headers = {}, body = []|
|
36
36
|
self.call(Request[verb, location.to_str, headers, body])
|
@@ -47,17 +47,17 @@ module Async
|
|
47
47
|
@app.close
|
48
48
|
end
|
49
49
|
|
50
|
-
include
|
50
|
+
include Methods
|
51
51
|
|
52
|
-
def call(
|
53
|
-
@app.call(
|
52
|
+
def call(request)
|
53
|
+
@app.call(request)
|
54
54
|
end
|
55
55
|
|
56
56
|
module Okay
|
57
57
|
def self.close
|
58
58
|
end
|
59
59
|
|
60
|
-
def self.call(request
|
60
|
+
def self.call(request)
|
61
61
|
Response[200, {}, []]
|
62
62
|
end
|
63
63
|
end
|
@@ -66,7 +66,7 @@ module Async
|
|
66
66
|
def self.close
|
67
67
|
end
|
68
68
|
|
69
|
-
def self.call(request
|
69
|
+
def self.call(request)
|
70
70
|
Response[200, {'content-type' => 'text/plain'}, ["Hello World!"]]
|
71
71
|
end
|
72
72
|
end
|
data/lib/async/http/pool.rb
CHANGED
@@ -20,12 +20,8 @@
|
|
20
20
|
|
21
21
|
require 'async/io/protocol/line'
|
22
22
|
|
23
|
-
require_relative '
|
24
|
-
require_relative '
|
25
|
-
|
26
|
-
require_relative '../request'
|
27
|
-
require_relative '../response'
|
28
|
-
require_relative '../headers'
|
23
|
+
require_relative 'request'
|
24
|
+
require_relative 'response'
|
29
25
|
|
30
26
|
require_relative '../body/chunked'
|
31
27
|
require_relative '../body/fixed'
|
@@ -52,6 +48,10 @@ module Async
|
|
52
48
|
@count = 0
|
53
49
|
end
|
54
50
|
|
51
|
+
def peer
|
52
|
+
@stream.io
|
53
|
+
end
|
54
|
+
|
55
55
|
attr :count
|
56
56
|
|
57
57
|
# Only one simultaneous connection at a time.
|
@@ -93,15 +93,13 @@ module Async
|
|
93
93
|
return @stream.io
|
94
94
|
end
|
95
95
|
|
96
|
-
class Request <
|
96
|
+
class Request < Protocol::Request
|
97
97
|
def initialize(protocol)
|
98
98
|
super(*protocol.read_request)
|
99
99
|
|
100
100
|
@protocol = protocol
|
101
101
|
end
|
102
102
|
|
103
|
-
attr :protocol
|
104
|
-
|
105
103
|
def hijack?
|
106
104
|
true
|
107
105
|
end
|
@@ -133,7 +131,7 @@ module Async
|
|
133
131
|
def receive_requests(task: Task.current)
|
134
132
|
while request = next_request
|
135
133
|
if response = yield(request, self)
|
136
|
-
write_response(
|
134
|
+
write_response(self.version, response.status, response.headers, response.body)
|
137
135
|
request.finish
|
138
136
|
|
139
137
|
# This ensures we yield at least once every iteration of the loop and allow other fibers to execute.
|
@@ -144,14 +142,21 @@ module Async
|
|
144
142
|
end
|
145
143
|
end
|
146
144
|
|
145
|
+
class Response < Protocol::Response
|
146
|
+
def initialize(protocol, request)
|
147
|
+
super(*protocol.read_response(request))
|
148
|
+
|
149
|
+
@protocol = protocol
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Used by the client to send requests to the remote server.
|
147
154
|
def call(request)
|
148
|
-
request.version ||= self.version
|
149
|
-
|
150
155
|
Async.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"}
|
151
156
|
|
152
157
|
# We carefully interpret https://tools.ietf.org/html/rfc7230#section-6.3.1 to implement this correctly.
|
153
158
|
begin
|
154
|
-
write_request(request.authority, request.method, request.path,
|
159
|
+
write_request(request.authority, request.method, request.path, self.version, request.headers)
|
155
160
|
rescue
|
156
161
|
# If we fail to fully write the request and body, we can retry this request.
|
157
162
|
raise RequestFailed.new
|
@@ -160,7 +165,7 @@ module Async
|
|
160
165
|
# 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.
|
161
166
|
write_body(request.body)
|
162
167
|
|
163
|
-
return Response.new(
|
168
|
+
return Response.new(self, request)
|
164
169
|
rescue
|
165
170
|
# This will ensure that #reusable? returns false.
|
166
171
|
@stream.close
|
@@ -18,10 +18,8 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require_relative '
|
22
|
-
require_relative '
|
23
|
-
require_relative '../headers'
|
24
|
-
require_relative '../body/writable'
|
21
|
+
require_relative 'request'
|
22
|
+
require_relative 'response'
|
25
23
|
|
26
24
|
require_relative 'http11'
|
27
25
|
|
@@ -79,6 +77,10 @@ module Async
|
|
79
77
|
@count = 0
|
80
78
|
end
|
81
79
|
|
80
|
+
def peer
|
81
|
+
@stream.io
|
82
|
+
end
|
83
|
+
|
82
84
|
attr :count
|
83
85
|
|
84
86
|
# Multiple requests can be processed at the same time.
|
@@ -122,13 +124,18 @@ module Async
|
|
122
124
|
@stream.close
|
123
125
|
end
|
124
126
|
|
125
|
-
class Request <
|
126
|
-
def initialize(stream)
|
127
|
+
class Request < Protocol::Request
|
128
|
+
def initialize(protocol, stream)
|
127
129
|
super(nil, nil, nil, VERSION, Headers.new, Body::Writable.new)
|
128
130
|
|
131
|
+
@protocol = protocol
|
129
132
|
@stream = stream
|
130
133
|
end
|
131
134
|
|
135
|
+
def hijack?
|
136
|
+
false
|
137
|
+
end
|
138
|
+
|
132
139
|
attr :stream
|
133
140
|
|
134
141
|
def assign_headers(headers)
|
@@ -150,16 +157,14 @@ module Async
|
|
150
157
|
end
|
151
158
|
end
|
152
159
|
end
|
153
|
-
|
154
|
-
def hijack?
|
155
|
-
false
|
156
|
-
end
|
157
160
|
end
|
158
161
|
|
159
162
|
def receive_requests(task: Task.current, &block)
|
160
163
|
# emits new streams opened by the client
|
161
164
|
@controller.on(:stream) do |stream|
|
162
|
-
|
165
|
+
@count += 1
|
166
|
+
|
167
|
+
request = Request.new(self, stream)
|
163
168
|
body = request.body
|
164
169
|
|
165
170
|
stream.on(:headers) do |headers|
|
@@ -188,7 +193,12 @@ module Async
|
|
188
193
|
end
|
189
194
|
|
190
195
|
stream.on(:close) do |error|
|
191
|
-
|
196
|
+
if error
|
197
|
+
body.stop(EOFError.new(error))
|
198
|
+
else
|
199
|
+
# In theory, we should have received half_close, so there is no need to:
|
200
|
+
# body.finish
|
201
|
+
end
|
192
202
|
end
|
193
203
|
end
|
194
204
|
|
@@ -231,41 +241,50 @@ module Async
|
|
231
241
|
Async.logger.error(request) {$!}
|
232
242
|
end
|
233
243
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
SCHEME => HTTPS,
|
242
|
-
METHOD => request.method,
|
243
|
-
PATH => request.path,
|
244
|
-
AUTHORITY => request.authority,
|
245
|
-
}, request.headers)
|
246
|
-
|
247
|
-
finished = Async::Notification.new
|
248
|
-
|
249
|
-
exception = nil
|
250
|
-
response = Response.new
|
251
|
-
response.version = self.version
|
252
|
-
response.headers = Headers.new
|
253
|
-
body = Body::Writable.new
|
254
|
-
response.body = body
|
244
|
+
class Response < Protocol::Response
|
245
|
+
def initialize(protocol, stream)
|
246
|
+
super(self.version, nil, nil, Headers.new, Body::Writable.new)
|
247
|
+
|
248
|
+
@protocol = protocol
|
249
|
+
@stream = stream
|
250
|
+
end
|
255
251
|
|
256
|
-
|
252
|
+
def assign_headers(headers)
|
257
253
|
headers.each do |key, value|
|
258
254
|
if key == STATUS
|
259
|
-
|
255
|
+
@status = value.to_i
|
260
256
|
elsif key == REASON
|
261
|
-
|
257
|
+
@reason = value
|
262
258
|
else
|
263
|
-
|
259
|
+
@headers[key] = value
|
264
260
|
end
|
265
261
|
end
|
266
|
-
|
267
|
-
|
268
|
-
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Used by the client to send requests to the remote server.
|
266
|
+
def call(request)
|
267
|
+
@count += 1
|
268
|
+
|
269
|
+
stream = @controller.new_stream
|
270
|
+
response = Response.new(self, stream)
|
271
|
+
body = response.body
|
272
|
+
|
273
|
+
exception = nil
|
274
|
+
finished = Async::Notification.new
|
275
|
+
waiting = true
|
276
|
+
|
277
|
+
stream.on(:close) do |error|
|
278
|
+
if waiting
|
279
|
+
if error
|
280
|
+
# If the stream was closed due to an error, we will raise it rather than returning normally.
|
281
|
+
exception = EOFError.new(error)
|
282
|
+
end
|
283
|
+
|
284
|
+
waiting = false
|
285
|
+
finished.signal
|
286
|
+
else
|
287
|
+
# At this point, we are now expecting two events: data and close.
|
269
288
|
# If we receive close after this point, it's not a request error, but a failure we need to signal to the body.
|
270
289
|
if error
|
271
290
|
body.stop(EOFError.new(error))
|
@@ -273,22 +292,52 @@ module Async
|
|
273
292
|
body.finish
|
274
293
|
end
|
275
294
|
end
|
295
|
+
end
|
296
|
+
|
297
|
+
stream.on(:headers) do |headers|
|
298
|
+
response.assign_headers(headers)
|
276
299
|
|
300
|
+
# Once we receive the headers, we can return. The body will be read in the background.
|
301
|
+
waiting = false
|
277
302
|
finished.signal
|
278
303
|
end
|
279
304
|
|
305
|
+
# This is a little bit tricky due to the event handlers.
|
306
|
+
# 1/ Caller invokes `response.stop` which causes `body.write` below to fail.
|
307
|
+
# 2/ We invoke `stream.close(:internal_error)` which eventually triggers `on(:close)` above.
|
308
|
+
# 3/ Error is set to :internal_error which causes us to call `body.stop` a 2nd time.
|
309
|
+
# So, we guard against that, by ensuring that `Writable#stop` only stores the first exception assigned to it.
|
280
310
|
stream.on(:data) do |chunk|
|
281
|
-
|
311
|
+
begin
|
312
|
+
# If the body is stopped, write will fail...
|
313
|
+
body.write(chunk.to_s) unless chunk.empty?
|
314
|
+
rescue
|
315
|
+
# ... so, we close the stream:
|
316
|
+
stream.close(:internal_error)
|
317
|
+
end
|
282
318
|
end
|
283
319
|
|
284
|
-
stream
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
320
|
+
write_request(request, stream)
|
321
|
+
|
322
|
+
Async.logger.debug(self) {"Request sent, waiting for signal."}
|
323
|
+
finished.wait
|
324
|
+
|
325
|
+
if exception
|
326
|
+
raise exception
|
290
327
|
end
|
291
328
|
|
329
|
+
Async.logger.debug(self) {"Stream finished: #{response.inspect}"}
|
330
|
+
return response
|
331
|
+
end
|
332
|
+
|
333
|
+
private def write_request(request, stream)
|
334
|
+
headers = Headers::Merged.new({
|
335
|
+
SCHEME => HTTPS,
|
336
|
+
METHOD => request.method,
|
337
|
+
PATH => request.path,
|
338
|
+
AUTHORITY => request.authority,
|
339
|
+
}, request.headers)
|
340
|
+
|
292
341
|
if request.body.nil? or request.body.empty?
|
293
342
|
stream.headers(headers, end_stream: true)
|
294
343
|
request.body.read if request.body
|
@@ -308,16 +357,6 @@ module Async
|
|
308
357
|
|
309
358
|
start_connection
|
310
359
|
@stream.flush
|
311
|
-
|
312
|
-
Async.logger.debug(self) {"Stream flushed, waiting for signal."}
|
313
|
-
finished.wait
|
314
|
-
|
315
|
-
if exception
|
316
|
-
raise exception
|
317
|
-
end
|
318
|
-
|
319
|
-
Async.logger.debug(self) {"Stream finished: #{response.inspect}"}
|
320
|
-
return response
|
321
360
|
end
|
322
361
|
end
|
323
362
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright,
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the "Software"), to deal
|
@@ -18,12 +18,44 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
+
require_relative '../request'
|
22
|
+
require_relative '../headers'
|
23
|
+
|
24
|
+
require_relative '../body/writable'
|
25
|
+
|
21
26
|
module Async
|
22
27
|
module HTTP
|
23
28
|
module Protocol
|
29
|
+
# Failed to send the request. The request body has NOT been consumed (i.e. #read) and you should retry the request.
|
30
|
+
class RequestFailed < StandardError
|
31
|
+
end
|
32
|
+
|
24
33
|
# The request was invalid/malformed in some way.
|
25
34
|
class BadRequest < StandardError
|
26
35
|
end
|
36
|
+
|
37
|
+
# This is generated by server protocols.
|
38
|
+
class Request < HTTP::Request
|
39
|
+
attr :protocol
|
40
|
+
|
41
|
+
def hijack?
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
def peer
|
46
|
+
if @protocol
|
47
|
+
@protocol.peer
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def remote_address
|
52
|
+
@remote_address ||= peer.remote_address
|
53
|
+
end
|
54
|
+
|
55
|
+
def remote_address= value
|
56
|
+
@remote_address = value
|
57
|
+
end
|
58
|
+
end
|
27
59
|
end
|
28
60
|
end
|
29
61
|
end
|
@@ -18,11 +18,35 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
+
require_relative '../response'
|
22
|
+
require_relative '../headers'
|
23
|
+
|
24
|
+
require_relative '../body/writable'
|
25
|
+
|
21
26
|
module Async
|
22
27
|
module HTTP
|
23
28
|
module Protocol
|
24
|
-
#
|
25
|
-
class
|
29
|
+
# This is generated by client protocols.
|
30
|
+
class Response < HTTP::Response
|
31
|
+
attr :protocol
|
32
|
+
|
33
|
+
def hijack?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def peer
|
38
|
+
if @protocol
|
39
|
+
@protocol.peer
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def remote_address
|
44
|
+
@remote_address ||= peer.remote_address
|
45
|
+
end
|
46
|
+
|
47
|
+
def remote_address= value
|
48
|
+
@remote_address = value
|
49
|
+
end
|
26
50
|
end
|
27
51
|
end
|
28
52
|
end
|
data/lib/async/http/request.rb
CHANGED
@@ -19,12 +19,13 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require_relative 'body/buffered'
|
22
|
+
require_relative 'body/reader'
|
22
23
|
require_relative 'middleware'
|
23
24
|
|
24
25
|
module Async
|
25
26
|
module HTTP
|
26
27
|
class Request
|
27
|
-
prepend Body::
|
28
|
+
prepend Body::Reader
|
28
29
|
|
29
30
|
def initialize(authority = nil, method = nil, path = nil, version = nil, headers = [], body = nil)
|
30
31
|
@authority = authority
|
data/lib/async/http/response.rb
CHANGED
@@ -19,11 +19,12 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require_relative 'body/buffered'
|
22
|
+
require_relative 'body/reader'
|
22
23
|
|
23
24
|
module Async
|
24
25
|
module HTTP
|
25
26
|
class Response
|
26
|
-
prepend Body::
|
27
|
+
prepend Body::Reader
|
27
28
|
|
28
29
|
def initialize(version = nil, status = 200, reason = nil, headers = [], body = nil)
|
29
30
|
@version = version
|
data/lib/async/http/server.rb
CHANGED
@@ -26,18 +26,16 @@ require_relative 'response'
|
|
26
26
|
|
27
27
|
module Async
|
28
28
|
module HTTP
|
29
|
-
class Server
|
30
|
-
def
|
31
|
-
|
32
|
-
@protocol_class = protocol_class || endpoint.protocol
|
33
|
-
|
34
|
-
if block_given?
|
35
|
-
define_singleton_method(:handle_request, block)
|
36
|
-
end
|
29
|
+
class Server < Middleware
|
30
|
+
def self.for(*args, &block)
|
31
|
+
self.new(block, *args)
|
37
32
|
end
|
38
33
|
|
39
|
-
def
|
40
|
-
|
34
|
+
def initialize(app, endpoint, protocol_class = Protocol::HTTP1)
|
35
|
+
super(app)
|
36
|
+
|
37
|
+
@endpoint = endpoint
|
38
|
+
@protocol_class = protocol_class || endpoint.protocol
|
41
39
|
end
|
42
40
|
|
43
41
|
def accept(peer, address, task: Task.current)
|
@@ -49,10 +47,11 @@ module Async
|
|
49
47
|
Async.logger.debug(self) {"Incoming connnection from #{address.inspect} to #{protocol}"}
|
50
48
|
|
51
49
|
protocol.receive_requests do |request|
|
50
|
+
request.remote_address = address
|
52
51
|
# Async.logger.debug(self) {"Incoming request from #{address.inspect}: #{request.method} #{request.path}"}
|
53
52
|
|
54
53
|
# If this returns nil, we assume that the connection has been hijacked.
|
55
|
-
|
54
|
+
self.call(request)
|
56
55
|
end
|
57
56
|
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
|
58
57
|
# Sometimes client will disconnect without completing a result or reading the entire buffer. That means we are done.
|
data/lib/async/http/version.rb
CHANGED
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.
|
4
|
+
version: 0.27.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-06-
|
11
|
+
date: 2018-06-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async
|
@@ -146,6 +146,7 @@ files:
|
|
146
146
|
- lib/async/http/body/fixed.rb
|
147
147
|
- lib/async/http/body/inflate.rb
|
148
148
|
- lib/async/http/body/readable.rb
|
149
|
+
- lib/async/http/body/reader.rb
|
149
150
|
- lib/async/http/body/rewindable.rb
|
150
151
|
- lib/async/http/body/streamable.rb
|
151
152
|
- lib/async/http/body/wrapper.rb
|
@@ -157,13 +158,13 @@ files:
|
|
157
158
|
- lib/async/http/middleware/builder.rb
|
158
159
|
- lib/async/http/pool.rb
|
159
160
|
- lib/async/http/protocol.rb
|
160
|
-
- lib/async/http/protocol/bad_request.rb
|
161
161
|
- lib/async/http/protocol/http1.rb
|
162
162
|
- lib/async/http/protocol/http10.rb
|
163
163
|
- lib/async/http/protocol/http11.rb
|
164
164
|
- lib/async/http/protocol/http2.rb
|
165
165
|
- lib/async/http/protocol/https.rb
|
166
|
-
- lib/async/http/protocol/
|
166
|
+
- lib/async/http/protocol/request.rb
|
167
|
+
- lib/async/http/protocol/response.rb
|
167
168
|
- lib/async/http/reference.rb
|
168
169
|
- lib/async/http/relative_location.rb
|
169
170
|
- lib/async/http/request.rb
|