protocol-http 0.31.0 → 0.33.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: 924ae96ed9d907e15583c52df242c08481c7740a677e6c3a594f9d76ce2aad6e
4
- data.tar.gz: b7f82710d54ba3e2bb2ffb974a2e46d82c734f53d909ea391986bfde260ea16d
3
+ metadata.gz: 1bf929d8d011f9c79cf7b92bdf8f5eaefcd4faab288b555a7889f33a995daf2e
4
+ data.tar.gz: 4de5349ae1e3cee6fb4857dbac1c78ef7e44249e1adc4526674803d0524af8f1
5
5
  SHA512:
6
- metadata.gz: 0ea73874b0712492e11a18d8b4d3339cf70604cf60c3b2da62d3cbd437a5af083dacee44dc222be71373abd726051ecf8cd6a063914cb96ad42ef54f35446ed5
7
- data.tar.gz: 10f2982481609e45edd91c5b9a71bc7cac26957849151455359eb0ba9e55cff79688564f39b4611ea57de622296b1474e8c2d5190c7bbb1a9ea1a559366f6fa0
6
+ metadata.gz: b2214c233b4543e929f23ef59e8ffaed14f597c9762596ffd8afc8d9a1657fc6c50ace4dc1859a10033e2bba68e36d8f10a8486a056444676939d73352ed374f
7
+ data.tar.gz: 27d9318755a9a2027e21d144a361287adf9cc6f2990bcae4ce93cbd9767c125e6811a1d19c20cf2da5ec67f729132271c9e1744c6ea1c82a88c3fff795e71502
checksums.yaml.gz.sig CHANGED
Binary file
@@ -32,15 +32,6 @@ module Protocol
32
32
  false
33
33
  end
34
34
 
35
- def finish
36
- super.tap do
37
- if @callback
38
- @callback.call
39
- @callback = nil
40
- end
41
- end
42
- end
43
-
44
35
  def close(error = nil)
45
36
  super.tap do
46
37
  if @callback
@@ -62,12 +62,6 @@ module Protocol
62
62
  self.new(body, Zlib::Deflate.new(level, window_size))
63
63
  end
64
64
 
65
- def stream?
66
- # We might want to revisit this design choice.
67
- # We could wrap the streaming body in a Deflate stream, but that would require an extra stream wrapper which we don't have right now. See also `Digestable#stream?`.
68
- false
69
- end
70
-
71
65
  def read
72
66
  return if @stream.finished?
73
67
 
@@ -38,10 +38,6 @@ module Protocol
38
38
  end
39
39
  end
40
40
 
41
- def stream?
42
- false
43
- end
44
-
45
41
  def read
46
42
  if chunk = super
47
43
  @digest.update(chunk)
@@ -56,10 +56,6 @@ module Protocol
56
56
  @remaining = @length
57
57
  end
58
58
 
59
- def stream?
60
- false
61
- end
62
-
63
59
  def read
64
60
  if @remaining > 0
65
61
  amount = [@remaining, @block_size].min
@@ -72,6 +68,14 @@ module Protocol
72
68
  end
73
69
  end
74
70
 
71
+ def stream?
72
+ true
73
+ end
74
+
75
+ def call(stream)
76
+ IO.copy_stream(@file, stream, @remaining)
77
+ end
78
+
75
79
  def join
76
80
  return "" if @remaining == 0
77
81
 
@@ -15,10 +15,6 @@ module Protocol
15
15
  self.new(body, Zlib::Inflate.new(encoding))
16
16
  end
17
17
 
18
- def stream?
19
- false
20
- end
21
-
22
18
  def read
23
19
  return if @stream.finished?
24
20
 
@@ -7,9 +7,15 @@
7
7
  module Protocol
8
8
  module HTTP
9
9
  module Body
10
- # An interface for reading data from a body.
10
+ # Represents a readable input streams.
11
11
  #
12
12
  # Typically, you'd override `#read` to return chunks of data.
13
+ #
14
+ # I n general, you read chunks of data from a body until it is empty and returns `nil`. Upon reading `nil`, the body is considered consumed and should not be read from again.
15
+ #
16
+ # Reading can also fail, for example if the body represents a streaming upload, and the connection is lost. In this case, the body will raise some kind of error.
17
+ #
18
+ # If you don't want to read from a stream, and instead want to close it immediately, you can call `close` on the body. If the body is already completely consumed, `close` will do nothing, but if there is still data to be read, it will cause the underlying stream to be reset (and possibly closed).
13
19
  class Readable
