async-http 0.74.0 → 0.76.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: 5a21c7008b5509bd6f8bdffd7dad87a7679bdc9f32584cc71fa2b468518cd57c
4
- data.tar.gz: 2836fc0ef066dfeb60240dce08dd88c07201250b948cfb04617ab52e9d20371c
3
+ metadata.gz: bd45f82b4d28a3e9a72bbdd7eaec1877b69cfce59a3d67b49a09b99b59b42fa4
4
+ data.tar.gz: 05ceb7e93478b63e53bc9a16b829cc2c34efdc22953143192a5a60b310aa04b1
5
5
  SHA512:
6
- metadata.gz: c249984efd9030c199c6b1199dfb4ededdd25a7db33e5156c0ecec2a5d883d703bc976216da13c33426d4814514676f80296687293c53cb4ddc33f44ff4f38a3
7
- data.tar.gz: f707e1908dc79b1e16f2be6546f1781003f4d88e21307fd5172d27198b61a08f87e350a43f784445a761cda89cb674669b552e77ef1e9de4cfaeb4095040ffa9
6
+ metadata.gz: 8b293794ba1fb14494a7187a0c1255674ea771df8cc587c6fa031abca25b8bf803f32ba4889d772d53fb8f4dce0705938f1013ab72c22353b4e09c7cc54f4467
7
+ data.tar.gz: b0c407ee2c817bd8436bff520ae09744f44e35ba79bd992e76a783be52911fdedb0b397ab12c3cd1ffb00b3042bfc5d429f11b5252e16edbc2bab178172d747d
checksums.yaml.gz.sig CHANGED
Binary file
@@ -36,7 +36,7 @@ module Async
36
36
  end
37
37
 
38
38
  def call(stream)
39
- return @block.call(stream)
39
+ @block.call(stream)
40
40
  end
41
41
 
42
42
  attr :input
@@ -17,7 +17,7 @@ module Async
17
17
 
18
18
  head, tail = ::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
19
19
 
20
- @head = ::IO::Stream::Buffered.new(head)
20
+ @head = ::IO::Stream(head)
21
21
  @tail = tail
22
22
 
23
23
  @reader = nil
@@ -52,8 +52,10 @@ module Async
52
52
  end
53
53
 
54
54
  @head.close_write
55
+ rescue => error
56
+ raise
55
57
  ensure
56
- @input.close($!)
58
+ @input.close(error)
57
59
 
58
60
  close_head if @writer&.finished?
59
61
  end
@@ -68,8 +70,10 @@ module Async
68
70
  while chunk = @head.read_partial
69
71
  @output.write(chunk)
70
72
  end
73
+ rescue => error
74
+ raise
71
75
  ensure
72
- @output.close($!)
76
+ @output.close_write(error)
73
77
 
74
78
  close_head if @reader&.finished?
75
79
  end
@@ -3,104 +3,13 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2018-2023, by Samuel Williams.
5
5
 
6
- require 'protocol/http/body/readable'
6
+ require 'protocol/http/body/writable'
7
7
  require 'async/queue'
8
8
 
9
9
  module Async
10
10
  module HTTP
11
11
  module Body
