tipi 0.41 → 0.46

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 (69) 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 +3 -1
  5. data/CHANGELOG.md +34 -0
  6. data/Gemfile +7 -1
  7. data/Gemfile.lock +53 -33
  8. data/README.md +184 -8
  9. data/Rakefile +1 -7
  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 +3 -1
  18. data/df/server_utils.rb +48 -46
  19. data/examples/full_service.rb +13 -0
  20. data/examples/hello.rb +5 -0
  21. data/examples/hello.ru +3 -3
  22. data/examples/http1_parser.rb +10 -8
  23. data/examples/http_server.js +1 -1
  24. data/examples/http_server.rb +4 -1
  25. data/examples/http_server_graceful.rb +1 -1
  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 +320 -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/lib/tipi/controller/bare_polyphony.rb +0 -0
  36. data/lib/tipi/controller/bare_stock.rb +10 -0
  37. data/lib/tipi/controller/extensions.rb +37 -0
  38. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  39. data/lib/tipi/controller/web_polyphony.rb +353 -0
  40. data/lib/tipi/controller/web_stock.rb +635 -0
  41. data/lib/tipi/controller.rb +12 -0
  42. data/lib/tipi/digital_fabric/agent.rb +5 -5
  43. data/lib/tipi/digital_fabric/agent_proxy.rb +15 -8
  44. data/lib/tipi/digital_fabric/executive.rb +7 -3
  45. data/lib/tipi/digital_fabric/protocol.rb +3 -3
  46. data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
  47. data/lib/tipi/digital_fabric/service.rb +17 -18
  48. data/lib/tipi/handler.rb +2 -2
  49. data/lib/tipi/http1_adapter.rb +85 -124
  50. data/lib/tipi/http2_adapter.rb +29 -16
  51. data/lib/tipi/http2_stream.rb +52 -57
  52. data/lib/tipi/rack_adapter.rb +2 -2
  53. data/lib/tipi/response_extensions.rb +1 -1
  54. data/lib/tipi/supervisor.rb +75 -0
  55. data/lib/tipi/version.rb +1 -1
  56. data/lib/tipi/websocket.rb +3 -3
  57. data/lib/tipi.rb +9 -7
  58. data/test/coverage.rb +2 -2
  59. data/test/helper.rb +60 -12
  60. data/test/test_http_server.rb +14 -41
  61. data/test/test_request.rb +2 -29
  62. data/tipi.gemspec +10 -10
  63. metadata +80 -54
  64. data/examples/automatic_certificate.rb +0 -193
  65. data/ext/tipi/extconf.rb +0 -12
  66. data/ext/tipi/http1_parser.c +0 -534
  67. data/ext/tipi/http1_parser.h +0 -18
  68. data/ext/tipi/tipi_ext.c +0 -5
  69. data/lib/tipi/http1_adapter_new.rb +0 -293
@@ -36,7 +36,7 @@ module DigitalFabric
36
36
  process_incoming_messages(false)
37
37
  rescue GracefulShutdown
38
38
  puts "Proxy got graceful shutdown, left: #{@requests.size} requests" if @requests.size > 0
39
- process_incoming_messages(true)
39
+ move_on_after(15) { process_incoming_messages(true) }
40
40
  ensure
41
41
  # keep_alive_timer&.stop
42
42
  unmount
@@ -56,11 +56,11 @@ module DigitalFabric
56
56
  def unmount
57
57
  return unless @mounted
58
58
 
59
- @service.unmount(self)
59
+ @service.unmount(self)
60
60
  @mounted = nil
61
61
  end
62
62
 
63
- def shutdown
63
+ def send_shutdown
64
64
  send_df_message(Protocol.shutdown)
65
65
  @fiber.raise GracefulShutdown.new
66
66
  end
@@ -144,13 +144,20 @@ module DigitalFabric
144
144
  t0 = Time.now
145
145
  t1 = nil
146
146
  with_request do |id|
147
- send_df_message(Protocol.http_request(id, req))
147
+ msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)
148
+ send_df_message(msg)
148
149
  while (message = receive)
150
+ kind = message[Protocol::Attribute::KIND]
149
151
  unless t1
150
152
  t1 = Time.now
151
- @service.record_latency_measurement(t1 - t0, req)
153
+ if kind == Protocol::HTTP_RESPONSE
154
+ headers = message[Protocol::Attribute::HttpResponse::HEADERS]
155
+ status = (headers && headers[':status']) || 200
156
+ if status < Qeweney::Status::BAD_REQUEST
157
+ @service.record_latency_measurement(t1 - t0, req)
158
+ end
159
+ end
152
160
  end
153
- kind = message[Protocol::Attribute::KIND]
154
161
  attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]
