tipi 0.40 → 0.45

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +3 -1
  4. data/.gitignore +5 -1
  5. data/CHANGELOG.md +35 -0
  6. data/Gemfile +7 -1
  7. data/Gemfile.lock +55 -29
  8. data/README.md +184 -8
  9. data/Rakefile +1 -3
  10. data/benchmarks/bm_http1_parser.rb +85 -0
  11. data/bin/benchmark +37 -0
  12. data/bin/h1pd +6 -0
  13. data/bin/tipi +3 -21
  14. data/bm.png +0 -0
  15. data/df/agent.rb +1 -1
  16. data/df/sample_agent.rb +2 -2
  17. data/df/server.rb +16 -102
  18. data/df/server_utils.rb +175 -0
  19. data/examples/full_service.rb +13 -0
  20. data/examples/hello.rb +5 -0
  21. data/examples/http1_parser.rb +55 -0
  22. data/examples/http_server.js +1 -1
  23. data/examples/http_server.rb +15 -3
  24. data/examples/http_server_graceful.rb +1 -1
  25. data/examples/http_server_static.rb +6 -18
  26. data/examples/https_server.rb +41 -15
  27. data/examples/rack_server_forked.rb +26 -0
  28. data/examples/rack_server_https_forked.rb +1 -1
  29. data/examples/servername_cb.rb +37 -0
  30. data/examples/websocket_demo.rb +1 -1
  31. data/lib/tipi/acme.rb +315 -0
  32. data/lib/tipi/cli.rb +93 -0
  33. data/lib/tipi/config_dsl.rb +13 -13
  34. data/lib/tipi/configuration.rb +2 -2
  35. data/{e → lib/tipi/controller/bare_polyphony.rb} +0 -0
  36. data/lib/tipi/controller/bare_stock.rb +10 -0
  37. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  38. data/lib/tipi/controller/web_polyphony.rb +351 -0
  39. data/lib/tipi/controller/web_stock.rb +631 -0
  40. data/lib/tipi/controller.rb +12 -0
  41. data/lib/tipi/digital_fabric/agent.rb +10 -8
  42. data/lib/tipi/digital_fabric/agent_proxy.rb +26 -12
  43. data/lib/tipi/digital_fabric/executive.rb +7 -3
  44. data/lib/tipi/digital_fabric/protocol.rb +19 -4
  45. data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
  46. data/lib/tipi/digital_fabric/service.rb +84 -56
  47. data/lib/tipi/handler.rb +2 -2
  48. data/lib/tipi/http1_adapter.rb +86 -125
  49. data/lib/tipi/http2_adapter.rb +29 -16
  50. data/lib/tipi/http2_stream.rb +52 -56
  51. data/lib/tipi/rack_adapter.rb +2 -53
  52. data/lib/tipi/response_extensions.rb +2 -2
  53. data/lib/tipi/supervisor.rb +75 -0
  54. data/lib/tipi/version.rb +1 -1
  55. data/lib/tipi/websocket.rb +3 -3
  56. data/lib/tipi.rb +8 -5
  57. data/test/coverage.rb +2 -2
  58. data/test/helper.rb +60 -12
  59. data/test/test_http_server.rb +14 -41
  60. data/test/test_request.rb +2 -29
  61. data/tipi.gemspec +12 -8
  62. metadata +88 -28
  63. data/examples/automatic_certificate.rb +0 -193
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'http/parser'
4
- require_relative './http2_adapter'
3
+ require 'h1p'
5
4
  require 'qeweney/request'
6
5
 
6
+ require_relative './http2_adapter'
7
+
7
8
  module Tipi
8
9
  # HTTP1 protocol implementation
9
10
  class HTTP1Adapter
@@ -14,127 +15,83 @@ module Tipi
14
15
  @conn = conn
15
16
  @opts = opts
16
17
  @first = true
17
- @parser = ::HTTP::Parser.new(self)
18
+ @parser = H1P::Parser.new(@conn)
18
19
  end
19
-
20
+
20
21
  def each(&block)
21
- @conn.recv_loop do |data|
22
- return if handle_incoming_data(data, &block)
22
+ while true
23
+ headers = @parser.parse_headers
24
+ break unless headers
25
+
26
+ # handle_request returns true if connection is not persistent or was
27
+ # upgraded
28
+ break if handle_request(headers, &block)
23
29
  end