12
- include ::Protocol::HTTP::Body
13
-
14
- # A dynamic body which you can write to and read from.
15
- class Writable < Readable
16
- class Closed < StandardError
17
- end
18
-
19
- # @param [Integer] length The length of the response body if known.
20
- # @param [Async::Queue] queue Specify a different queue implementation, e.g. `Async::LimitedQueue.new(8)` to enable back-pressure streaming.
21
- def initialize(length = nil, queue: Async::Queue.new)
22
- @queue = queue
23
-
24
- @length = length
25
-
26
- @count = 0
27
-
28
- @finished = false
29
-
30
- @closed = false
31
- @error = nil
32
- end
33
-
34
- def length
35
- @length
36
- end
37
-
38
- # Stop generating output; cause the next call to write to fail with the given error.
39
- def close(error = nil)
40
- unless @closed
41
- @queue.enqueue(nil)
42
-
43
- @closed = true
44
- @error = error
45
- end
46
-
47
- super
48
- end
49
-
50
- def closed?
51
- @closed
52
- end
53
-
54
- def ready?
55
- !@queue.empty?
56
- end
57
-
58
- # Has the producer called #finish and has the reader consumed the nil token?
59
- def empty?
60
- @finished
61
- end
62
-
63
- # Read the next available chunk.
64
- def read
65
- return if @finished
66
-
67
- unless chunk = @queue.dequeue
68
- @finished = true
69
- end
70
-
71
- return chunk
72
- end
73
-
74
- # Write a single chunk to the body. Signal completion by calling `#finish`.
75
- def write(chunk)
76
- # If the reader breaks, the writer will break.
77
- # The inverse of this is less obvious (*)
78
- if @closed
79
- raise(@error || Closed)
80
- end
81
-
82
- @count += 1
83
- @queue.enqueue(chunk)
84
- end
85
-
86
- alias << write
87
-
88
- def inspect
89
- "\#<#{self.class} #{@count} chunks written, #{status}>"
90
- end
91
-
92
- private
93
-
94
- def status
95
- if @finished
96
- 'finished'
97
- elsif @closed
98
- 'closing'
99
- else
100
- 'waiting'
101
- end
102
- end
103
- end
12
+ Writable = ::Protocol::HTTP::Body::Writable
104
13
  end
105
14
  end
106
15
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
6
  require_relative 'connection'
7
7
 
@@ -33,22 +33,16 @@ module Async
33
33
 
34
34
  if protocol = request.protocol
35
35
  # This is a very tricky apect of handling HTTP/1 upgrade connections. In theory, this approach is a bit inefficient, because we spin up a task just to handle writing to the underlying stream when we could be writing to the stream directly. But we need to maintain some level of compatibility with HTTP/2. Additionally, we don't know if the upgrade request will be accepted, so starting to write the body at this point needs to be handled with care.
36
- task.async do |subtask|
37
- subtask.annotate("Upgrading request.")
38
-
36
+ task.async(annotation: "Upgrading request...") do
39
37
  # If this fails, this connection will be closed.
40
38
  write_upgrade_body(protocol, body)
41
39
  end
42
40
  elsif request.connect?
43
- task.async do |subtask|
44
- subtask.annotate("Tunnelling body.")
45
-
41
+ task.async(annotation: "Tunnneling request...") do
46
42
  write_tunnel_body(@version, body)
47
43
  end
48
44
  else
49
- task.async do |subtask|
50
- subtask.annotate("Streaming body.")
51
-
45
+ task.async(annotation: "Streaming request...") do
52
46
  # 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.
53
47
  write_body(@version, body, false, trailer)
54
48
  end
@@ -59,33 +59,54 @@ module Async
59
59
  if response
60
60
  trailer = response.headers.trailer!
61
61
 
62
- write_response(@version, response.status, response.headers)
63
-
64
62
  # Some operations in this method are long running, that is, it's expected that `body.call(stream)` could literally run indefinitely. In order to facilitate garbage collection, we want to nullify as many local variables before calling the streaming body. This ensures that the garbage collection can clean up as much state as possible during the long running operation, so we don't retain objects that are no longer needed.
65
-
63
+
66
64
  if body and protocol = response.protocol
65
+ # We force a 101 response if the protocol is upgraded - HTTP/2 CONNECT will return 200 for success, but this won't be understood by HTTP/1 clients:
66
+ write_response(@version, 101, response.headers)
67
+
67
68
  stream = write_upgrade_body(protocol)
68
69
 
69
70
  # At this point, the request body is hijacked, so we don't want to call #finish below.
70
- request = response = nil
71
+ request = nil
72
+ response = nil
73
+
74
+ # We must return here as no further request processing can be done:
75
+ return body.call(stream)
76
+ elsif response.status == 101
77
+ # This code path is to support legacy behavior where the response status is set to 101, but the protocol is not upgraded. This may not be a valid use case, but it is supported for compatibility. We expect the response headers to contain the `upgrade` header.
78
+ write_response(@version, response.status, response.headers)
71
79
 
