async-http 0.35.1 → 0.36.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/README.md +14 -0
- data/async-http.gemspec +1 -1
- data/examples/request.rb +1 -1
- data/lib/async/http/accept_encoding.rb +7 -3
- data/lib/async/http/body/inflate.rb +6 -1
- data/lib/async/http/body/writable.rb +1 -1
- data/lib/async/http/client.rb +12 -7
- data/lib/async/http/internet.rb +1 -1
- data/lib/async/http/middleware.rb +4 -2
- data/lib/async/http/protocol/http1/connection.rb +8 -0
- data/lib/async/http/protocol/http1/request.rb +1 -1
- data/lib/async/http/protocol/http1/server.rb +3 -3
- data/lib/async/http/protocol/http1.rb +8 -5
- data/lib/async/http/protocol/http2/client.rb +2 -2
- data/lib/async/http/protocol/http2/request.rb +10 -6
- data/lib/async/http/protocol/http2/response.rb +13 -8
- data/lib/async/http/protocol/http2/server.rb +2 -2
- data/lib/async/http/request.rb +8 -2
- data/lib/async/http/server.rb +12 -3
- data/lib/async/http/url_endpoint.rb +4 -0
- data/lib/async/http/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6e1e7f6525a09868ce9ad825b3b58b7b64f32c5df8ed580c8f77d575cb709f4
|
4
|
+
data.tar.gz: dc532ce80d64d36907a8436d22acc6be3cb0d38833b0bd99cdaf1223a1d0248b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84dd9b7d698be6596c5658be91e05cfaa18bb17125973300e330291d05bb85fa1b3a7084a099dd7d1334d520dae5e45f71791d8c40402f6424e666b1b17c7c7e
|
7
|
+
data.tar.gz: 878de7d6275b62fbb1a63f3317fefaaa2e3d689f3d723f8a4d4dbe75a770503c8d52d3855ce33bb07d07a21aae146f6caf2bfb14afa2d67f8b4ae6da7bdc9d9c
|
data/README.md
CHANGED
@@ -139,6 +139,20 @@ Transfer/sec: 5.98MB
|
|
139
139
|
|
140
140
|
According to these results, the cost of handling connections is quite high, while general throughput seems pretty decent.
|
141
141
|
|
142
|
+
## Semantic Model
|
143
|
+
|
144
|
+
### Scheme
|
145
|
+
|
146
|
+
HTTP/1 has an implicit scheme determined by the kind of connection made to the server (either `http` or `https`), while HTTP/2 models this explicitly and the client indicates this in the request using the `:scheme` pseudo-header (typically `https`). To normalize this, `Async::HTTP::Client` and `Async::HTTP::Server` have a default scheme which is used if none is supplied.
|
147
|
+
|
148
|
+
### Version
|
149
|
+
|
150
|
+
HTTP/1 has an explicit version while HTTP/2 does not expose the version in any way.
|
151
|
+
|
152
|
+
### Reason
|
153
|
+
|
154
|
+
HTTP/1 responses contain a reason field which is largely irrelevant. HTTP/2 does not support this field.
|
155
|
+
|
142
156
|
## Contributing
|
143
157
|
|
144
158
|
1. Fork it
|
data/async-http.gemspec
CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.add_dependency("async", "~> 1.6")
|
20
20
|
spec.add_dependency("async-io", "~> 1.16")
|
21
21
|
|
22
|
-
spec.add_dependency("http-protocol", "~> 0.
|
22
|
+
spec.add_dependency("http-protocol", "~> 0.8.0")
|
23
23
|
|
24
24
|
# spec.add_dependency("openssl")
|
25
25
|
|
data/examples/request.rb
CHANGED
@@ -19,7 +19,7 @@ Async.run do |task|
|
|
19
19
|
'accept' => 'text/html',
|
20
20
|
}
|
21
21
|
|
22
|
-
request = Async::HTTP::Request.new("www.google.com", "GET", "/search?q=cats", headers)
|
22
|
+
request = Async::HTTP::Request.new(client.scheme, "www.google.com", "GET", "/search?q=cats", headers)
|
23
23
|
|
24
24
|
puts "Sending request..."
|
25
25
|
response = client.call(request)
|
@@ -27,8 +27,12 @@ module Async
|
|
27
27
|
module HTTP
|
28
28
|
# Set a valid accept-encoding header and decode the response.
|
29
29
|
class AcceptEncoding < Middleware
|
30
|
+
ACCEPT_ENCODING = 'accept-encoding'.freeze
|
31
|
+
CONTENT_ENCODING = 'content-encoding'.freeze
|
32
|
+
|
30
33
|
DEFAULT_WRAPPERS = {
|
31
|
-
'gzip' => Body::Inflate.method(:for)
|
34
|
+
'gzip' => Body::Inflate.method(:for),
|
35
|
+
'identity' => ->(body){body},
|
32
36
|
}
|
33
37
|
|
34
38
|
def initialize(app, wrappers = DEFAULT_WRAPPERS)
|
@@ -39,11 +43,11 @@ module Async
|
|
39
43
|
end
|
40
44
|
|
41
45
|
def call(request)
|
42
|
-
request.headers[
|
46
|
+
request.headers[ACCEPT_ENCODING] = @accept_encoding
|
43
47
|
|
44
48
|
response = super
|
45
49
|
|
46
|
-
if !response.body.empty? and content_encoding = response.headers
|
50
|
+
if !response.body.empty? and content_encoding = response.headers.delete(CONTENT_ENCODING)
|
47
51
|
body = response.body
|
48
52
|
|
49
53
|
# We want to unwrap all encodings
|
@@ -36,6 +36,7 @@ module Async
|
|
36
36
|
if chunk = super
|
37
37
|
@input_length += chunk.bytesize
|
38
38
|
|
39
|
+
# It's possible this triggers the stream to finish.
|
39
40
|
chunk = @stream.inflate(chunk)
|
40
41
|
|
41
42
|
@output_length += chunk.bytesize
|
@@ -47,7 +48,11 @@ module Async
|
|
47
48
|
@stream.close
|
48
49
|
end
|
49
50
|
|
50
|
-
|
51
|
+
if @stream.finished? and chunk.empty?
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
|
55
|
+
return chunk
|
51
56
|
end
|
52
57
|
end
|
53
58
|
end
|
data/lib/async/http/client.rb
CHANGED
@@ -28,23 +28,26 @@ require_relative 'middleware'
|
|
28
28
|
module Async
|
29
29
|
module HTTP
|
30
30
|
class Client
|
31
|
-
def initialize(endpoint, protocol = endpoint.protocol, authority = endpoint.
|
31
|
+
def initialize(endpoint, protocol = endpoint.protocol, scheme = endpoint.scheme, authority = endpoint.authority, retries: 3, connection_limit: nil)
|
32
32
|
@endpoint = endpoint
|
33
|
-
|
34
33
|
@protocol = protocol
|
35
|
-
@authority = authority
|
36
34
|
|
37
35
|
@retries = retries
|
38
|
-
@pool =
|
36
|
+
@pool = make_pool(connection_limit)
|
37
|
+
|
38
|
+
@scheme = scheme
|
39
|
+
@authority = authority
|
39
40
|
end
|
40
41
|
|
41
42
|
attr :endpoint
|
42
43
|
attr :protocol
|
43
|
-
attr :authority
|
44
44
|
|
45
45
|
attr :retries
|
46
46
|
attr :pool
|
47
47
|
|
48
|
+
attr :scheme
|
49
|
+
attr :authority
|
50
|
+
|
48
51
|
def self.open(*args, &block)
|
49
52
|
client = self.new(*args)
|
50
53
|
|
@@ -64,7 +67,9 @@ module Async
|
|
64
67
|
include Methods
|
65
68
|
|
66
69
|
def call(request)
|
67
|
-
request.
|
70
|
+
request.scheme ||= self.scheme
|
71
|
+
request.authority ||= self.authority
|
72
|
+
|
68
73
|
attempt = 0
|
69
74
|
|
70
75
|
# We may retry the request if it is possible to do so. https://tools.ietf.org/html/draft-nottingham-httpbis-retry-01 is a good guide for how retrying requests should work.
|
@@ -105,7 +110,7 @@ module Async
|
|
105
110
|
|
106
111
|
protected
|
107
112
|
|
108
|
-
def
|
113
|
+
def make_pool(connection_limit = nil)
|
109
114
|
Pool.new(connection_limit) do
|
110
115
|
Async.logger.debug(self) {"Making connection to #{@endpoint.inspect}"}
|
111
116
|
|
data/lib/async/http/internet.rb
CHANGED
@@ -39,7 +39,7 @@ module Async
|
|
39
39
|
|
40
40
|
body = Body::Buffered.wrap(body)
|
41
41
|
|
42
|
-
request = Request.new(endpoint.authority, method, endpoint.path, nil, headers, body)
|
42
|
+
request = Request.new(client.scheme, endpoint.authority, method, endpoint.path, nil, headers, body)
|
43
43
|
|
44
44
|
return client.call(request)
|
45
45
|
end
|
@@ -32,8 +32,10 @@ module Async
|
|
32
32
|
|
33
33
|
module Methods
|
34
34
|
VERBS.each do |verb|
|
35
|
-
define_method(verb.downcase) do |location, headers = [], body =
|
36
|
-
self.call(
|
35
|
+
define_method(verb.downcase) do |location, headers = [], body = nil|
|
36
|
+
self.call(
|
37
|
+
Request[verb, location.to_str, headers, body]
|
38
|
+
)
|
37
39
|
end
|
38
40
|
end
|
39
41
|
end
|
@@ -31,6 +31,14 @@ module Async
|
|
31
31
|
module Protocol
|
32
32
|
module HTTP1
|
33
33
|
class Connection < ::HTTP::Protocol::HTTP1::Connection
|
34
|
+
def initialize(stream, version)
|
35
|
+
super(stream)
|
36
|
+
|
37
|
+
@version = version
|
38
|
+
end
|
39
|
+
|
40
|
+
attr :version
|
41
|
+
|
34
42
|
CRLF = "\r\n"
|
35
43
|
|
36
44
|
attr :stream
|
@@ -38,7 +38,7 @@ module Async
|
|
38
38
|
return request
|
39
39
|
rescue
|
40
40
|
# Bad Request
|
41
|
-
write_response(
|
41
|
+
write_response(@version, 400, {}, nil)
|
42
42
|
|
43
43
|
raise
|
44
44
|
end
|
@@ -51,10 +51,10 @@ module Async
|
|
51
51
|
return if @stream.closed?
|
52
52
|
|
53
53
|
if response
|
54
|
-
write_response(
|
54
|
+
write_response(@version, response.status, response.headers, response.body, request.head?)
|
55
55
|
else
|
56
56
|
# If the request failed to generate a response, it was an internal server error:
|
57
|
-
write_response(
|
57
|
+
write_response(@version, 500, {}, nil)
|
58
58
|
end
|
59
59
|
|
60
60
|
# Gracefully finish reading the request body if it was not already done so.
|
@@ -24,14 +24,17 @@ require_relative 'http1/server'
|
|
24
24
|
module Async
|
25
25
|
module HTTP
|
26
26
|
module Protocol
|
27
|
-
# A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request.
|
28
27
|
module HTTP1
|
29
|
-
|
30
|
-
|
28
|
+
VERSION = "HTTP/1.1"
|
29
|
+
|
30
|
+
def self.client(stream)
|
31
|
+
Client.new(stream, VERSION)
|
31
32
|
end
|
32
33
|
|
33
|
-
|
34
|
-
|
34
|
+
# A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request.
|
35
|
+
# TODO Verify correct behaviour.
|
36
|
+
def self.server(stream)
|
37
|
+
Server.new(stream, VERSION)
|
35
38
|
end
|
36
39
|
end
|
37
40
|
end
|
@@ -30,12 +30,12 @@ module Async
|
|
30
30
|
class Client < ::HTTP::Protocol::HTTP2::Client
|
31
31
|
include Connection
|
32
32
|
|
33
|
-
def initialize(stream
|
33
|
+
def initialize(stream)
|
34
34
|
@stream = stream
|
35
35
|
|
36
36
|
framer = ::HTTP::Protocol::HTTP2::Framer.new(@stream)
|
37
37
|
|
38
|
-
super(framer
|
38
|
+
super(framer)
|
39
39
|
end
|
40
40
|
|
41
41
|
# Used by the client to send requests to the remote server.
|
@@ -29,7 +29,7 @@ module Async
|
|
29
29
|
def initialize(protocol, stream_id)
|
30
30
|
@input = Body::Writable.new
|
31
31
|
|
32
|
-
super(nil, nil, nil, VERSION, Headers.new, @input)
|
32
|
+
super(nil, nil, nil, nil, VERSION, Headers.new, @input)
|
33
33
|
|
34
34
|
@protocol = protocol
|
35
35
|
@stream = Stream.new(self, protocol, stream_id)
|
@@ -43,7 +43,15 @@ module Async
|
|
43
43
|
|
44
44
|
def receive_headers(stream, headers, end_stream)
|
45
45
|
headers.each do |key, value|
|
46
|
-
if key ==
|
46
|
+
if key == SCHEME
|
47
|
+
return @stream.send_failure(400, "Request scheme already specified") if @scheme
|
48
|
+
|
49
|
+
@scheme = value
|
50
|
+
elsif key == AUTHORITY
|
51
|
+
return @stream.send_failure(400, "Request authority already specified") if @authority
|
52
|
+
|
53
|
+
@authority = value
|
54
|
+
elsif key == METHOD
|
47
55
|
return @stream.send_failure(400, "Request method already specified") if @method
|
48
56
|
|
49
57
|
@method = value
|
@@ -51,10 +59,6 @@ module Async
|
|
51
59
|
return @stream.send_failure(400, "Request path already specified") if @path
|
52
60
|
|
53
61
|
@path = value
|
54
|
-
elsif key == AUTHORITY
|
55
|
-
return @stream.send_failure(400, "Request authority already specified") if @authority
|
56
|
-
|
57
|
-
@authority = value
|
58
62
|
else
|
59
63
|
@headers[key] = value
|
60
64
|
end
|
@@ -38,6 +38,15 @@ module Async
|
|
38
38
|
@exception = nil
|
39
39
|
end
|
40
40
|
|
41
|
+
# Notify anyone waiting on the response headers to be received (or failure).
|
42
|
+
protected def notify!
|
43
|
+
if @notification
|
44
|
+
@notification.signal
|
45
|
+
@notification = nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Wait for the headers to be received or for stream reset.
|
41
50
|
def wait
|
42
51
|
# If you call wait after the headers were already received, it should return immediately.
|
43
52
|
if @notification
|
@@ -67,11 +76,7 @@ module Async
|
|
67
76
|
@body = @input = Body::Writable.new(@length)
|
68
77
|
end
|
69
78
|
|
70
|
-
|
71
|
-
if @notification
|
72
|
-
@notification.signal
|
73
|
-
@notification = nil
|
74
|
-
end
|
79
|
+
notify!
|
75
80
|
end
|
76
81
|
|
77
82
|
def receive_data(stream, data, end_stream)
|
@@ -88,10 +93,10 @@ module Async
|
|
88
93
|
|
89
94
|
def receive_reset_stream(stream, error_code)
|
90
95
|
if error_code > 0
|
91
|
-
@exception = EOFError.new(error_code)
|
96
|
+
@exception = EOFError.new("Stream reset: error_code=#{error_code}")
|
92
97
|
end
|
93
98
|
|
94
|
-
|
99
|
+
notify!
|
95
100
|
end
|
96
101
|
|
97
102
|
# Send a request and read it into this response.
|
@@ -99,7 +104,7 @@ module Async
|
|
99
104
|
# https://http2.github.io/http2-spec/#rfc.section.8.1.2.3
|
100
105
|
# All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields, unless it is a CONNECT request (Section 8.3). An HTTP request that omits mandatory pseudo-header fields is malformed (Section 8.1.2.6).
|
101
106
|
pseudo_headers = [
|
102
|
-
[SCHEME,
|
107
|
+
[SCHEME, request.scheme],
|
103
108
|
[METHOD, request.method],
|
104
109
|
[PATH, request.path],
|
105
110
|
]
|
@@ -30,12 +30,12 @@ module Async
|
|
30
30
|
class Server < ::HTTP::Protocol::HTTP2::Server
|
31
31
|
include Connection
|
32
32
|
|
33
|
-
def initialize(stream
|
33
|
+
def initialize(stream)
|
34
34
|
@stream = stream
|
35
35
|
|
36
36
|
framer = ::HTTP::Protocol::HTTP2::Framer.new(stream)
|
37
37
|
|
38
|
-
super(framer
|
38
|
+
super(framer)
|
39
39
|
|
40
40
|
@requests = Async::Queue.new
|
41
41
|
end
|
data/lib/async/http/request.rb
CHANGED
@@ -27,7 +27,8 @@ module Async
|
|
27
27
|
class Request
|
28
28
|
prepend Body::Reader
|
29
29
|
|
30
|
-
def initialize(authority = nil, method = nil, path = nil, version = nil, headers = [], body = nil)
|
30
|
+
def initialize(scheme = nil, authority = nil, method = nil, path = nil, version = nil, headers = [], body = nil)
|
31
|
+
@scheme = scheme
|
31
32
|
@authority = authority
|
32
33
|
@method = method
|
33
34
|
@path = path
|
@@ -36,6 +37,7 @@ module Async
|
|
36
37
|
@body = body
|
37
38
|
end
|
38
39
|
|
40
|
+
attr_accessor :scheme
|
39
41
|
attr_accessor :authority
|
40
42
|
attr_accessor :method
|
41
43
|
attr_accessor :path
|
@@ -54,7 +56,7 @@ module Async
|
|
54
56
|
def self.[](method, path, headers, body)
|
55
57
|
body = Body::Buffered.wrap(body)
|
56
58
|
|
57
|
-
self.new(nil, method, path, nil, headers, body)
|
59
|
+
self.new(nil, nil, method, path, nil, headers, body)
|
58
60
|
end
|
59
61
|
|
60
62
|
def idempotent?
|
@@ -64,6 +66,10 @@ module Async
|
|
64
66
|
def to_s
|
65
67
|
"#{@method} #{@path} #{@version}"
|
66
68
|
end
|
69
|
+
|
70
|
+
def inspect
|
71
|
+
"\#<#{self.class} #{self.to_s} scheme=#{@scheme.inspect} authority=#{@authority.inspect} headers=#{@headers.to_h.inspect} body=#{@body.inspect}>"
|
72
|
+
end
|
67
73
|
end
|
68
74
|
end
|
69
75
|
end
|
data/lib/async/http/server.rb
CHANGED
@@ -31,23 +31,32 @@ module Async
|
|
31
31
|
self.new(block, *args)
|
32
32
|
end
|
33
33
|
|
34
|
-
def initialize(app, endpoint,
|
34
|
+
def initialize(app, endpoint, protocol = endpoint.protocol, scheme = endpoint.scheme)
|
35
35
|
super(app)
|
36
36
|
|
37
37
|
@endpoint = endpoint
|
38
|
-
@
|
38
|
+
@protocol = protocol
|
39
|
+
@scheme = scheme
|
39
40
|
end
|
40
41
|
|
42
|
+
attr :scheme
|
43
|
+
|
41
44
|
def accept(peer, address, task: Task.current)
|
42
45
|
peer.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
43
46
|
|
44
47
|
stream = Async::IO::Stream.new(peer)
|
45
|
-
protocol = @
|
48
|
+
protocol = @protocol.server(stream)
|
46
49
|
|
47
50
|
Async.logger.debug(self) {"Incoming connnection from #{address.inspect} to #{protocol}"}
|
48
51
|
|
49
52
|
protocol.each do |request|
|
53
|
+
# We set the default scheme unless it was otherwise specified.
|
54
|
+
# https://tools.ietf.org/html/rfc7230#section-5.5
|
55
|
+
request.scheme ||= self.scheme
|
56
|
+
|
57
|
+
# This is a slight optimization to avoid having to get the address from the socket.
|
50
58
|
request.remote_address = address
|
59
|
+
|
51
60
|
# Async.logger.debug(self) {"Incoming request from #{address.inspect}: #{request.method} #{request.path}"}
|
52
61
|
|
53
62
|
# If this returns nil, we assume that the connection has been hijacked.
|
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.36.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-10-
|
11
|
+
date: 2018-10-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0.
|
47
|
+
version: 0.8.0
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0.
|
54
|
+
version: 0.8.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: async-rspec
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|