tipi 0.40 → 0.45

Sign up to get free protection for your applications and to get access to all the features.
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