72
- body.call(stream)
73
- elsif request.connect? and response.success?
74
80
  stream = write_tunnel_body(request.version)
75
81
 
76
82
  # Same as above:
77
- request = response = nil
83
+ request = nil
84
+ response = nil
78
85
 
79
- body.call(stream)
86
+ # We must return here as no further request processing can be done:
87
+ return body&.call(stream)
80
88
  else
81
- head = request.head?
82
- version = request.version
83
-
84
- # Same as above:
85
- request = nil unless request.body
86
- response = nil
89
+ write_response(@version, response.status, response.headers)
87
90
 
88
- write_body(version, body, head, trailer)
91
+ if request.connect? and response.success?
92
+ stream = write_tunnel_body(request.version)
93
+
94
+ # Same as above:
95
+ request = nil
96
+ response = nil
97
+
98
+ # We must return here as no further request processing can be done:
99
+ return body.call(stream)
100
+ else
101
+ head = request.head?
102
+ version = request.version
103
+
104
+ # Same as above:
105
+ request = nil unless request.body
106
+ response = nil
107
+
108
+ write_body(version, body, head, trailer)
109
+ end
89
110
  end
90
111
 
91
112
  # We are done with the body, you shouldn't need to call close on it:
@@ -66,14 +66,14 @@ module Async
66
66
  end
67
67
 
68
68
  def close(error = nil)
69
- super
70
-
71
69
  # Ensure the reader task is stopped.
72
70
  if @reader
73
71
  reader = @reader
74
72
  @reader = nil
75
73
  reader.stop
76
74
  end
75
+
76
+ super
77
77
  end
78
78
 
79
79
  def read_in_background(parent: Task.current)
@@ -101,6 +101,8 @@ module Async
101
101
  ensure
102
102
  # Don't call #close twice.
103
103
  if @reader
104
+ @reader = nil
105
+
104
106
  self.close(error)
105
107
  end
106
108
  end
@@ -3,14 +3,14 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020-2023, by Samuel Williams.
5
5
 
6
- require_relative '../../body/writable'
6
+ require 'protocol/http/body/writable'
7
7
 
8
8
  module Async
9
9
  module HTTP
10
10
  module Protocol
11
11
  module HTTP2
12
12
  # A writable body which requests window updates when data is read from it.
13
- class Input < Body::Writable
13
+ class Input < ::Protocol::HTTP::Body::Writable
14
14
  def initialize(stream, length)
15
15
  super(length)
16
16
 
@@ -50,18 +50,25 @@ module Async
50
50
  end
51
51
  end
52
52
 
53
- # This method should only be called from within the context of the output task.
54
- def close(error = nil)
55
- if @stream
56
- @stream.finish_output(error)
53
+ def close_write(error = nil)
54
+ if stream = @stream
57
55
  @stream = nil
56
+ stream.finish_output(error)
58
57
  end
59
58
  end
60
59
 
60
+ # This method should only be called from within the context of the output task.
61
+ def close(error = nil)
62
+ close_write(error)
63
+ stop(error)
64
+ end
65
+
61
66
  # This method should only be called from within the context of the HTTP/2 stream.
62
67
  def stop(error)
63
- @task&.stop
64
- @task = nil
68
+ if task = @task
69
+ @task = nil
70
+ task.stop(error)
71
+ end
65
72
  end
66
73
 
67
74
  private
@@ -70,10 +77,12 @@ module Async
70
77
  task.annotate("Streaming #{@body} to #{@stream}.")
71
78
 
72
79
  input = @stream.wait_for_input
80
+ stream = ::Protocol::HTTP::Body::Stream.new(input, self)
73
81
 
74
- @body.call(::Protocol::HTTP::Body::Stream.new(input, self))
75
- rescue Async::Stop
76
- # Ignore.
82
+ @body.call(stream)
83
+ rescue => error
84
+ self.close(error)
85
+ raise
77
86
  end
