tp2 0.14.1 → 0.16

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: 3c99469ae66a0fa3a184c3196d2e7978a5ae1cdac9b55820a4badc176030033a
4
- data.tar.gz: 4e4f7646de8efdc89c23af826f2502c0aabc5ef8e7773272e4596ccf1c106822
3
+ metadata.gz: 9b39e332d3140ec95dee3edc265fec814873208d8b17797fccd5513b843b614b
4
+ data.tar.gz: f39b677a8e703b9d9f8a48aa223365b6355eb5810735812cf600ae1645351d89
5
5
  SHA512:
6
- metadata.gz: 3dc15dc21f2d199cf3d807cc99e82d2f624269862e9adbf49e0d1bcd29bf00046f6c90bdb9523decfa9ad78377278e2e56a0cb8b6cc30f990273aa9349fe5bed
7
- data.tar.gz: c236ef22298ffca5c23a432dfa31e83c243821b0c95d1705eeb109d0fd9312669e43551a9ca64d90e5f16abd9ec593f77416aa04306ab86d675e06a6890effb5
6
+ metadata.gz: 0cab6849c05ca00890073e2ef7fe2db7410fbda9ac10f4d908fba7c04e02452dc31ec4f9c24c56dd5719ca11c6bcd57de325b1e422fab4114515674e3273c854
7
+ data.tar.gz: 7cb455c7da2207024e6ce8d41435dee1c6ed2d4357b259d9b34a750f829504576304aac7cc8e998a5bd06eeca61db8e643311bcf67c0726528bf3a0f7d04699a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # 0.16 2025-09-11
2
+
3
+ - Remove support for HTTP/0.9, HTTP/1.0, use chunked transfer encoding
4
+ exclusively for HTTP responses
5
+ - Remove call to app.ready
6
+
7
+ # 0.15 2025-08-30
8
+
9
+ - Call app.ready after setting up, if method available
10
+ - Update uringmachine, qeweney deps
11
+
1
12
  # 0.14.1 2025-07-08
2
13
 
3
14
  - Add request elapsed time to log
