syntropy 0.28.2 → 0.29.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afaeaedff9f32e935a57dd4f4b10c3615f86da0dcac57bd73bc067bbd2d74e52
4
- data.tar.gz: 22c6daed846a2b953cdbb3386196caa18336761e78e108dbeb8c08da161f5278
3
+ metadata.gz: b63b4a2ba22db6a8edcb7fc01dbbc67ba36e7a6392bcb2d5cec18fbf03e961c9
4
+ data.tar.gz: 06b6347df280046f31da969514d9a844c3b6197f6e0a2cc73ddc804e0861a320
5
5
  SHA512:
6
- metadata.gz: 65e16a9fcad4eb182b042e84cb1ece66e3e20cb4c785d91ed821450033f276a5173837a485811b77dd9ae2234765a614bef99aaea3dbc4d934d1b96a44600e8b
7
- data.tar.gz: 27563059f9c8d1afce71ee25be57450de7c19eaa4ab5ef540d9a4f9ea07830d7d79d1ce7c836e25a3bc47191fc4ec038120dc6ea178802736bdcdc3ad92132e6
6
+ metadata.gz: 7c66c3ab9254e638dd0e0705680fb9d395ff87c539b2e55cc6005f5e70e7bb11d64b534c85e1ef06052ce194a70423b118872715c13e90b2e0c7082ecd2566fb
7
+ data.tar.gz: 9bb15db34ee78a8a6c8d2e4d87459368e85525d617ded497b78d1d693473f580877a9b177ee5181249a1e16d1c0352eb8d3afc670e3a211454ebaa828f86f107
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # 0.29.0 2026-05-07
2
+
3
+ - Use UM#file_watch for dev mode
4
+ - Integrate TP2 code
5
+ - Update UringMachine
6
+
1
7
  # 0.28.2 2026-01-30
2
8
 
3
9
  - Update TP2, UringMachine
data/README.md CHANGED
@@ -48,8 +48,6 @@ Syntropy is based on:
48
48
 