14
20
  # Close the stream immediately.
15
21
  def close(error = nil)
@@ -29,65 +35,46 @@ module Protocol
29
35
  false
30
36
  end
31
37
 
38
+ # Whether the stream can be rewound using {rewind}.
32
39
  def rewindable?
33
40
  false
34
41
  end
35
42
 
43
+ # Rewind the stream to the beginning.
44
+ # @returns [Boolean] Whether the stream was successfully rewound.
36
45
  def rewind
37
46
  false
38
47
  end
39
48
 
49
+ # The total length of the body, if known.
50
+ # @returns [Integer | Nil] The total length of the body, or `nil` if the length is unknown.
40
51
  def length
41
52
  nil
42
53
  end
43
54
 
44
55
  # Read the next available chunk.
45
56
  # @returns [String | Nil] The chunk of data, or `nil` if the stream has finished.
57
+ # @raises [StandardError] If an error occurs while reading.
46
58
  def read
47
59
  nil
48
60
  end
49
61
 
50
- # Should the internal mechanism prefer to use {call}?
51
- # @returns [Boolean]
52
- def stream?
53
- false
54
- end
55
-
56
- # Write the body to the given stream.
57
- def call(stream)
58
- while chunk = self.read
59
- stream.write(chunk)
60
-
61
- # Flush the stream unless we are immediately expecting more data:
62
- unless self.ready?
63
- stream.flush
64
- end
65
- end
66
- ensure
67
- stream.close
68
- end
69
-
70
- # Read all remaining chunks into a buffered body and close the underlying input.
71
- # @returns [Buffered] The buffered body.
72
- def finish
73
- # Internally, this invokes `self.each` which then invokes `self.close`.
74
- Buffered.read(self)
75
- end
76
-
77
62
  # Enumerate all chunks until finished, then invoke `#close`.
78
63
  #
64
+ # Closes the stream when finished or if an error occurs.
65
+ #
79
66
  # @yields {|chunk| ...} The block to call with each chunk of data.
80
67
  # @parameter chunk [String | Nil] The chunk of data, or `nil` if the stream has finished.
81
68
  def each
82
- return to_enum(:each) unless block_given?
69
+ return to_enum unless block_given?
83
70
 
84
- begin
85
- while chunk = self.read
86
- yield chunk
87
- end
88
- ensure
89
- self.close($!)
71
+ while chunk = self.read
72
+ yield chunk
90
73
  end
74
+ rescue => error
75
+ raise
76
+ ensure
77
+ self.close(error)
91
78
  end
92
79
 
93
80
  # Read all remaining chunks into a single binary string using `#each`.
@@ -107,6 +94,35 @@ module Protocol
107
94
  end
108
95
  end
109
96
 
97
+ def stream?
98
+ false
99
+ end
100
+
101
+ # Write the body to the given stream.
102
+ #
103
+ # In some cases, the stream may also be readable, such as when hijacking an HTTP/1 connection. In that case, it may be acceptable to read and write to the stream directly.
104
+ #
105
+ # If the stream is not ready, it will be flushed after each chunk. Closes the stream when finished or if an error occurs.
106
+ #
107
+ def call(stream)
108
+ self.each do |chunk|
109
+ stream.write(chunk)
110
+
111
+ # Flush the stream unless we are immediately expecting more data:
112
+ unless self.ready?
113
+ stream.flush
114
+ end
115
+ end
116
+ end
117
+
118
+ # Read all remaining chunks into a buffered body and close the underlying input.
119
+ #
120
+ # @returns [Buffered] The buffered body.
121
+ def finish
122
+ # Internally, this invokes `self.each` which then invokes `self.close`.
123
+ Buffered.read(self)
124
+ end
125
+
110
126
  def as_json(...)