30
+ rescue H1P::Error, ArgumentError
31
+ # an ArgumentError might be raised in the parser if an invalid input
32
+ # string is given as the HTTP method (String#upcase will raise on invalid HTTP string)
33
+ #
34
+ # ignore
24
35
  rescue SystemCallError, IOError
25
36
  # ignore
26
37
  ensure
27
38
  finalize_client_loop
28
39
  end
29
-
30
- # return [Boolean] true if client loop should stop
31
- def handle_incoming_data(data, &block)
32
- rx = data.bytesize
33
- @parser << data
34
- while (request = @requests_head)
35
- request.headers[':rx'] = rx
36
- if @first
37
- request.headers[':first'] = true
38
- @first = nil
39
- end
40
- return true if upgrade_connection(request.headers, &block)
41
-
42
- @requests_head = request.__next__
43
- block.call(request)
44
- return true unless request.keep_alive?
40
+
41
+ def handle_request(headers, &block)
42
+ scheme = (proto = headers['x-forwarded-proto']) ?
43
+ proto.downcase : scheme_from_connection
44
+ headers[':scheme'] = scheme
45
+ @protocol = headers[':protocol']
46
+ if @first
47
+ headers[':first'] = true
48
+ @first = nil
45
49
  end
46
- nil
50
+
51
+ return true if upgrade_connection(headers, &block)
52
+
53
+ request = Qeweney::Request.new(headers, self)
54
+ if !@parser.complete?
55
+ request.buffer_body_chunk(@parser.read_body_chunk(true))
56
+ end
57
+ block.call(request)
58
+ return !persistent_connection?(headers)
47
59
  end
48
-
60
+
61
+ def persistent_connection?(headers)
62
+ if headers[':protocol'] == 'http/1.1'
63
+ return headers['connection'] != 'close'
64
+ else
65
+ connection = headers['connection']
66
+ return connection && connection != 'close'
67
+ end
68
+ end
69
+
49
70
  def finalize_client_loop
50
- # release references to various objects
51
- @requests_head = @requests_tail = nil
52
71
  @parser = nil
53
72
  @splicing_pipe = nil
54
73
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
55
74
  @conn.close
56
75
  end
57
-
76
+
58
77
  # Reads a body chunk for the current request. Transfers control to the parse
59
78
  # loop, and resumes once the parse_loop has fired the on_body callback
60
- def get_body_chunk(request)
61
- @waiting_for_body_chunk = true
62
- @next_chunk = nil
63
- while !@requests_tail.complete? && (data = @conn.readpartial(8192))
64
- request.rx_incr(data.bytesize)
65
- @parser << data
66
- return @next_chunk if @next_chunk
67
-
68
- snooze
69
- end
70
- nil
71
- ensure
72
- @waiting_for_body_chunk = nil
73
- end
74
-
75
- # Waits for the current request to complete. Transfers control to the parse
76
- # loop, and resumes once the parse_loop has fired the on_message_complete
77
- # callback
78
- def consume_request(request)
79
- request = @requests_head
80
- @conn.recv_loop do |data|
81
- request.rx_incr(data.bytesize)
82
- @parser << data
83
- return if request.complete?
84
- end
85
- end
86
-
87
- def protocol
88
- version = @parser.http_version
89
- "HTTP #{version.join('.')}"
90
- end
91
-
92
- def on_headers_complete(headers)
93
- headers = normalize_headers(headers)
94
- headers[':path'] = @parser.request_url
95
- headers[':method'] = @parser.http_method.downcase
96
- scheme = (proto = headers['x-forwarded-proto']) ?
97
- proto.downcase : scheme_from_connection
98
- headers[':scheme'] = scheme
99
- queue_request(Qeweney::Request.new(headers, self))
79
+ def get_body_chunk(request, buffered_only = false)
80
+ @parser.read_body_chunk(buffered_only)
100
81
  end
101
82
 
102
- def normalize_headers(headers)
103
- headers.each_with_object({}) do |(k, v), h|
104
- k = k.downcase
105
- hk = h[k]
106
- if hk
107
- hk = h[k] = [hk] unless hk.is_a?(Array)
108
- v.is_a?(Array) ? hk.concat(v) : hk << v
109
- else
110
- h[k] = v
111
- end
112
- end
113
- end
114
-
115
- def queue_request(request)
116
- if @requests_head
117
- @requests_tail.__next__ = request
118
- @requests_tail = request
119
- else
120
- @requests_head = @requests_tail = request
121
- end
83
+ def get_body(request)
84
+ @parser.read_body
122
85
  end
