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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38d3e8fe6b2bedcd8dfc48956ba07e2f70f9be363e419887baaad5c9dab3fab5
4
- data.tar.gz: c924a3f88353066264b12d485a50ef894bd8c00f574a86d9fc7884519e00e366
3
+ metadata.gz: b6e1e7f6525a09868ce9ad825b3b58b7b64f32c5df8ed580c8f77d575cb709f4
4
+ data.tar.gz: dc532ce80d64d36907a8436d22acc6be3cb0d38833b0bd99cdaf1223a1d0248b
5
5
  SHA512:
6
- metadata.gz: 7c930d3851d96957c9fc905c0b00ca0e0bb4e9ffc011f66f7e38763cc6293292e2442e7d67205c8c67934dbab82828b54735a3dce3855af3be9dd221828f5684
7
- data.tar.gz: b6c3e1521e69ff048ba0dd5e8b89951d360eea7881f3df7488b66f48aad1b6e35562b9d91e7f1ecffb64cf130868b639596dcc52b05404d5fbf3e2a0f6e10534
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.7.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['accept-encoding'] = @accept_encoding
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['content-encoding']
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
- return chunk.empty? ? nil : chunk
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
@@ -92,7 +92,7 @@ module Async
92
92
  alias << write
93
93
 
94
94
  def inspect
95
- "\#<#{self.class} #{@count} chunks written#{@finished ? ', finished' : ''}>"
95
+ "\#<#{self.class} #{@count} chunks written#{@finished ? ', finished' : ', waiting'}>"
96
96
  end
97
97
  end
98
98
  end
@@ -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.hostname, retries: 3, **options)
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 = connect(**options)
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.authority ||= @authority
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 connect(connection_limit: nil)
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
 
@@ -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(Request[verb, location.to_str, headers, body])
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
@@ -26,7 +26,7 @@ module Async
26
26
  module HTTP1
27
27
  class Request < Protocol::Request
28
28
  def initialize(protocol)
29
- super(*protocol.read_request)
29
+ super(nil, *protocol.read_request)
30
30
 
31
31
  @protocol = protocol
32
32
  end
@@ -38,7 +38,7 @@ module Async
38
38
  return request
39
39
  rescue
40
40
  # Bad Request
41
- write_response(self.version, 400, {}, nil)
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(self.version, response.status, response.headers, response.body, request.head?)
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(self.version, 500, {}, nil)
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
- def self.client(*args)
30
- Client.new(*args)
28
+ VERSION = "HTTP/1.1"
29
+
30
+ def self.client(stream)
31
+ Client.new(stream, VERSION)
31
32
  end
32
33
 
33
- def self.server(*args)
34
- Server.new(*args)
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, *args)
33
+ def initialize(stream)
34
34
  @stream = stream
35
35
 
36
36
  framer = ::HTTP::Protocol::HTTP2::Framer.new(@stream)
37
37
 
38
- super(framer, *args)
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 == METHOD
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
- # We are ready for processing:
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
- @notification.signal
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, HTTPS],
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, *args)
33
+ def initialize(stream)
34
34
  @stream = stream
35
35
 
36
36
  framer = ::HTTP::Protocol::HTTP2::Framer.new(stream)
37
37
 
38
- super(framer, *args)
38
+ super(framer)
39
39
 
40
40
  @requests = Async::Queue.new
41
41
  end
@@ -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
@@ -31,23 +31,32 @@ module Async
31
31
  self.new(block, *args)
32
32
  end
33
33
 
34
- def initialize(app, endpoint, protocol_class = nil)
34
+ def initialize(app, endpoint, protocol = endpoint.protocol, scheme = endpoint.scheme)
35
35
  super(app)
36
36
 
37
37
  @endpoint = endpoint
38
- @protocol_class = protocol_class || endpoint.protocol
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 = @protocol_class.server(stream)
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.
@@ -82,6 +82,10 @@ module Async
82
82
  @options[:hostname] || @url.hostname
83
83
  end
84
84
 
85
+ def scheme
86
+ @options[:scheme] || @url.scheme
87
+ end
88
+
85
89
  def authority
86
90
  if default_port?
87
91
  hostname
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERSION = "0.35.1"
23
+ VERSION = "0.36.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.35.1
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-25 00:00:00.000000000 Z
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.7.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.7.0
54
+ version: 0.8.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: async-rspec
57
57
  requirement: !ruby/object:Gem::Requirement