78
87
 
79
88
  # Reads chunks from the given body and writes them to the stream as fast as possible.
@@ -86,11 +95,17 @@ module Async
86
95
  # chunk.clear unless chunk.frozen?
87
96
  # GC.start
88
97
  end
89
-
90
- self.close
98
+ rescue => error
99
+ raise
91
100
  ensure
92
- @body&.close($!)
93
- @body = nil
101
+ # Ensure the body we are reading from is fully closed:
102
+ if body = @body
103
+ @body = nil
104
+ body.close(error)
105
+ end
106
+
107
+ # Ensure the output of this body is closed:
108
+ self.close_write(error)
94
109
  end
95
110
 
96
111
  # Send `maximum_size` bytes of data using the specified `stream`. If the buffer has no more chunks, `END_STREAM` will be sent on the final chunk.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
6
  require_relative '../request'
7
7
  require_relative 'stream'
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
6
  require_relative '../response'
7
7
  require_relative 'stream'
@@ -54,6 +54,11 @@ module Async
54
54
  @response.status = status
55
55
  @headers = ::Protocol::HTTP::Headers.new
56
56
 
57
+ # If the protocol request was successful, ensure the response protocol matches:
58
+ if status == 200 and protocol = @response.request.protocol
59
+ @response.protocol = Array(protocol).first
60
+ end
61
+
57
62
  headers.each do |key, value|
58
63
  # It's guaranteed that this should be the first header:
59
64
  if key == CONTENT_LENGTH
@@ -118,7 +123,7 @@ module Async
118
123
 
119
124
  @exception = error
120
125
 
121
- notify!
126
+ self.notify!
122
127
  end
123
128
  end
124
129
 
@@ -59,7 +59,7 @@ module Async
59
59
 
60
60
  # TODO this might need to be in an ensure block:
61
61
  if @input and frame.end_stream?
62
- @input.close($!)
62
+ @input.close_write
63
63
  @input = nil
64
64
  end
65
65
  rescue ::Protocol::HTTP2::HeaderError => error
@@ -98,7 +98,7 @@ module Async
98
98
  end
99
99
 
100
100
  if frame.end_stream?
101
- @input.close
101
+ @input.close_write
102
102
  @input = nil
103
103
  end
104
104
  end
@@ -149,7 +149,7 @@ module Async
149
149
  super
150
150
 
151
151
  if @input
152
- @input.close(error)
152
+ @input.close_write(error)
153
153
  @input = nil
154
154
  end
155
155
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2023, by Samuel Williams.
4
+ # Copyright, 2017-2024, by Samuel Williams.
5
5
 
6
6
  require 'protocol/http/request'
7
7
  require 'protocol/http/headers'
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module HTTP
8
- VERSION = "0.74.0"
8
+ VERSION = "0.76.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -16,6 +16,10 @@ Please see the [project documentation](https://socketry.github.io/async-http/) f
16
16
 