123
-
124
- def on_body(chunk)
125
- if @waiting_for_body_chunk
126
- @next_chunk = chunk
127
- @waiting_for_body_chunk = nil
128
- else
129
- @requests_tail.buffer_body_chunk(chunk)
130
- end
86
+
87
+ def complete?(request)
88
+ @parser.complete?
131
89
  end
132
-
133
- def on_message_complete
134
- @waiting_for_body_chunk = nil
135
- @requests_tail.complete!(@parser.keep_alive?)
90
+
91
+ def protocol
92
+ @protocol
136
93
  end
137
-
94
+
138
95
  # Upgrades the connection to a different protocol, if the 'Upgrade' header is
139
96
  # given. By default the only supported upgrade protocol is HTTP2. Additional
140
97
  # protocols, notably WebSocket, can be specified by passing a hash to the
@@ -160,27 +117,28 @@ module Tipi
160
117
  def upgrade_connection(headers, &block)
161
118
  upgrade_protocol = headers['upgrade']
162
119
  return nil unless upgrade_protocol
163
-
120
+
164
121
  upgrade_protocol = upgrade_protocol.downcase.to_sym
165
122
  upgrade_handler = @opts[:upgrade] && @opts[:upgrade][upgrade_protocol]
166
123
  return upgrade_with_handler(upgrade_handler, headers) if upgrade_handler
167
124
  return upgrade_to_http2(headers, &block) if upgrade_protocol == :h2c
168
-
125
+
169
126
  nil
170
127
  end
171
-
128
+
172
129
  def upgrade_with_handler(handler, headers)
173
- @parser = @requests_head = @requests_tail = nil
130
+ @parser = nil
174
131
  handler.(self, headers)
175
132
  true
176
133
  end
177
-
134
+
178
135
  def upgrade_to_http2(headers, &block)
179
- @parser = @requests_head = @requests_tail = nil
180
- HTTP2Adapter.upgrade_each(@conn, @opts, http2_upgraded_headers(headers), &block)
136
+ headers = http2_upgraded_headers(headers)
137
+ body = @parser.read_body
138
+ HTTP2Adapter.upgrade_each(@conn, @opts, headers, body, &block)
181
139
  true
182
140
  end
183
-
141
+
184
142
  # Returns headers for HTTP2 upgrade
185
143
  # @param headers [Hash] request headers
186
144
  # @return [Hash] headers for HTTP2 upgrade
@@ -198,10 +156,10 @@ module Tipi
198
156
  def scheme_from_connection
199
157
  @conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
200
158
  end
201
-
159
+
202
160
  # response API
203
161
 
204
- CRLF = "\r\n"
162
+ CRLF = "\r\n"
205
163
  CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
206
164
 
207
165
  # Sends response including headers and body. Waits for the request to complete
@@ -210,7 +168,6 @@ module Tipi
210
168
  # @param body [String] response body
211
169
  # @param headers
212
170
  def respond(request, body, headers)
213
- consume_request(request) if @parsing
214
171
  formatted_headers = format_headers(headers, body, false)
215
172
  request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
216
173
  if body
@@ -220,21 +177,21 @@ module Tipi
220
177
  end
221
178
  end
222
179
 
223
- def respond_from_io(request, io, headers, chunk_size = 2**14)
224
- consume_request(request) if @parsing
180
+ CHUNK_LENGTH_PROC = ->(len) { "#{len.to_s(16)}\r\n" }
225
181
 
182
+ def respond_from_io(request, io, headers, chunk_size = 2**14)
226
183
  formatted_headers = format_headers(headers, true, true)
227
184
  request.tx_incr(formatted_headers.bytesize)
228
-
185
+
229
186
  # assume chunked encoding
230
187
  Thread.current.backend.splice_chunks(
231
188
  io,
232
189
  @conn,
233
190
  formatted_headers,
234
191
  "0\r\n\r\n",
235
- ->(len) { "#{len.to_s(16)}\r\n" },
192
+ CHUNK_LENGTH_PROC,
236
193
  "\r\n",
237
- 16384
194
+ chunk_size
238
195
  )
