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 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