155
162
  return if http_request_message(id, req, kind, attributes)
156
163
  end
@@ -240,7 +247,7 @@ module DigitalFabric
240
247
  else
241
248
  req.send_headers(headers) if headers && !req.headers_sent?
242
249
  req.send_chunk(body, done: complete) if body or complete
243
-
250
+
244
251
  if complete && transfer_count_key
245
252
  rx, tx = req.transfer_counts
246
253
  send_transfer_count(transfer_count_key, rx, tx)
@@ -284,7 +291,7 @@ module DigitalFabric
284
291
  response = receive
285
292
  case response[0]
286
293
  when Protocol::WS_RESPONSE
287
- headers = response[2] || {}
294
+ headers = response[2] || {}
288
295
  status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
289
296
  if status != Qeweney::Status::SWITCHING_PROTOCOLS
290
297
  req.respond(nil, headers)
@@ -16,7 +16,7 @@ module DigitalFabric
16
16
  @service.mount(route, self)
17
17
  @current_request_count = 0
18
18
  # @updater = spin_loop(:executive_updater, interval: 10) { update_service_stats }
19
- # update_service_stats
19
+ update_service_stats
20
20
  end
21
21
 
22
22
  def current_request_count
@@ -33,9 +33,13 @@ module DigitalFabric
33
33
  req.respond(message.to_json, { 'Content-Type' => 'text.json' })
34
34
  when '/stream/stats'
35
35
  stream_stats(req)
36
+ when '/upload'
37
+ req.respond("body: #{req.read.inspect}")
36
38
  else
37
39
  req.respond('Invalid path', { ':status' => Qeweney::Status::NOT_FOUND })
38
40
  end
41
+ rescue => e
42
+ puts "Error: #{e.inspect}"
39
43
  ensure
40
44
  @current_request_count -= 1
41
45
  end
@@ -43,7 +47,7 @@ module DigitalFabric
43
47
  def stream_stats(req)
44
48
  req.send_headers({ 'Content-Type' => 'text/event-stream' })
45
49
 
46
- @service.timer.every(10) do
50
+ every(10) do
47
51
  message = last_service_stats
48
52
  req.send_chunk(format_sse_event(message.to_json))
49
53
  end
@@ -79,7 +83,7 @@ module DigitalFabric
79
83
  raise 'Invalid output from top (cpu)'
80
84
  end
81
85
  cpu_utilization = 100 - Regexp.last_match(1).to_i
82
-
86
+
83
87
  unless top =~ TOP_MEM_REGEXP && Regexp.last_match(1) =~ TOP_MEM_FREE_REGEXP
84
88
  raise 'Invalid output from top (mem)'
85
89
  end
@@ -102,8 +102,8 @@ module DigitalFabric
102
102
  DF_UPGRADE_RESPONSE
103
103
  end
104
104
 
105
- def http_request(id, req)
106
- [ HTTP_REQUEST, id, req.headers, req.next_chunk, req.complete? ]
105
+ def http_request(id, headers, buffered_chunk, complete)
106
+ [ HTTP_REQUEST, id, headers, buffered_chunk, complete ]
107
107
  end
108
108
 
109
109
  def http_response(id, body, headers, complete, transfer_count_key = nil)
@@ -141,7 +141,7 @@ module DigitalFabric
141
141
  def ws_data(id, data)
142
142
  [ WS_DATA, id, data ]
143
143
  end
144
-
144
+
145
145
  def ws_close(id)
146
146
  [ WS_CLOSE, id ]
147
147
  end
@@ -17,10 +17,6 @@ module DigitalFabric
17
17
  @agent.get_http_request_body(@id, 1)
18
18
  end
19
19
 
20
- def consume_request(request)
21
- @agent.get_http_request_body(@id, nil)
22
- end
23
-
24
20
  def respond(request, body, headers)