239
196
  end
240
197
 
@@ -246,11 +203,15 @@ module Tipi
246
203
  # @param chunked [boolean] whether to use chunked transfer encoding
247
204
  # @return [void]
248
205
  def send_headers(request, headers, empty_response: false, chunked: true)
249
- formatted_headers = format_headers(headers, !empty_response, @parser.http_minor == 1 && chunked)
206
+ formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
250
207
  request.tx_incr(formatted_headers.bytesize)
251
208
  @conn.write(formatted_headers)
252
209
  end
253
-
210
+
211
+ def http1_1?(request)
212
+ request.headers[':protocol'] == 'http/1.1'
213
+ end
214
+
254
215
  # Sends a response body chunk. If no headers were sent, default headers are
255
216
  # sent using #send_headers. if the done option is true(thy), an empty chunk
256
217
  # will be sent to signal response completion to the client.
@@ -267,7 +228,7 @@ module Tipi
267
228
  request.tx_incr(data.bytesize)
268
229
  @conn.write(data)
269
230
  end
270
-
231
+
271
232
  def send_chunk_from_io(request, io, r, w, chunk_size)
272
233
  len = w.splice(io, chunk_size)
273
234
  if len > 0
@@ -289,12 +250,12 @@ module Tipi
289
250
  request.tx_incr(5)
290
251
  @conn << "0\r\n\r\n"
291
252
  end
292
-
253
+
293
254
  def close
294
255
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
295
256
  @conn.close
296
257
  end
297
-
258
+
298
259
  private
299
260
 
300
261
  INTERNAL_HEADER_REGEXP = /^:/.freeze
@@ -311,13 +272,13 @@ module Tipi
311
272
  lines = format_status_line(body, status, chunked)
312
273
  headers.each do |k, v|
313
274
  next if k =~ INTERNAL_HEADER_REGEXP
314
-
275
+
315
276
  collect_header_lines(lines, k, v)
316
277
  end
317
278
  lines << CRLF
318
279
  lines
319
280
  end
320
-
281
+
321
282
  def format_status_line(body, status, chunked)
322
283
  if !body
323
284
  empty_status_line(status)
@@ -325,7 +286,7 @@ module Tipi
325
286
  with_body_status_line(status, body, chunked)
326
287
  end
327
288
  end
328
-
289
+
329
290
  def empty_status_line(status)
330
291
  if status == 204
331
292
  +"HTTP/1.1 #{status}\r\n"
@@ -333,7 +294,7 @@ module Tipi
333
294
  +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
334
295
  end
335
296
  end
336
-
297
+
337
298
  def with_body_status_line(status, body, chunked)
338
299
  if chunked
339
300
  +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
@@ -3,18 +3,30 @@
3
3
  require 'http/2'
4
4
  require_relative './http2_stream'
5
5
 
6
+ # patch to fix bug in HTTP2::Stream
7
+ class HTTP2::Stream
8
+ def end_stream?(frame)
9
+ case frame[:type]
10
+ when :data, :headers, :continuation
11
+ frame[:flags]&.include?(:end_stream)
12
+ else false
13
+ end
14
+ end
15
+ end
16
+
6
17
  module Tipi
7
18
  # HTTP2 server adapter
8
19
  class HTTP2Adapter
9
- def self.upgrade_each(socket, opts, headers, &block)
10
- adapter = new(socket, opts, headers)
20
+ def self.upgrade_each(socket, opts, headers, body, &block)
21
+ adapter = new(socket, opts, headers, body)
11
22
  adapter.each(&block)
12
23
  end
13
-
14
- def initialize(conn, opts, upgrade_headers = nil)
24
+
25
+ def initialize(conn, opts, upgrade_headers = nil, upgrade_body = nil)
15
26
  @conn = conn
16
27
  @opts = opts
17
28
  @upgrade_headers = upgrade_headers
29
+ @upgrade_body = upgrade_body
18
30
  @first = true
19
31
  @rx = (upgrade_headers && upgrade_headers[':rx']) || 0
20
32
  @tx = (upgrade_headers && upgrade_headers[':tx']) || 0
@@ -24,33 +36,34 @@ module Tipi
24
36
  @interface.on(:frame, &method(:send_frame))
25
37
  @streams = {}
26
38
  end
27
-
39
+
28
40
  def send_frame(data)
29
41
  if @transfer_count_request