17
17
  Please see the [project releases](https://socketry.github.io/async-http/releases/index) for all releases.
18
18
 
19
+ ### v0.75.0
20
+
21
+ - Better handling of HTTP/1 \&lt;-\&gt; HTTP/2 proxying, specifically upgrade/CONNECT requests.
22
+
19
23
  ### v0.74.0
20
24
 
21
25
  - [`Async::HTTP::Internet` accepts keyword arguments](https://socketry.github.io/async-http/releases/index#async::http::internet-accepts-keyword-arguments)
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.75.0
4
+
5
+ - Better handling of HTTP/1 \<-\> HTTP/2 proxying, specifically upgrade/CONNECT requests.
6
+
3
7
  ## v0.74.0
4
8
 
5
9
  ### `Async::HTTP::Internet` accepts keyword arguments
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.74.0
4
+ version: 0.76.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -58,7 +58,7 @@ cert_chain:
58
58
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
59
59
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
60
60
  -----END CERTIFICATE-----
61
- date: 2024-08-31 00:00:00.000000000 Z
61
+ date: 2024-09-10 00:00:00.000000000 Z
62
62
  dependencies:
63
63
  - !ruby/object:Gem::Dependency
64
64
  name: async
@@ -122,14 +122,14 @@ dependencies:
122
122
  requirements:
123
123
  - - "~>"
124
124
  - !ruby/object:Gem::Version
125
- version: '0.30'
125
+ version: '0.34'
126
126
  type: :runtime
127
127
  prerelease: false
128
128
  version_requirements: !ruby/object:Gem::Requirement
129
129
  requirements:
130
130
  - - "~>"
131
131
  - !ruby/object:Gem::Version
132
- version: '0.30'
132
+ version: '0.34'
133
133
  - !ruby/object:Gem::Dependency
134
134
  name: protocol-http1
135
135
  requirement: !ruby/object:Gem::Requirement
@@ -182,10 +182,8 @@ files:
182
182
  - bake/async/http/h2spec.rb
183
183
  - lib/async/http.rb
184
184
  - lib/async/http/body.rb
185
- - lib/async/http/body/delayed.rb
186
185
  - lib/async/http/body/hijack.rb
187
186
  - lib/async/http/body/pipe.rb
188
- - lib/async/http/body/slowloris.rb
189
187
  - lib/async/http/body/writable.rb
190
188
  - lib/async/http/client.rb
191
189
  - lib/async/http/endpoint.rb
metadata.gz.sig CHANGED
Binary file
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
5
- # Copyright, 2020, by Bruno Sutic.
6
- # Copyright, 2023, by Thomas Morgan.
7
-
8
- require 'protocol/http/body/wrapper'
9
-
10
- module Async
11
- module HTTP
12
- module Body
13
- class Delayed < ::Protocol::HTTP::Body::Wrapper
14
- def initialize(body, delay = 0.01)
15
- super(body)
16
-
17
- @delay = delay
18
- end
19
-
20
- def ready?
21
- false
22
- end
23
-
24
- def read
25
- Async::Task.current.sleep(@delay)
26
-
27
- return super
28
- end
29
- end
30
- end
31
- end
32
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
5
-
6
- require_relative 'writable'
7
-
8
- require 'async/clock'
9
-
10
- module Async
11
- module HTTP
12
- module Body
13
- # A dynamic body which you can write to and read from.
14
- class Slowloris < Writable
15
- class ThroughputError < StandardError
16
- def initialize(throughput, minimum_throughput, time_since_last_write)
17
- super("Slow write: #{throughput.round(1)}bytes/s less than required #{minimum_throughput.round}bytes/s.")
18
- end
19
- end
20
-
21
- # In order for this implementation to work correctly, you need to use a LimitedQueue.
22
- # @param minimum_throughput [Integer] the minimum bytes per second otherwise this body will be forcefully closed.
23
- def initialize(*arguments, minimum_throughput: 1024, **options)
24
- super(*arguments, **options)
25
-
26
- @minimum_throughput = minimum_throughput
27
-
28
- @last_write_at = nil
29
- @last_chunk_size = nil
30
- end
31
-
32
- attr :minimum_throughput
33
-
34
- # If #read is called regularly to maintain throughput, that is good. If #read is not called, that is a problem. Throughput is dependent on data being available, from #write, so it doesn't seem particularly problimatic to do this check in #write.
35
- def write(chunk)
36
- if @last_chunk_size
37
- time_since_last_write = Async::Clock.now - @last_write_at
38
- throughput = @last_chunk_size / time_since_last_write
39
-
40
- if throughput < @minimum_throughput
41
- error = ThroughputError.new(throughput, @minimum_throughput, time_since_last_write)
42
-
43
- self.close(error)
44
- end
45
- end
46
-
47
- super.tap do
48
- @last_write_at = Async::Clock.now
49
- @last_chunk_size = chunk&.bytesize
50
- end
51
- end
52
- end
53
- end
54
- end
55
- end