data/README.md CHANGED
@@ -1,4 +1,10 @@
1
- # TP2
1
+ <p align="center"><img src="tp2-logo.png" /></p>
2
+
3
+ # TP2 - a io_uring-based app server for Ruby
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/tp2.svg)](http://rubygems.org/gems/tp2)
6
+ [![Tipi Test](https://github.com/digital-fabric/tp2/workflows/Tests/badge.svg)](https://github.com/digital-fabric/tp2/actions?query=workflow%3ATests)
7
+ [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/digital-fabric/tp2/blob/master/LICENSE)
2
8
 
3
9
  TP2 is an experimental HTTP server based on
4
10
  [UringMachine](https://github.com/digital-fabric/uringmachine) and [Qeweney](https://github.com/digital-fabric/qeweney).
data/TODO.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## Immediate
2
+
3
+ - [ ] Remove support for HTTP/0.9, HTTP/1.0
4
+ - [ ] Reply with 505 HTTP version not supported:
5
+
6
+ ```
7
+ < GET / HTTP/0.9
8
+
9
+ > HTTP/1.1 505
10
+ > Connection: close
11
+ ```
12
+
13
+ - [ ] Use chunked transfer encoding exclusively
14
+ - [ ] Cache rendered headers
15
+ - [ ] Look at sketch in ~/Desktop/docs/
16
+
17
+ - [ ] Add options for:
18
+ - [ ] Date header
19
+ - [ ] Server header
20
+
21
+ ##
22
+
1
23
  - Add failing tests for request bombs (requests with lots of bytes)
2
24
  - Add test for `ProtocolError` handling
3
25
  - Add limits with tests for:
@@ -2,10 +2,16 @@
2
2
 
3
3
  require 'qeweney'
4
4
  require 'stringio'
5
+ require 'tp2/errors'
5
6
 
6
7
  module TP2
7
- class HTTP1Connection
8
- attr_reader :fd, :response_headers
8
+ # Implements an HTTP/1.1 connection received by the TP2 server. This
9
+ # implementation rejects incoming HTTP/0.9 or HTTP/1.0 requests. The response
10
+ # body is sent exclusively using chunked transfer encoding. Request bodies are
11
+ # accepted using either fixed length (Content-Length header) or chunked
12
+ # transfer encoding.
13
+ class Connection
14
+ attr_reader :fd, :response_headers, :logger
9
15
 
10
16
  def initialize(machine, fd, opts, &app)
11
17
  @machine = machine
@@ -37,37 +43,52 @@ module TP2
37
43
  @machine.close_async(@fd)
38
44
  end
39
45
 
40
- # Returns true if connection should persist
46
+ # Processes an incoming request by parsing the headers, creating a request
47
+ # object and handing it off to the app handler. Returns true if the
48
+ # connection should be persisted.
41
49
  def serve_request
42
50
  headers = parse_headers
43
51
  return false if !headers
44
52
 
45
53
  request = Qeweney::Request.new(headers, self)
54
+
46
55
  request.start_stamp = monotonic_clock
47
56
  @app.call(request)
48
57
  persist_connection?(headers)
49
- rescue ProtocolError => e
50
- @logger&.error(
51
- message: 'Protocol error, closing connection',
52
- error: e
53
- )
54
- false
55
- rescue SystemCallError => e
56
- @logger&.error(
57
- message: 'I/O error, closing connection',
58
- error: e
59
- )
60
- false
61
58
  rescue StandardError => e
62
- @logger&.error(
63
- message: 'Internal error while serving request, abandoning connection',
64
- error: e
65
- )
59
+ handle_error(request, e)
60
+ false
61
+ end
62
+
63
+ # Handles an error encountered while serving a request by logging the error
64
+ # and optionally sending an error response with the relevant HTTP status
65
+ # code. For I/O errors, no response is sent.
66
+ #
67
+ # @param request [Qeweney::Request] HTTP request
68
+ # @param err [Exception] error
69
+ # @return [void]
70
+ def handle_error(request, err)
71
+ case err
72
+ when SystemCallError
73
+ log_error(err, 'I/O error')
74
+ when ProtocolError
75
+ log_error(err, err.message)
76
+ respond(request, err.message, ':status' => err.http_status)
77
+ else
78
+ log_error(err, 'Internal error')
79
+ return if !request || @done
66
80
 
67
- if request && !@done
68
81
  respond(request, 'Internal server error', ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
69
82
  end
70
- false
83
+ end
84
+
85
+ # Logs the given err and given message.
86
+ #
87
+ # @param err [Exception] error
88
+ # @param message [String] error message
89
+ # @return [void]
90
+ def log_error(err, message)
91
+ @logger&.error(message: "#{message}, closing connection", error: err)
71
92
  end
72
93
 
73
94
  def get_body(req)
@@ -118,16 +139,15 @@ module TP2
118
139
  # @param body [String] response body
119
140
  # @param headers
120
141
  def respond(request, body, headers)
121
- formatted_headers = format_headers(headers, body, false)
122
- request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
142
+ formatted_headers = format_headers(headers, body)
143
+ request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
123
144
  if body
124
- buf = formatted_headers + body
145
+ buf = "#{formatted_headers}#{body.bytesize.to_s(16)}\r\n#{body}\r\n#{EMPTY_CHUNK}"
125
146
  @machine.send(@fd, buf, buf.bytesize, SEND_FLAGS)
126
- # handle_write(formatted_headers + body)
127
147
  else
128
148
  @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
129
149
  end
130
- @logger&.info(request: request, response_headers: headers)
150
+ @logger&.info(request: request, response_headers: headers) if request
131
151
  @done = true
132
152
  @response_headers = headers
133
153
  end
@@ -137,10 +157,9 @@ module TP2
137
157
  # @param request [Qeweney::Request] HTTP request
138
158
  # @param headers [Hash] response headers
139
159
  # @param empty_response [boolean] whether a response body will be sent
140
- # @param chunked [boolean] whether to use chunked transfer encoding
141
160
  # @return [void]
142
- def send_headers(request, headers, empty_response: false, chunked: true)
143
- formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
161
+ def send_headers(request, headers, empty_response: false)
162
+ formatted_headers = format_headers(headers, !empty_response)
144
163
  request.tx_incr(formatted_headers.bytesize)
145
164
  @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
146
165
  @response_headers = headers
@@ -224,20 +243,15 @@ module TP2
224
243
 
225
244
  private
226
245
 
227
- RE_REQUEST_LINE = %r{^([a-z]+)\s+([^\s]+)\s+(http/[0-9.]{1,3})}i
246
+ RE_REQUEST_LINE = /^([a-z]+)\s+([^\s]+)\s+http\/([019\.]{1,3})/i
228
247
  RE_HEADER_LINE = /^([a-z0-9-]+):\s+(.+)/i
229
248
  MAX_REQUEST_LINE_LEN = 1 << 14 # 16KB
230
249
  MAX_HEADER_LINE_LEN = 1 << 10 # 1KB
231
250
  MAX_CHUNK_SIZE_LEN = 16
232
251
 
233
- class ProtocolError < StandardError
234
- end
235
-
236
252
  def persist_connection?(headers)
237
253
  connection = headers['connection']&.downcase
238
- return connection != 'close' if headers[':protocol'] == 'http/1.1'
239
-
240
- connection && connection != 'close'
254
+ return connection != 'close'
241
255
  end
242
256
 
243
257
  def parse_headers
@@ -263,12 +277,15 @@ module TP2
263
277
  return nil if !line
264
278
 
265
279
  m = line.match(RE_REQUEST_LINE)
266
- raise ProtocolError, "Invalid request line: #{line[0..2047].inspect}" if !m
280
+ raise ProtocolError, 'Invalid request line' if !m
281
+
282
+ http_version = m[3]
283
+ raise UnsupportedHTTPVersionError, 'HTTP version not supported' if http_version != '1.1'
267
284
 
268
285
  {
269
286
  ':method' => m[1].downcase,
270
287
  ':path' => m[2],
271
- ':protocol' => m[3].downcase
288
+ ':protocol' => 'http/1.1'
272
289
  }
273
290
  end
274
291
 
@@ -305,21 +322,16 @@ module TP2
305
322
  buffer ? (buffer << chunk) : chunk
306
323
  end
307
324
 
308
- def http1_1?(request)
309
- request.headers[':protocol'] == 'http/1.1'
310
- end
311
-
312
325
  INTERNAL_HEADER_REGEXP = /^:/
313
326
 
314
327
  # Formats response headers into an array. If empty_response is true(thy),
315
328
  # the response status code will default to 204, otherwise to 200.
316
329
  # @param headers [Hash] response headers
317
330
  # @param body [boolean] whether a response body will be sent
318
- # @param chunked [boolean] whether to use chunked transfer encoding
319
331
  # @return [String] formatted response headers
320
- def format_headers(headers, body, chunked)
332
+ def format_headers(headers, body)
321
333
  status = headers[':status'] || (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
322
- lines = format_status_line(body, status, chunked)
334
+ lines = format_status_line(body, status)
323
335
  headers.each do |k, v|
324
336
  next if k =~ INTERNAL_HEADER_REGEXP
325
337
 
@@ -329,11 +341,11 @@ module TP2
329
341
  lines
330
342
  end
331
343
 
332
- def format_status_line(body, status, chunked)
344
+ def format_status_line(body, status)
333
345
  if !body
334
346
  empty_status_line(status)
335
347
  else
336
- with_body_status_line(status, body, chunked)
348
+ with_body_status_line(status, body)
337
349
  end
338
350
  end
339
351
 
@@ -345,12 +357,8 @@ module TP2
345
357
  end
346
358
  end
347
359
 
348
- def with_body_status_line(status, body, chunked)
349
- if chunked
350
- +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
351
- else
352
- +"HTTP/1.1 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
353
- end
360
+ def with_body_status_line(status, body)
361
+ +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
354
362
  end
355
363
 
356
364
  def collect_header_lines(lines, key, value)
data/lib/tp2/errors.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TP2
4
+ class ProtocolError < StandardError
5
+ def http_status
6
+ Qeweney::Status::BAD_REQUEST
7
+ end
8
+ end
9
+
10
+ class UnsupportedHTTPVersionError < ProtocolError
11
+ def http_status
12
+ Qeweney::Status::HTTP_VERSION_NOT_SUPPORTED
13
+ end
14
+ end
15
+ end
data/lib/tp2/server.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'tp2/http1_connection'
3
+ require 'tp2/connection'
4
4
  require 'tp2/request_extensions'
5
5
  require 'tp2/rack_adapter'
6
6
 
@@ -100,7 +100,7 @@ module TP2
100
100
 
101
101
  def accept_incoming(listen_fd)
102
102
  @machine.accept_each(listen_fd) do |fd|
103
- conn = HTTP1Connection.new(@machine, fd, @opts, &@app)
103
+ conn = Connection.new(@machine, fd, @opts, &@app)
104
104
  f = @machine.spin(conn) do
105
105
  it.run
106
106
  ensure
data/lib/tp2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module TP2
2
- VERSION = '0.14.1'
2
+ VERSION = '0.16'
3
3
  end
data/test/helper.rb CHANGED
@@ -36,6 +36,29 @@ module Kernel
36
36
  end
37
37
  end
38
38
 
39
+ class Qeweney::Request
40
+ def response_headers
41
+ adapter.headers
42
+ end
43
+
44
+ def response_status
45
+ adapter.status
46
+ end
47
+
48
+ def response_body
49
+ adapter.body
50
+ end
51
+
52
+ def response_json
53
+ raise if response_content_type != 'application/json'
54
+ JSON.parse(response_body, symbolize_names: true)
55
+ end
56
+
57
+ def response_content_type
58
+ response_headers['Content-Type']
59
+ end
60
+ end
61
+
39
62
  module Minitest::Assertions
40
63
  def assert_in_range exp_range, act
41
64
  msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative './helper'
4
4
 
5
- class HTTP1ConnectionTest < Minitest::Test
5
+ class ConnectionTest < Minitest::Test
6
6
  def make_socket_pair
7
7
  port = 10000 + rand(30000)
8
8
  server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
@@ -25,7 +25,7 @@ class HTTP1ConnectionTest < Minitest::Test
25
25
  @reqs = []
26
26
  @hook = nil
27
27
  @app = ->(req) { @hook&.call(req); @reqs << req }
28
- @adapter = TP2::HTTP1Connection.new(@machine, @s_fd, {}, &@app)
28
+ @adapter = TP2::Connection.new(@machine, @s_fd, {}, &@app)
29
29
  end
30
30
 
31
31
  def teardown
@@ -48,8 +48,31 @@ class HTTP1ConnectionTest < Minitest::Test
48
48
  res == 0 ? nil : buf
49
49
  end
50
50
 
51
- def test_basic_request_parsing
51
+ def test_http_unsupported_versions
52
+ write_http_request "GET / HTTP/0.9\r\n\r\n"
53
+ @adapter.serve_request
54
+ response = read_client_side
55
+ assert_equal "HTTP/1.1 505\r\nTransfer-Encoding: chunked\r\n\r\n1a\r\nHTTP version not supported\r\n0\r\n\r\n", response
56
+
57
+ setup
58
+
52
59
  write_http_request "GET / HTTP/1.0\r\n\r\n"
60
+ @adapter.serve_request
61
+ response = read_client_side
62
+ assert_equal "HTTP/1.1 505\r\nTransfer-Encoding: chunked\r\n\r\n1a\r\nHTTP version not supported\r\n0\r\n\r\n", response
63
+
64
+ setup
65
+
66
+ @hook = ->(req) { req.respond('hi') }
67
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
68
+ @adapter.serve_request
69
+ @machine.close(@s_fd)
70
+ response = read_client_side
71
+ assert_equal "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nhi\r\n0\r\n\r\n", response
72
+ end
73
+
74
+ def test_basic_request_parsing
75
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
53
76
 
54
77
  @adapter.serve_request
55
78
  assert_equal 1, @reqs.size
@@ -58,7 +81,7 @@ class HTTP1ConnectionTest < Minitest::Test
58
81
  assert_equal({
59
82
  ':method' => 'get',
60
83
  ':path' => '/',
61
- ':protocol' => 'http/1.0'
84
+ ':protocol' => 'http/1.1'
62
85
  }, headers)
63
86
  end
64
87
 
@@ -196,34 +219,6 @@ class HTTP1ConnectionTest < Minitest::Test
196
219
  assert_equal '123456789abcdefghijklmnopqrstuv', body
197
220
  end
198
221
 
199
- def test_body_to_eof
200
- skip
201
-
202
- write_http_request <<~HTTP.crlf_lines
203
- POST /foo HTTP/1.1
204
- Server: foo.com
205
-
206
- barbaz
207
- HTTP
208
-
209
- @bodies = []
210
- @hook = ->(req) { @bodies << req.read }
211
-
212
- @adapter.run
213
- assert_equal 1, @reqs.size
214
-
215
- req0 = @reqs.shift
216
- headers = req0.headers
217
- assert_equal({
218
- ':method' => 'post',
219
- ':path' => '/foo',
220
- ':protocol' => 'http/1.1',
221
- 'server' => 'foo.com'
222
- }, headers)
223
- body = @bodies.shift
224
- assert_equal 'barbaz', body
225
- end
226
-
227
222
  def test_each_chunk
228
223
  write_http_request <<~HTTP.crlf_lines
229
224
  POST /foo HTTP/1.1
@@ -278,30 +273,12 @@ class HTTP1ConnectionTest < Minitest::Test
278
273
  assert_equal ['123456789abcdefghijklmnopqrstuv'], chunks
279
274
  end
280
275
 
281
- def test_that_server_uses_content_length_in_http_1_0
282
- @hook = ->(req) {
283
- req.respond('Hello, world!', {})
284
- }
285
-
286
- write_http_request "GET / HTTP/1.0\r\n\r\n"
287
- @adapter.run
288
- response = read_client_side
289
-
290
- expected = <<~HTTP.crlf_lines
291
- HTTP/1.1 200
292
- Content-Length: 13
293
-
294
- Hello, world!
295
- HTTP
296
- assert_equal(expected, response)
297
- end
298
-
299
276
  def test_204_status_on_empty_response
300
277
  @hook = ->(req) {
301
278
  req.respond(nil, {})
302
279
  }
303
280
 
304
- write_http_request "GET / HTTP/1.0\r\n\r\n"
281
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
305
282
  @adapter.run
306
283
  response = read_client_side
307
284
 
@@ -325,46 +302,29 @@ class HTTP1ConnectionTest < Minitest::Test
325
302
  @adapter.run
326
303
 
327
304
  response = read_client_side
328
- expected = <<~HTTP.crlf_lines.chomp
329
- HTTP/1.1 200
330
- Content-Length: 13
331
-
332
- Hello, world!
333
- HTTP
305
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n"
334
306
  assert_equal(expected, response)
335
307
  end
336
308
 
337
- def test_that_server_maintains_connection_when_using_keep_alives
309
+ def test_that_server_maintains_connection_if_no_connection_close_header
338
310
  @hook = ->(req) {
339
311
  req.respond('Hi', {})
340
312
  }
341
313
 
342
- write_http_request "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n", false
314
+ write_http_request "GET / HTTP/1.1\r\nConnection: close\r\n\r\n", false
343
315
  res = @adapter.serve_request
344
- assert_equal true, res
316
+ assert_equal false, res
345
317
 
346
318
  response = read_client_side
347
- assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
319
+ assert_equal("HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nHi\r\n0\r\n\r\n", response)
348
320
 
349
321
  write_http_request "GET / HTTP/1.1\r\n\r\n", false
350
322
  res = @adapter.serve_request
351
323
  assert_equal true, res
352
324
 
353
325
  response = read_client_side
354
- expected = <<~HTTP.crlf_lines
355
- HTTP/1.1 200
356
- Content-Length: 2
357
-
358
- Hi
359
- HTTP
326
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nHi\r\n0\r\n\r\n"
360
327
  assert_equal(expected, response)
361
-
362
- write_http_request "GET / HTTP/1.0\r\n\r\n"
363
- res = @adapter.serve_request
364
- assert_equal false, !!res
365
-
366
- response = read_client_side
367
- assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
368
328
  end
369
329
 
370
330
  def test_pipelining_client
@@ -381,15 +341,8 @@ class HTTP1ConnectionTest < Minitest::Test
381
341
  @adapter.run
382
342
  response = read_client_side
383
343
 
384
- expected = <<~HTTP.crlf_lines.chomp
385
- HTTP/1.1 200
386
- Content-Length: 13
387
-
388
- Hello, world!HTTP/1.1 200
389
- Content-Length: 14
390
-
391
- Hello, foobar!
392
- HTTP
344
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n" +
345
+ "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\ne\r\nHello, foobar!\r\n0\r\n\r\n"
393
346
  assert_equal(expected, response)
394
347
  end
395
348
 
@@ -538,7 +491,7 @@ class HTTP1ConnectionTest < Minitest::Test
538
491
 
539
492
  content = IO.read(__FILE__)
540
493
  file_size = content.bytesize
541
- expected = "HTTP/1.1 200\r\nContent-Length: 6\r\n\r\nfoobar"
494
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n0\r\n\r\n"
542
495
 
543
496
  assert_equal expected, response
544
497
  end
data/test/test_server.rb CHANGED
@@ -65,14 +65,25 @@ class ServerTest < Minitest::Test
65
65
  res == 0 ? nil : buf
66
66
  end
67
67
 
68
- def test_basic_app_response
68
+ def test_http_1_0_response
69
69
  @app = ->(req) {
70
70
  req.respond('Hello, world!', {})
71
71
  }
72
72
 
73
73
  write_http_request "GET / HTTP/1.0\r\n\r\n"
74
74
  response = read_client_side
75
- expected = "HTTP/1.1 200\r\nContent-Length: 13\r\n\r\nHello, world!"
75
+ expected = "HTTP/1.1 505\r\nTransfer-Encoding: chunked\r\n\r\n1a\r\nHTTP version not supported\r\n0\r\n\r\n"
76
+ assert_equal(expected, response)
77
+ end
78
+
79
+ def test_basic_app_response
80
+ @app = ->(req) {
81
+ req.respond('Hello, world!', {})
82
+ }
83
+
84
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
85
+ response = read_client_side
86
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n"
76
87
  assert_equal(expected, response)
77
88
  end
78
89
 
@@ -84,7 +95,8 @@ class ServerTest < Minitest::Test
84
95
  write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nSCHmet /bar HTTP/1.1\r\n\r\n"
85
96
 
86
97
  response = read_client_side
87
- expected = "HTTP/1.1 200\r\nContent-Length: 11\r\n\r\nmethod: getHTTP/1.1 200\r\nContent-Length: 14\r\n\r\nmethod: schmet"
98
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nmethod: get\r\n0\r\n\r\n" +
99
+ "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\ne\r\nmethod: schmet\r\n0\r\n\r\n"
88
100
  assert_equal(expected, response)
89
101
  end
90
102
 
@@ -104,7 +116,7 @@ class ServerTest < Minitest::Test
104
116
  @machine.snooze
105
117
 
106
118
  response = read_client_side
107
- expected = "HTTP/1.1 200\r\nContent-Length: 11\r\n\r\nTerminated!"
119
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nTerminated!\r\n0\r\n\r\n"
108
120
  assert_equal(expected, response)
109
121
  end
110
122
 
@@ -143,7 +155,7 @@ class ServerTest < Minitest::Test
143
155
  ':protocol' => 'http/1.1',
144
156
  'server' => 'foo.com',
145
157
  'content-length' => '3',
146
- ':tx' => 48,
158
+ ':tx' => 56,
147
159
  }, headers)
148
160
  body = @bodies.shift
149
161
  assert_equal 'abc', body
@@ -156,7 +168,7 @@ class ServerTest < Minitest::Test
156
168
  ':protocol' => 'http/1.1',
157
169
  'server' => 'bar.com',
158
170
  'content-length' => '6',
159
- ':tx' => 51,
171
+ ':tx' => 59,
160
172
  }, headers)
161
173
  body = @bodies.shift
162
174
  assert_equal 'defghi', body
data/tp2-logo.png ADDED
Binary file
data/tp2.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.required_ruby_version = '>= 3.4'
21
21
  s.executables = ['tp2']
22
22
 
23
- s.add_dependency 'uringmachine', '~> 0.16'
24
- s.add_dependency 'qeweney', '~> 0.21'
23
+ s.add_dependency 'uringmachine', '~> 0.18'
24
+ s.add_dependency 'qeweney', '~> 0.22'
25
25
  s.add_dependency 'rack', '~> 3.1.15'
26
26
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tp2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: '0.16'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.16'
18
+ version: '0.18'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.16'
25
+ version: '0.18'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: qeweney
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0.21'
32
+ version: '0.22'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.21'
39
+ version: '0.22'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: rack
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -71,7 +71,8 @@ files:
71
71
  - examples/rack.ru
72
72
  - examples/simple.rb
73
73
  - lib/tp2.rb
74
- - lib/tp2/http1_connection.rb
74
+ - lib/tp2/connection.rb
75
+ - lib/tp2/errors.rb
75
76
  - lib/tp2/logger.rb
76
77
  - lib/tp2/rack_adapter.rb
77
78
  - lib/tp2/request_extensions.rb
@@ -79,8 +80,9 @@ files:
79
80
  - lib/tp2/version.rb
80
81
  - test/helper.rb
81
82
  - test/run.rb
82
- - test/test_http1_connection.rb
83
+ - test/test_connection.rb
83
84
  - test/test_server.rb
85
+ - tp2-logo.png
84
86
  - tp2.gemspec
85
87
  homepage: https://github.com/noteflakes/tp2
86
88
  licenses:
@@ -107,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
109
  - !ruby/object:Gem::Version
108
110
  version: '0'
109
111
  requirements: []
110
- rubygems_version: 3.6.8
112
+ rubygems_version: 3.7.0.dev
111
113
  specification_version: 4
112
114
  summary: Experimental HTTP/1 server for UringMachine
113
115
  test_files: []