30
42
  @transfer_count_request.tx_incr(data.bytesize)
31
43
  end
32
44
  @conn << data
45
+ rescue Polyphony::BaseException
46
+ raise
33
47
  rescue Exception => e
34
48
  @connection_fiber.transfer e
35
49
  end
36
-
50
+
37
51
  UPGRADE_MESSAGE = <<~HTTP.gsub("\n", "\r\n")
38
52
  HTTP/1.1 101 Switching Protocols
39
53
  Connection: Upgrade
40
54
  Upgrade: h2c
41
-
55
+
42
56
  HTTP
43
-
57
+
44
58
  def upgrade
45
59
  @conn << UPGRADE_MESSAGE
46
60
  @tx += UPGRADE_MESSAGE.bytesize
47
61
  settings = @upgrade_headers['http2-settings']
48
- Fiber.current.schedule(nil)
49
- @interface.upgrade(settings, @upgrade_headers, '')
62
+ @interface.upgrade(settings, @upgrade_headers, @upgrade_body || '')
50
63
  ensure
51
64
  @upgrade_headers = nil
52
65
  end
53
-
66
+
54
67
  # Iterates over incoming requests
55
68
  def each(&block)
56
69
  @interface.on(:stream) { |stream| start_stream(stream, &block) }
@@ -60,7 +73,7 @@ module Tipi
60
73
  @rx += data.bytesize
61
74
  @interface << data
62
75
  end
63
- rescue SystemCallError, IOError
76
+ rescue SystemCallError, IOError, HTTP2::Error::Error
64
77
  # ignore
65
78
  ensure
66
79
  finalize_client_loop
@@ -71,26 +84,26 @@ module Tipi
71
84
  @rx = 0
72
85
  count
73
86
  end
74
-
87
+
75
88
  def get_tx_count
76
89
  count = @tx
77
90
  @tx = 0
78
91
  count
79
92
  end
80
-
93
+
81
94
  def start_stream(stream, &block)
82
95
  stream = HTTP2StreamHandler.new(self, stream, @conn, @first, &block)
83
96
  @first = nil if @first
84
97
  @streams[stream] = true
85
98
  end
86
-
99
+
87
100
  def finalize_client_loop
88
101
  @interface = nil
89
102
  @streams.each_key(&:stop)
90
103
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
91
104
  @conn.close
92
105
  end
93
-
106
+
94
107
  def close
95
108
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
96
109
  @conn.close
@@ -8,15 +8,14 @@ module Tipi
8
8
  class HTTP2StreamHandler
9
9
  attr_accessor :__next__
10
10
  attr_reader :conn
11
-
11
+
12
12
  def initialize(adapter, stream, conn, first, &block)
13
13
  @adapter = adapter
14
14
  @stream = stream
15
15
  @conn = conn
16
16
  @first = first
17
17
  @connection_fiber = Fiber.current
18
- @stream_fiber = spin { |req| handle_request(req, &block) }
19
- Thread.current.fiber_unschedule(@stream_fiber)
18
+ @stream_fiber = spin { run(&block) }
20
19
 
21
20
  # Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).
22
21
  # The request handler is run on a separate fiber for each stream, allowing
@@ -33,20 +32,20 @@ module Tipi
33
32
  stream.on(:data, &method(:on_data))
34
33
  stream.on(:half_close, &method(:on_half_close))
35
34
  end
36
-
37
- def handle_request(request, &block)
35
+
36
+ def run(&block)
37
+ request = receive
38
38
  error = nil
39
39
  block.(request)
40
40
  @connection_fiber.schedule
41
- rescue Polyphony::MoveOn
42
- # ignore
41
+ rescue Polyphony::BaseException
42
+ raise
43
43
  rescue Exception => e
44
44
  error = e
45
45
  ensure
46
- @done = true
47
46
  @connection_fiber.schedule error
48
47
  end
49
-
48
+
50
49
  def on_headers(headers)
51
50
  @request = Qeweney::Request.new(headers.to_h, self)
52
51
  @request.rx_incr(@adapter.get_rx_count)
@@ -55,31 +54,21 @@ module Tipi
55
54
  @request.headers[':first'] = true
56
55
  @first = false
57
56
  end
58
- @stream_fiber.schedule @request
57
+ @stream_fiber << @request
59
58
  end
60
59
 
