async-http 0.35.1 → 0.36.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|