111
127
  {
112
128
  class: self.class.name,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
6
  require_relative 'wrapper'
7
7
  require_relative 'buffered'
@@ -43,10 +43,6 @@ module Protocol
43
43
  Buffered.new(@chunks)
44
44
  end
45
45
 
46
- def stream?
47
- false
48
- end
49
-
50
46
  def read
51
47
  if @index < @chunks.size
52
48
  chunk = @chunks[@index]
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022, by Samuel Williams.
5
+
6
+ require_relative 'readable'
7
+ require_relative 'stream'
8
+
9
+ module Protocol
10
+ module HTTP
11
+ module Body
12
+ # A body that invokes a block that can read and write to a stream.
13
+ #
14
+ # In some cases, it's advantageous to directly read and write to the underlying stream if possible. For example, HTTP/1 upgrade requests, WebSockets, and similar. To handle that case, response bodies can implement `stream?` and return `true`. When `stream?` returns true, the body **should** be consumed by calling `call(stream)`. Server implementations may choose to always invoke `call(stream)` if it's efficient to do so. Bodies that don't support it will fall back to using `#each`.
15
+ #
16
+ # When invoking `call(stream)`, the stream can be read from and written to, and closed. However, the stream is only guaranteed to be open for the duration of the `call(stream)` call. Once the method returns, the stream **should** be closed by the server.
17
+ class Streamable < Readable
18
+ class Closed < StandardError
19
+ end
20
+
21
+ def initialize(block, input = nil)
22
+ @block = block
23
+ @input = input
24
+ @output = nil
25
+ end
26
+
27
+ # Closing a stream indicates we are no longer interested in reading from it.
28
+ def close(error = nil)
29
+ if @input
30
+ @input.close
31
+ @input = nil
32
+ end
33
+
34
+ if @output
35
+ @output.close(error)
36
+ end
37
+ end
38
+
39
+ attr :block
40
+
41
+ class Output
42
+ def initialize(input, block)
43
+ stream = Stream.new(input, self)
44
+
45
+ @from = nil
46
+
47
+ @fiber = Fiber.new do |from|
48
+ @from = from
49
+ block.call(stream)
50
+ rescue Closed
51
+ # Ignore.
52
+ ensure
53
+ @fiber = nil
54
+
55
+ # No more chunks will be generated:
56
+ if from = @from
57
+ @from = nil
58
+ from.transfer(nil)
59
+ end
60
+ end
61
+ end
62
+
63
+ # Can be invoked by the block to write to the stream.
64
+ def write(chunk)
65
+ if from = @from
66
+ @from = nil
67
+ @from = from.transfer(chunk)
68
+ else
69
+ raise RuntimeError, "Stream is not being read!"
70
+ end
71
+ end
72
+
73
+ # Can be invoked by the block to close the stream.
74
+ def close(error = nil)
75
+ if from = @from
76
+ @from = nil
77
+ from.transfer(nil)
78
+ elsif @fiber
79
+ @fiber.raise(error || Closed)
80
+ end
81
+ end
82
+
83
+ def read
84
+ raise RuntimeError, "Stream is already being read!" if @from
85
+
86
+ @fiber&.transfer(Fiber.current)
87
+ end
88
+ end
89
+
90
+ # Invokes the block in a fiber which yields chunks when they are available.
91
+ def read
92
+ @output ||= Output.new(@input, @block)
93
+
94
+ return @output.read
95
+ end
96
+
97
+ def stream?
98
+ true
99
+ end
100
+
101
+ def call(stream)
102
+ raise "Streaming body has already been read!" if @output
103
+
104
+ @block.call(stream)
105
+ rescue => error
106
+ raise
107
+ ensure
108
+ self.close(error)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -27,15 +27,11 @@ module Protocol
27
27
  # The wrapped body.
28
28
  attr :body
29
29
 
30
- # Buffer any remaining body.
31
- def finish
32
- @body.finish
33
- end
34
-
35
30
  def close(error = nil)
36
31
  @body.close(error)
37
32
 
38
- super
33
+ # It's a no-op:
34
+ # super
39
35
  end
40
36
 
41
37
  def empty?
@@ -77,14 +73,6 @@ module Protocol
77
73
  def inspect
78
74
  @body.inspect
79
75
  end
80
-
81
- def stream?
82
- @body.stream?
83
- end
84
-
85
- def call(stream)
86
- @body.call(stream)
87
- end
88
76
  end
89
77
  end
90
78
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2018-2023, by Samuel Williams.
5
+
6
+ require_relative 'readable'
7
+
8
+ module Protocol
9
+ module HTTP
10
+ module Body
11
+ # A dynamic body which you can write to and read from.
12
+ class Writable < Readable
13
+ class Closed < StandardError
14
+ end
15
+
16
+ # @param [Integer] length The length of the response body if known.
17
+ # @param [Async::Queue] queue Specify a different queue implementation, e.g. `Async::LimitedQueue.new(8)` to enable back-pressure streaming.
18
+ def initialize(length = nil, queue: Thread::Queue.new)
19
+ @queue = queue
20
+
21
+ @length = length
22
+
23
+ @count = 0
24
+
25
+ @finished = false
26
+
27
+ @closed = false
28
+ @error = nil
29
+ end
30
+
31
+ def length
32
+ @length
33
+ end
34
+
35
+ # Stop generating output; cause the next call to write to fail with the given error. Does not prevent existing chunks from being read. In other words, this indicates both that no more data will be or should be written to the body.
36
+ def close(error = nil)
37
+ unless @closed
38
+ @queue.close
39
+
40
+ @closed = true
41
+ @error = error
42
+ end
43
+
44
+ super
45
+ end
46
+
47
+ def closed?
48
+ @closed
49
+ end
50
+
51
+ def ready?
52
+ !@queue.empty? || @queue.closed?
53
+ end
54
+
55
+ # Has the producer called #finish and has the reader consumed the nil token?
56
+ def empty?
57
+ @queue.empty? && @queue.closed?
58
+ end
59
+
60
+ # Read the next available chunk.
61
+ def read
62
+ @queue.pop
63
+ end
64
+
65
+ # Write a single chunk to the body. Signal completion by calling `#finish`.
66
+ def write(chunk)
67
+ # If the reader breaks, the writer will break.
68
+ # The inverse of this is less obvious (*)
69
+ if @closed
70
+ raise(@error || Closed)
71
+ end
72
+
73
+ @count += 1
74
+ @queue.push(chunk)
75
+ end
76
+
77
+ alias << write
78
+
79
+ def inspect
80
+ "\#<#{self.class} #{@count} chunks written, #{status}>"
81
+ end
82
+
83
+ private
84
+
85
+ def status
86
+ if @queue.empty?
87
+ if @queue.closed?
88
+ 'closed'
89
+ else
90
+ 'waiting'
91
+ end
92
+ else
93
+ if @queue.closed?
94
+ 'closing'
95
+ else
96
+ 'ready'
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP
8
- VERSION = "0.31.0"
8
+ VERSION = "0.33.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -22,6 +22,11 @@ Please see the [project documentation](https://socketry.github.io/protocol-http/
22
22
 
23
23
  Please see the [project releases](https://socketry.github.io/protocol-http/releases/index) for all releases.
24
24
 
25
+ ### v0.33.0
26
+
27
+ - Clarify behaviour of streaming bodies and copy `Protocol::Rack::Body::Streaming` to `Protocol::HTTP::Body::Streamable`.
28
+ - Copy `Async::HTTP::Body::Writable` to `Protocol::HTTP::Body::Writable`.
29
+
25
30
  ### v0.31.0
26
31
 
27
32
  - Ensure chunks are flushed if required, when streaming.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## v0.33.0
4
+
5
+ - Clarify behaviour of streaming bodies and copy `Protocol::Rack::Body::Streaming` to `Protocol::HTTP::Body::Streamable`.
6
+ - Copy `Async::HTTP::Body::Writable` to `Protocol::HTTP::Body::Writable`.
7
+
3
8
  ## v0.31.0
4
9
 
5
10
  - Ensure chunks are flushed if required, when streaming.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.0
4
+ version: 0.33.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -47,7 +47,7 @@ cert_chain:
47
47
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
48
48
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
49
49
  -----END CERTIFICATE-----
50
- date: 2024-09-03 00:00:00.000000000 Z
50
+ date: 2024-09-05 00:00:00.000000000 Z
51
51
  dependencies: []
52
52
  description:
53
53
  email:
@@ -68,7 +68,9 @@ files:
68
68
  - lib/protocol/http/body/reader.rb
69
69
  - lib/protocol/http/body/rewindable.rb
70
70
  - lib/protocol/http/body/stream.rb
71
+ - lib/protocol/http/body/streamable.rb
71
72
  - lib/protocol/http/body/wrapper.rb
73
+ - lib/protocol/http/body/writable.rb
72
74
  - lib/protocol/http/content_encoding.rb
73
75
  - lib/protocol/http/cookie.rb
74
76
  - lib/protocol/http/error.rb
metadata.gz.sig CHANGED
Binary file