61
60
  def on_data(data)
62
61
  data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
63
- if @waiting_for_body_chunk
64
- @waiting_for_body_chunk = nil
65
- @stream_fiber.schedule data
66
- else
67
- @request.buffer_body_chunk(data)
68
- end
62
+
63
+ (@buffered_chunks ||= []) << data
64
+ @get_body_chunk_fiber&.schedule
69
65
  end
70
66
 
71
67
  def on_half_close
72
- if @waiting_for_body_chunk
73
- @waiting_for_body_chunk = nil
74
- @stream_fiber.schedule
75
- elsif @waiting_for_half_close
76
- @waiting_for_half_close = nil
77
- @stream_fiber.schedule
78
- else
79
- @request.complete!
80
- end
68
+ @get_body_chunk_fiber&.schedule
69
+ @complete = true
81
70
  end
82
-
71
+
83
72
  def protocol
84
73
  'h2'
85
74
  end
@@ -90,33 +79,40 @@ module Tipi
90
79
  ensure
91
80
  @adapter.unset_request_for_transfer_count(request)
92
81
  end
93
-
94
- def get_body_chunk(request)
95
- # called in the context of the stream fiber
96
- return nil if @request.complete?
97
-
98
- with_transfer_count(request) do
99
- @waiting_for_body_chunk = true
100
- # the chunk (or an exception) will be returned once the stream fiber is
101
- # resumed
82
+
83
+ def get_body_chunk(request, buffered_only = false)
84
+ @buffered_chunks ||= []
85
+ return @buffered_chunks.shift unless @buffered_chunks.empty?
86
+ return nil if @complete
87
+
88
+ begin
89
+ @get_body_chunk_fiber = Fiber.current
102
90
  suspend
91
+ ensure
92
+ @get_body_chunk_fiber = nil
103
93
  end
104
- ensure
105
- @waiting_for_body_chunk = nil
94
+ @buffered_chunks.shift
106
95
  end
107
-
108
- # Wait for request to finish
109
- def consume_request(request)
110
- return if @request.complete?
111
-
112
- with_transfer_count(request) do
113
- @waiting_for_half_close = true
114
- suspend
96
+
97
+ def get_body(request)
98
+ @buffered_chunks ||= []
99
+ return @buffered_chunks.join if @complete
100
+
101
+ while !@complete
102
+ begin
103
+ @get_body_chunk_fiber = Fiber.current
104
+ suspend
105
+ ensure
106
+ @get_body_chunk_fiber = nil
107
+ end
115
108
  end
116
- ensure
117
- @waiting_for_half_close = nil
109
+ @buffered_chunks.join
110
+ end
111
+
112
+ def complete?(request)
113
+ @complete
118
114
  end
119
-
115
+
120
116
  # response API
121
117
  def respond(request, chunk, headers)
122
118
  headers[':status'] ||= Qeweney::Status::OK
@@ -153,10 +149,10 @@ module Tipi
153
149
  end
154
150
  end
155
151
  end
156
-
152
+
157
153
  def send_headers(request, headers, empty_response: false)
158
154
  return if @headers_sent
159
-
155
+
160
156
  headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
161
157
  with_transfer_count(request) do
162
158
  @stream.headers(transform_headers(headers), end_stream: false)
@@ -165,10 +161,10 @@ module Tipi
165
161
  rescue HTTP2::Error::StreamClosed
166
162
  # ignore
167
163
  end
168
-
164
+
169
165
  def send_chunk(request, chunk, done: false)
170
166
  send_headers({}, false) unless @headers_sent
171
-
167
+
172
168
  if chunk
173
169
  with_transfer_count(request) do
174
170
  @stream.data(chunk, end_stream: done)
@@ -179,7 +175,7 @@ module Tipi
179
175
  rescue HTTP2::Error::StreamClosed
180
176
  # ignore
181
177
  end
182
-
178
+
183
179
  def finish(request)
184
180
  if @headers_sent
185
181
  @stream.close
@@ -192,10 +188,10 @@ module Tipi
192
188
  rescue HTTP2::Error::StreamClosed
193
189
  # ignore
194
190
  end
195
-
191
+
196
192
  def stop
197
- return if @done
198
-
193
+ return if @complete
194
+
199
195
  @stream.close
200
196
  @stream_fiber.schedule(Polyphony::MoveOn.new)
201
197
  end