25
21
  @agent.send_df_message(
26
22
  Protocol.http_response(@id, body, headers, true)
@@ -25,7 +25,7 @@ module DigitalFabric
25
25
  @http_latency_max = 0
26
26
  @last_counters = @counters.merge(stamp: Time.now.to_f - 1)
27
27
  @fiber = Fiber.current
28
- @timer = Polyphony::Timer.new('service_timer', resolution: 5)
28
+ # @timer = Polyphony::Timer.new('service_timer', resolution: 5)
29
29
  end
30
30
 
31
31
  def calculate_stats
@@ -50,6 +50,8 @@ module DigitalFabric
50
50
  switch_rate = backend_stats[:switch_count] / elapsed
51
51
  poll_rate = backend_stats[:poll_count] / elapsed
52
52
 
53
+ object_space_stats = ObjectSpace.count_objects
54
+
53
55
  {
54
56
  service: {
55
57
  agent_count: @agents.size,
@@ -73,6 +75,8 @@ module DigitalFabric
73
75
  process: {
74
76
  cpu_usage: cpu,
75
77
  rss: rss.to_f / 1024,
78
+ objects_total: object_space_stats[:TOTAL],
79
+ objects_free: object_space_stats[:FREE]
76
80
  }
77
81
  }
78
82
  end
@@ -81,10 +85,12 @@ module DigitalFabric
81
85
  s = `ps -p #{pid} -o %cpu,rss`
82
86
  cpu, rss = s.lines[1].chomp.strip.split(' ')
83
87
  [cpu.to_f, rss.to_i]
88
+ rescue Polyphony::BaseException
89
+ raise
84
90
  rescue Exception
85
91
  [nil, nil]
86
92
  end
87
-
93
+
88
94
  def get_stats
89
95
  calculate_stats
90
96
  end
@@ -117,14 +123,14 @@ module DigitalFabric
117
123
 
118
124
  puts format('slow request (%.1f): %p', latency, req.headers)
119
125
  end
120
-
126
+
121
127
  def http_request(req, allow_df_upgrade = false)
122
128
  @current_request_count += 1
123
129
  @counters[:http_requests] += 1
124
130
  @counters[:connections] += 1 if req.headers[':first']
125
131
 
126
132
  return upgrade_request(req, allow_df_upgrade) if req.upgrade_protocol
127
-
133
+
128
134
  inject_request_headers(req)
129
135
  agent = find_agent(req)
130
136
  unless agent
@@ -153,7 +159,7 @@ module DigitalFabric
153
159
  req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
154
160
  req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
155
161
  end
156
-
162
+
157
163
  def upgrade_request(req, allow_df_upgrade)
158
164
  case (protocol = req.upgrade_protocol)
159
165
  when 'df'
@@ -172,7 +178,7 @@ module DigitalFabric
172
178
  agent.http_upgrade(req, protocol)
173
179
  end
174
180
  end
175
-
181
+
176
182
  def df_upgrade(req)
177
183
  # we don't want to count connected agents
178
184
  @current_request_count -= 1
@@ -185,7 +191,7 @@ module DigitalFabric
185
191
  ensure
186
192
  @current_request_count += 1
187
193
  end
188
-
194
+
189
195
  def mount(route, agent)
190
196
  if route[:path]
191
197
  route[:path_regexp] = path_regexp(route[:path])
@@ -194,7 +200,7 @@ module DigitalFabric
194
200
  @agents[agent] = route
195
201
  @routing_changed = true
196
202
  end
197
-
203
+
198
204
  def unmount(agent)
199
205
  route = @agents[agent]
200
206
  return unless route
@@ -205,7 +211,7 @@ module DigitalFabric
205
211
  end
206
212
 
207
213
  INVALID_HOST = 'INVALID_HOST'
208
-
214
+
209
215
  def find_agent(req)
210
216
  compile_agent_routes if @routing_changed
211
217
 
@@ -231,13 +237,6 @@ module DigitalFabric
231
237
  @route_keys = @routes.keys
232
238
  end
233
239
 
234
- def wait_for_agent(wait_list)
235
- wait_list << Fiber.current
236
- @timer.move_on_after(10) { suspend }
237
- ensure
238
- wait_list.delete(self)
239
- end
240
-
241
240
  def path_regexp(path)
242
241
  /^#{path}/
243
242
  end
@@ -245,8 +244,8 @@ module DigitalFabric
245
244
  def graceful_shutdown
246
245
  @shutdown = true
247
246
  @agents.keys.each do |agent|
248
- if agent.respond_to?(:shutdown)
249
- agent.shutdown
247
+ if agent.respond_to?(:send_shutdown)
248
+ agent.send_shutdown
250
249
  else
251
250
  @agents.delete(agent)
252
251
  end
data/lib/tipi/handler.rb CHANGED
@@ -20,14 +20,14 @@ module Tipi
20
20
  ensure
21
21
  socket.close
22
22
  end
23
-
23
+
24
24
  ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
25
25
  H2_PROTOCOL = 'h2'
26
26
 
27
27
  def protocol_adapter(socket, opts)
28
28
  use_http2 = socket.respond_to?(:alpn_protocol) &&
29
29
  socket.alpn_protocol == H2_PROTOCOL
30
-
30
+
31
31
  klass = use_http2 ? HTTP2Adapter : HTTP1Adapter
32
32
  klass.new(socket, opts)
33
33
  end
@@ -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,19 +177,19 @@ 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
194
  chunk_size
238
195
  )
@@ -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"