49
49
  - [UringMachine](https://github.com/digital-fabric/uringmachine) - a lean mean
50
50
  [io_uring](https://unixism.net/loti/what_is_io_uring.html) machine for Ruby.
51
- - [TP2](https://github.com/digital-fabric/tp2) - an io_uring-based web server for
52
- concurrent Ruby apps.
53
51
  - [Qeweney](https://github.com/digital-fabric/qeweney) a uniform interface for
54
52
  working with HTTP requests and responses.
55
53
  - [Papercraft](https://github.com/digital-fabric/papercraft) HTML templating with plain Ruby.
data/bin/syntropy CHANGED
@@ -84,11 +84,10 @@ env[:banner] = false
84
84
 
85
85
  # We set Syntropy.machine so we can reference it from anywhere
86
86
  env[:machine] = Syntropy.machine = UM.new
87
- env[:logger] = env[:logger] && TP2::Logger.new(env[:machine], **env)
87
+ env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
88
88
 
89
89
  require 'syntropy/version'
90
90
  require 'syntropy/dev_mode' if env[:dev_mode]
91
91
 
92
- env[:logger]&.info(message: "Running Syntropy version #{Syntropy::VERSION}")
93
92
  app = Syntropy::App.load(env)
94
- TP2.run(env) { app.call(it) }
93
+ Syntropy.run(env) { app.call(it) }
@@ -29,4 +29,4 @@ COPY --from=gems --chown=app:app /usr/local/bundle /usr/local/bundle
29
29
 
30
30
  EXPOSE 1234
31
31
 
32
- CMD ["bundle", "exec", "tp2", "."]
32
+ CMD ["bundle", "exec", "syntropy", "."]
data/lib/syntropy/app.rb CHANGED
@@ -7,8 +7,6 @@ require 'qeweney'
7
7
  require 'papercraft'
8
8
 
9
9
  require 'syntropy/errors'
10
- require 'syntropy/file_watch'
11
-
12
10
  require 'syntropy/module'
13
11
  require 'syntropy/routing_tree'
14
12
 
@@ -474,9 +472,9 @@ module Syntropy
474
472
  # @return [void]
475
473
  def start
476
474
  @machine.spin do
477
- # we do startup stuff asynchronously, in order to first let TP2 do its
478
- # setup tasks
479
- @machine.sleep 0.2
475
+ # we do startup stuff asynchronously, in order to first let Syntropy do
476
+ # its setup tasks.
477
+ @machine.sleep 0.1
480
478
  route_count = @routing_tree.static_map.size + @routing_tree.dynamic_map.size
481
479
  @logger&.info(
482
480
  message: "Serving from #{@root_dir} (#{route_count} routes found)"
@@ -493,11 +491,21 @@ module Syntropy
493
491
  def file_watcher_loop
494
492
  wf = @env[:watch_files]
495
493
  period = wf.is_a?(Numeric) ? wf : 0.1
496
- Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
494
+
495
+ @machine.file_watch(@root_dir, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) { |e|
496
+ fn = e[:fn]
497
497
  @logger&.info(message: 'File change detected', fn: fn)
498
498
  @module_loader.invalidate_fn(fn)
499
499
  debounce_file_change
500
- end
500
+ }
501
+
502
+
503
+
504
+ # Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
505
+ # @logger&.info(message: 'File change detected', fn: fn)
506
+ # @module_loader.invalidate_fn(fn)
507
+ # debounce_file_change
508
+ # end
501
509
  rescue Exception => e
502
510
  p e
503
511
  p e.backtrace
@@ -0,0 +1,402 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'qeweney'
4
+ require 'stringio'
5
+ require 'syntropy/errors'
6
+ require 'syntropy/request_extensions'
7
+
8
+ module Syntropy
9
+ # Implements an HTTP/1.1 connection received by the Syntropy server. This
10
+ # implementation rejects incoming HTTP/0.9 or HTTP/1.0 requests. The response
11
+ # body is sent exclusively using chunked transfer encoding. Request bodies are
12
+ # accepted using either fixed length (Content-Length header) or chunked
13
+ # transfer encoding.
14
+ class Connection
15
+ attr_reader :fd, :response_headers, :logger
16
+
17
+ def initialize(server, machine, fd, env, &app)
18
+ @server = server
19
+ @machine = machine
20
+ @fd = fd
21
+ @env = env
22
+ @logger = env[:logger]
23
+ @io = machine.io(fd, :socket)
24
+ @app = app
25
+
26
+ @done = nil
27
+ @response_headers = nil
28
+ end
29
+
30
+ def run
31
+ loop do
32
+ @done = nil
33
+ @response_headers = nil
34
+ persist = serve_request
35
+ break if !persist
36
+ end
37
+ rescue UM::Terminate
38
+ # server is terminated, do nothing
39
+ rescue StandardError => e
40
+ @logger&.error(
41
+ message: 'Uncaught error while running connection',
42
+ error: e
43
+ )
44
+ ensure
45
+ @machine.close_async(@fd)
46
+ end
47
+
48
+ # Processes an incoming request by parsing the headers, creating a request
49
+ # object and handing it off to the app handler. Returns true if the
50
+ # connection should be persisted.
51
+ def serve_request
52
+ @closed = nil
53
+ headers = parse_headers
54
+ return false if !headers
55
+
56
+ request = Qeweney::Request.new(headers, self)
57
+
58
+ request.start_stamp = monotonic_clock
59
+ @app.call(request)
60
+ persist_connection?(headers)
61
+ rescue StandardError => e
62
+ handle_error(request, e)
63
+ false
64
+ end
65
+
66
+ # Handles an error encountered while serving a request by logging the error
67
+ # and optionally sending an error response with the relevant HTTP status
68
+ # code. For I/O errors, no response is sent.
69
+ #
70
+ # @param request [Qeweney::Request] HTTP request
71
+ # @param err [Exception] error
72
+ # @return [void]
73
+ def handle_error(request, err)
74
+ case err
75
+ when SystemCallError
76
+ log_error(err, 'I/O error')
77
+ false
78
+ when ProtocolError
79
+ log_error(err, err.message)
80
+ respond(request, err.message, ':status' => err.http_status)
81
+ else
82
+ log_error(err, 'Internal error')
83
+ return if !request || @done
84
+
85
+ respond(request, 'Internal server error', ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
86
+ end
87
+ end
88
+
89
+ # Logs the given err and given message.
90
+ #
91
+ # @param err [Exception] error
92
+ # @param message [String] error message
93
+ # @return [void]
94
+ def log_error(err, message)
95
+ @logger&.error(message: "#{message}, closing connection", error: err)
96
+ end
97
+
98
+ def get_body(req)
99
+ headers = req.headers
100
+ return nil if headers[':body-done-reading']
101
+
102
+ content_length = headers['content-length']
103
+ if content_length
104
+
105
+ chunk = @io.read(content_length.to_i)
106
+ headers[':body-done-reading'] = true
107
+ return chunk
108
+ end
109
+
110
+ chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
111
+ if chunked_encoding
112
+ buf = +''
113
+ while (chunk = read_chunk(headers, nil))
114
+ buf << chunk
115
+ end
116
+ headers[':body-done-reading'] = true
117
+ return buf
118
+ end
119
+
120
+ nil
121
+ end
122
+
123
+ def get_body_chunk(req, _buffered_only = false)
124
+ headers = req.headers
125
+ content_length = headers['content-length']
126
+ if content_length
127
+ return nil if headers[':body-done-reading']
128
+
129
+ chunk = @io.read(content_length.to_i)
130
+ headers[':body-done-reading'] = true
131
+ return chunk
132
+ end
133
+
134
+ chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
135
+ return read_chunk(headers, nil) if chunked_encoding
136
+
137
+ return nil if headers[':body-done-reading']
138
+
139
+ # if content-length is not specified, we read to EOF, up to max 1MB size
140
+ chunk = read(1 << 20, nil, false)
141
+ headers[':body-done-reading'] = true
142
+ chunk
143
+ end
144
+
145
+ def complete?(req)
146
+ req.headers[':body-done-reading']
147
+ end
148
+
149
+ # response API
150
+
151
+ # Sets response headers before sending any response. This method is used to
152
+ # add headers such as Set-Cookie or cache control headers to a response
153
+ # before actually responding, specifically in middleware hooks.
154
+ #
155
+ # @param headers [Hash] response headers
156
+ # @return [void]
157
+ def set_response_headers(headers)
158
+ @response_headers ? @response_headers.merge!(headers) : @response_headers = headers
159
+ end
160
+
161
+ def set_cookie(*cookies)
162
+ existing_cookies = @response_headers && @response_headers['Set-Cookie']
163
+ if existing_cookies
164
+ @response_headers['Set-Cookie'] = existing_cookies + cookies
165
+ else
166
+ set_response_headers('Set-Cookie' => cookies)
167
+ end
168
+ end
169
+
170
+ SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
171
+
172
+ EMPTY_CHUNK = "0\r\n\r\n"
173
+ EMPTY_CHUNK_LEN = EMPTY_CHUNK.bytesize
174
+
175
+ CHUNKED_ENCODING_POSTLUDE = "\r\n#{EMPTY_CHUNK}"
176
+
177
+ # Sends response including headers and body. Waits for the request to complete
178
+ # if not yet completed. The body is sent using chunked transfer encoding.
179
+ # @param request [Qeweney::Request] HTTP request
180
+ # @param body [String] response body
181
+ # @param headers
182
+ def respond(request, body, headers)
183
+ headers = @response_headers.merge(headers) if @response_headers
184
+
185
+ formatted_headers = format_headers(headers, body)
186
+ @response_headers = headers
187
+ request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
188
+ if body
189
+ chunk_prelude = "#{body.bytesize.to_s(16)}\r\n"
190
+ @machine.sendv(@fd, formatted_headers, chunk_prelude, body, CHUNKED_ENCODING_POSTLUDE)
191
+ else
192
+ @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
193
+ end
194
+ @logger&.info(request: request, response_headers: headers) if request
195
+ @done = true
196
+ end
197
+
198
+ # Sends response headers. If empty_response is truthy, the response status
199
+ # code will default to 204, otherwise to 200.
200
+ # @param request [Qeweney::Request] HTTP request
201
+ # @param headers [Hash] response headers
202
+ # @param empty_response [boolean] whether a response body will be sent
203
+ # @return [void]
204
+ def send_headers(request, headers, empty_response: false)
205
+ formatted_headers = format_headers(headers, !empty_response)
206
+ request.tx_incr(formatted_headers.bytesize)
207
+ @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
208
+ @response_headers = headers
209
+ end
210
+
211
+ # Sends a response body chunk. If no headers were sent, default headers are
212
+ # sent using #send_headers. if the done option is true(thy), an empty chunk
213
+ # will be sent to signal response completion to the client.
214
+ # @param request [Qeweney::Request] HTTP request
215
+ # @param chunk [String] response body chunk
216
+ # @param done [boolean] whether the response is completed
217
+ # @return [void]
218
+ def send_chunk(request, chunk, done: false)
219
+ data = +''
220
+ data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
221
+ data << EMPTY_CHUNK if done
222
+ return if data.empty?
223
+
224
+ request.tx_incr(data.bytesize)
225
+ @machine.send(@fd, data, data.bytesize, SEND_FLAGS)
226
+ return if @done || !done
227
+
228
+ @logger&.info(request: request, response_headers: @response_headers)
229
+ @done = true
230
+ end
231
+
232
+ # Finishes the response to the current request. If no headers were sent,
233
+ # default headers are sent using #send_headers.
234
+ # @return [void]
235
+ def finish(request)
236
+ request.tx_incr(EMPTY_CHUNK_LEN)
237
+ @machine.send(@fd, EMPTY_CHUNK, EMPTY_CHUNK_LEN, SEND_FLAGS)
238
+ return if @done
239
+
240
+ @logger&.info(request, request, response_headers: @response_headers)
241
+ @done = true
242
+ end
243
+
244
+ def respond_with_static_file(req, path, env, cache_headers)
245
+ fd = @machine.open(path, UM::O_RDONLY)
246
+ env ||= {}
247
+ if env[:headers]
248
+ env[:headers].merge!(cache_headers)
249
+ else
250
+ env[:headers] = cache_headers
251
+ end
252
+
253
+ maxlen = env[:max_len] || 65_536
254
+ buf = String.new(capacity: maxlen)
255
+ headers_sent = nil
256
+ loop do
257
+ res = @machine.read(fd, buf, maxlen, 0)
258
+ if res < maxlen && !headers_sent
259
+ return respond(req, buf, env[:headers])
260
+ elsif res == 0
261
+ return finish(req)
262
+ end
263
+
264
+ if !headers_sent
265
+ send_headers(req, env[:headers])
266
+ headers_sent = true
267
+ end
268
+ done = res < maxlen
269
+ send_chunk(req, buf, done: done)
270
+ return if done
271
+ end
272
+ end
273
+
274
+ def close
275
+ return if @closed
276
+
277
+ @closed = true
278
+ @machine.shutdown(@fd, UM::SHUT_WR)
279
+ @machine.close_async(@fd)
280
+ end
281
+
282
+ def monotonic_clock
283
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
284
+ end
285
+
286
+ def with_stream
287
+ yield @io, @fd
288
+ end
289
+
290
+ private
291
+
292
+ RE_REQUEST_LINE = /^([a-z]+)\s+([^\s]+)\s+http\/([019\.]{1,3})/i
293
+ RE_HEADER_LINE = /^([a-z0-9-]+):\s+(.+)/i
294
+ MAX_REQUEST_LINE_LEN = 1 << 14 # 16KB
295
+ MAX_HEADER_LINE_LEN = 1 << 10 # 1KB
296
+ MAX_CHUNK_SIZE_LEN = 16
297
+
298
+ def persist_connection?(headers)
299
+ connection = headers['connection']&.downcase
300
+ return connection != 'close'
301
+ end
302
+
303
+ def parse_headers
304
+ headers = get_request_line(MAX_REQUEST_LINE_LEN)
305
+ return nil if !headers
306
+
307
+ loop do
308
+ line = @io.read_line(MAX_HEADER_LINE_LEN)
309
+ break if line.nil? || line.empty?
310
+
311
+ m = line.match(RE_HEADER_LINE)
312
+ raise ProtocolError, "Invalid header: #{line[0..2047].inspect}" if !m
313
+
314
+ headers[m[1].downcase] = m[2]
315
+ end
316
+
317
+ headers
318
+ end
319
+
320
+ def get_request_line(buf)
321
+ line = @io.read_line(MAX_REQUEST_LINE_LEN)
322
+ return nil if !line
323
+
324
+ m = line.match(RE_REQUEST_LINE)
325
+ raise ProtocolError, 'Invalid request line' if !m
326
+
327
+ http_version = m[3]
328
+ raise UnsupportedHTTPVersionError, 'HTTP version not supported' if http_version != '1.1'
329
+
330
+ {
331
+ ':method' => m[1].downcase,
332
+ ':path' => m[2],
333
+ ':protocol' => 'http/1.1'
334
+ }
335
+ end
336
+
337
+ def read_chunk(headers, buffer)
338
+ chunk_size_str = @io.read_line(MAX_CHUNK_SIZE_LEN)
339
+ return nil if !chunk_size_str
340
+
341
+ chunk_size = chunk_size_str.to_i(16)
342
+ if chunk_size == 0
343
+ headers[':body-done-reading'] = true
344
+ @io.read_line(0)
345
+ return nil
346
+ end
347
+
348
+ chunk = @io.read(chunk_size)
349
+ @io.read_line(0)
350
+
351
+ buffer ? (buffer << chunk) : chunk
352
+ end
353
+
354
+ INTERNAL_HEADER_REGEXP = /^:/
355
+
356
+ # Formats response headers into an array. If empty_response is true(thy),
357
+ # the response status code will default to 204, otherwise to 200.
358
+ # @param headers [Hash] response headers
359
+ # @param body [boolean] whether a response body will be sent
360
+ # @return [String] formatted response headers
361
+ def format_headers(headers, body)
362
+ status = headers[':status'] || (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
363
+ lines = format_status_line(body, status)
364
+ lines << @env[:server_headers] if @env[:server_headers]
365
+ headers.each do |k, v|
366
+ next if k =~ INTERNAL_HEADER_REGEXP
367
+
368
+ collect_header_lines(lines, k, v)
369
+ end
370
+ lines << "\r\n"
371
+ lines
372
+ end
373
+
374
+ def format_status_line(body, status)
375
+ if !body
376
+ empty_status_line(status)
377
+ else
378
+ with_body_status_line(status, body)
379
+ end
380
+ end
381
+
382
+ def empty_status_line(status)
383
+ if status == 204
384
+ +"HTTP/1.1 #{status}\r\n"
385
+ else
386
+ +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
387
+ end
388
+ end
389
+
390
+ def with_body_status_line(status, body)
391
+ +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
392
+ end
393
+
394
+ def collect_header_lines(lines, key, value)
395
+ if value.is_a?(Array)
396
+ value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
397
+ else
398
+ lines << "#{key}: #{value}\r\n"
399
+ end
400
+ end
401
+ end
402
+ end
@@ -71,4 +71,16 @@ module Syntropy
71
71
  super(msg, Status::BAD_REQUEST)
72
72
  end
73
73
  end
74
+
75
+ class ProtocolError < Error
76
+ def http_status
77
+ Qeweney::Status::BAD_REQUEST
78
+ end
79
+ end
80
+
81
+ class UnsupportedHTTPVersionError < ProtocolError
82
+ def http_status
83
+ Qeweney::Status::HTTP_VERSION_NOT_SUPPORTED
84
+ end
85
+ end
74
86
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Syntropy
6
+ class Logger
7
+ def initialize(machine, fd = $stdout.fileno, **opts)
8
+ @machine = machine
9
+ @fd = fd
10
+ @opts = opts
11
+ end
12
+
13
+ def info(o)
14
+ call(:INFO, o)
15
+ end
16
+
17
+ def warn(o)
18
+ call(:WARN, o)
19
+ end
20
+
21
+ def error(o)
22
+ call(:ERROR, o)
23
+ end
24
+
25
+ private
26
+
27
+ # @param level <Symbol> log level
28
+ # @param o <Hash> hash
29
+ def call(level, o)
30
+ emit(make_entry(level, o))
31
+ rescue StandardError => e
32
+ puts 'Uncaught error while emitting log entry:'
33
+ p e: e
34
+ p e.backtrace
35
+ exit
36
+ end
37
+
38
+ def emit(entry)
39
+ @machine.write_async(@fd, "#{entry.to_json}\n")
40
+ end
41
+
42
+ def make_entry(level, o)
43
+ if o[:request]
44
+ make_request_entry(level, o)
45
+ elsif o[:error]
46
+ make_error_entry(level, o)
47
+ else
48
+ make_hash_entry(level, o)
49
+ end
50
+ end
51
+
52
+ def make_error_entry(level, o)
53
+ err = o[:error]
54
+ {
55
+ level: level.to_s,
56
+ ts: (t = Time.now; t.to_i),
57
+ ts_s: t.iso8601
58
+ }
59
+ .merge(o)
60
+ .merge(
61
+ error: "#{err.class}: #{err.message}",
62
+ backtrace: err.backtrace
63
+ )
64
+ end
65
+
66
+ def make_request_entry(level, o)
67
+ request = o[:request]
68
+ request_headers = request.headers
69
+ response_headers = o[:response_headers]
70
+ elapsed = request.adapter.monotonic_clock - request.start_stamp
71
+ {
72
+ level: level.to_s,
73
+ ts: (t = Time.now; t.to_i),
74
+ ts_s: t.iso8601,
75
+ message: o[:message] || 'HTTP request done',
76
+ client_ip: request.forwarded_for || '?',
77
+ http_method: request_headers[':method'].upcase,
78
+ user_agent: request_headers['user-agent'],
79
+ uri: full_uri(request_headers),
80
+ status: response_headers[':status'] || '200',
81
+ elapsed: elapsed
82
+ }
83
+ end
84
+
85
+ def make_hash_entry(level, hash)
86
+ {
87
+ level: level.to_s,
88
+ ts: (t = Time.now; t.to_i),
89
+ ts_s: t.iso8601
90
+ }
91
+ .merge(hash)
92
+ end
93
+
94
+ def full_uri(headers)
95
+ format(
96
+ '%<scheme>s://%<host>s%<path>s',
97
+ scheme: headers['x_forwarded_proto'] || 'http',
98
+ host: headers['host'],
99
+ path: headers[':path']
100
+ )
101
+ end
102
+ end
103
+ end
@@ -3,6 +3,32 @@
3
3
  require 'qeweney'
4
4
  require 'json'
5
5
 
6
+ class Qeweney::Request
7
+ attr_accessor :start_stamp
8
+
9
+ def respond_with_static_file(path, etag, last_modified, opts)
10
+ cache_headers = (etag || last_modified) ? {
11
+ 'etag' => etag,
12
+ 'last-modified' => last_modified
13
+ } : {}
14
+
15
+ adapter.respond_with_static_file(self, path, opts, cache_headers)
16
+ end
17
+
18
+ def set_response_headers(headers)
19
+ adapter.set_response_headers(headers)
20
+ end
21
+
22
+ def set_cookie(*)
23
+ adapter.set_cookie(*)
24
+ end
25
+
26
+ def upgrade(protocol, custom_headers = nil, &block)
27
+ super(protocol, custom_headers)
28
+ adapter.with_stream(&block)
29
+ end
30
+ end
31
+
6
32
  module Syntropy
7
33
  # Extensions for the Qeweney::Request class
8
34
  module RequestExtensions