raptor 0.16.0 → 0.17.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: 02fe742ea31e44d742de9f0ed5b94cc53901d39e25b054fe0bd4affd14e169c1
4
- data.tar.gz: 4279672bfd532f9bcadfb5ce81d85b8ba4921b8072e302bfc33bf74ce4609224
3
+ metadata.gz: fbd24155ed44e8b8faf1f5b11a6078a34ab58bf43c290f3ffcf2a4a582c74d16
4
+ data.tar.gz: 7d6a0f38240c4ae3e723650fcd7a9888f2b59b69f3145018a918524565324972
5
5
  SHA512:
6
- metadata.gz: 283c078528cfd9cdfdfb795e4587a60607fa382d09efe1194330d5a9a6c7f319af94bc63a2da62396c7aba8484a0041260fcd70b0d82a5c324c7fd6b9221abc5
7
- data.tar.gz: 9ee16f87805c340d17da270a81131f191b09500cbd622e3bd06bd32acff3f2ec215004e110187de1d01f6815fdcdfdf962c50c521398ce45e469358d3c155dd8
6
+ metadata.gz: 811414a1f6a2469ef38c20935564ba204efdc5988437be5d0fa8a167d85a42204abe2a743115d090b24cf2cc8489e8dffdf57a93c1313936dcfa04a7d2213768
7
+ data.tar.gz: 29a62a959b22ca99c55eea39919d8fe3618fdb5061054718c38503c658c803d77a3a2b40cc9b8089da917faf4041569545073c24c468f6e8552f0d113ee0d469
@@ -41,7 +41,7 @@ steps:
41
41
  soft_fail: true
42
42
  image: "rubylang/ruby:master-dev"
43
43
  commands:
44
- - apt-get update && apt-get install -y libyaml-dev
44
+ - apt-get update && apt-get install -y libyaml-dev libssl-dev
45
45
  - ruby -v
46
46
  - bundle install
47
47
  - bundle exec rake ci
@@ -52,7 +52,7 @@ steps:
52
52
  env:
53
53
  RUBY_YJIT_ENABLE: "1"
54
54
  commands:
55
- - apt-get update && apt-get install -y libyaml-dev
55
+ - apt-get update && apt-get install -y libyaml-dev libssl-dev
56
56
  - ruby -v
57
57
  - bundle install
58
58
  - bundle exec rake ci
@@ -63,7 +63,7 @@ steps:
63
63
  env:
64
64
  RUBY_GC_STRESS: "1"
65
65
  commands:
66
- - apt-get update && apt-get install -y libyaml-dev
66
+ - apt-get update && apt-get install -y libyaml-dev libssl-dev
67
67
  - ruby -v
68
68
  - bundle install
69
69
  - bundle exec rake ci
@@ -75,7 +75,7 @@ steps:
75
75
  RUBY_YJIT_ENABLE: "1"
76
76
  RUBY_GC_STRESS: "1"
77
77
  commands:
78
- - apt-get update && apt-get install -y libyaml-dev
78
+ - apt-get update && apt-get install -y libyaml-dev libssl-dev
79
79
  - ruby -v
80
80
  - bundle install
81
81
  - bundle exec rake ci
data/CHANGELOG.md CHANGED
@@ -1,10 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.17.0] - 2026-08-29
4
+
5
+ - Reserve BPF-routed connections before accept
6
+ - Skip unnecessary HTTP/1.1 response work
7
+ - Assemble HTTP/2 response headers in C
8
+ - Emit HTTP/1.1 response headers and chunked bodies in C
9
+ - Decode chunked request bodies in C
10
+
11
+ ## [0.16.1] - 2026-08-23
12
+
13
+ - Drop the stale `Ractors=NUM` option from the Rackup handler
14
+
3
15
  ## [0.16.0] - 2026-08-22
4
16
 
5
17
  - Parse HTTP/2 requests in a dedicated ractor pool
6
18
  - Parse the first HTTP/2 frame batch inline on accept
7
- - Nest the `ractors` config under `http1` and `http2`, defaulting both to `round(cores / workers)` clamped to 1..3 for HTTP/1.1 and 1..2 for HTTP/2
19
+ - Nest the `ractors` config under `http1` and `http2`
8
20
 
9
21
  ## [0.15.1] - 2026-08-16
10
22
 
data/Dockerfile CHANGED
@@ -18,8 +18,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
18
18
  && rm -rf /var/lib/apt/lists/*
19
19
 
20
20
  COPY --from=hey-builder /go/bin/hey /usr/local/bin/hey
21
- COPY --from=hey-builder /usr/local/go /usr/local/go
22
- ENV PATH=/usr/local/go/bin:${PATH}
23
21
 
24
22
  ENV BUNDLE_PATH=/workspace/.bundle
25
23
  ENV BUNDLE_APP_CONFIG=/workspace/.bundle
data/README.md CHANGED
@@ -37,7 +37,7 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
37
37
  ```
38
38
  > bundle exec raptor -w 10 -t 3 hello_world.ru
39
39
  [Raptor 83654|Main|Main] Cluster initializing:
40
- [Raptor 83654|Main|Main] ├─ Version: 0.16.0
40
+ [Raptor 83654|Main|Main] ├─ Version: 0.17.0
41
41
  [Raptor 83654|Main|Main] ├─ Ruby Version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
42
42
  [Raptor 83654|Main|Main] ├─ Environment: development
43
43
  [Raptor 83654|Main|Main] ├─ Master PID: 83654
@@ -201,28 +201,28 @@ Worker 1 (phase 0): pid=91351, requests=1199, busy=1/3, backlog=0, booted, last_
201
201
 
202
202
  ## (Micro) Benchmarks
203
203
 
204
- Raptor 0.16.0 vs Puma 8.0.2 vs PumaPlus 0.1.0 vs Falcon 0.57.0 across two workload profiles. **IO-bound** is a GET
205
- endpoint that interleaves 5-10 short sleeps (total 2.5-15ms) with small CPU work, simulating a read path that makes
206
- several DB or cache calls. **CPU-bound** is a POST endpoint that accepts a small JSON body, interleaves 3-5 chunks of
207
- JSON item building (total 450-1500 items) with sub-100µs sleeps, and returns the built array, simulating a write path
208
- that does most of its work in Ruby with a few near-zero-cost cache hits.
204
+ Raptor 0.17.0 vs Puma 8.0.2 vs Falcon 0.57.0 across two workload profiles. **IO-bound** is a GET endpoint that
205
+ interleaves 5-10 short sleeps (total 2.5-15ms) with small CPU work, simulating a read path that makes several DB or
206
+ cache calls. **CPU-bound** is a POST endpoint that accepts a small JSON body, interleaves 3-5 chunks of JSON item
207
+ building (total 450-1500 items) with sub-100µs sleeps, and returns the built array, simulating a write path that does
208
+ most of its work in Ruby with a few near-zero-cost cache hits.
209
209
 
210
210
  Each cell reports the median throughput and median p95 latency independently across 5 runs, so the two numbers in a row
211
211
  may come from different runs. Every run starts a fresh server process so the samples are independent of each other;
212
212
  state accumulated in a previous run cannot bias the next. Across the whole table, the widest spread
213
- ((max - min) / 2 / median) between runs of a single cell was ±24.8% for throughput and ±40.3% for p95.
213
+ ((max - min) / 2 / median) between runs of a single cell was ±22.4% for throughput and ±37.4% for p95.
214
214
 
215
- | Protocol | Workload | Raptor req/s | Raptor p95 | Puma req/s | Puma p95 | vs Puma req/s | vs Puma p95 | PumaPlus req/s | PumaPlus p95 | vs PumaPlus req/s | vs PumaPlus p95 | Falcon req/s | Falcon p95 | vs Falcon req/s | vs Falcon p95 |
216
- | --------------------- | -------- | ------------ | ---------- | ----------- | --------- | ------------- | ------------ | -------------- | ------------ | ----------------- | --------------- | ------------ | ---------- | --------------- | ------------- |
217
- | HTTP/1.1 | IO | 3.21k req/s | 63.20 ms | 1.48k req/s | 131.00 ms | 117.7% higher | 51.8% lower | 1.63k req/s | 84.20 ms | 97.0% higher | 24.9% lower | 12.27k req/s | 14.00 ms | 73.8% lower | 351.4% higher |
218
- | HTTP/1.1 | CPU | 8.29k req/s | 33.30 ms | 8.56k req/s | 21.20 ms | 3.2% lower | 57.1% higher | 8.08k req/s | 19.20 ms | 2.6% higher | 73.4% higher | 6.62k req/s | 27.00 ms | 25.2% higher | 23.3% higher |
219
- | HTTP/1.1 (keep-alive) | IO | 2.43k req/s | 54.40 ms | 1.44k req/s | 110.20 ms | 68.7% higher | 50.6% lower | 1.58k req/s | 87.10 ms | 54.2% higher | 37.5% lower | 6.28k req/s | 27.90 ms | 61.3% lower | 95.0% higher |
220
- | HTTP/1.1 (keep-alive) | CPU | 8.40k req/s | 18.80 ms | 8.49k req/s | 21.90 ms | 1.1% lower | 14.2% lower | 8.36k req/s | 18.40 ms | 0.4% higher | 2.2% higher | 7.09k req/s | 31.50 ms | 18.5% higher | 40.3% lower |
221
- | HTTP/2 | IO | 0.97k req/s | 197.97 ms | N/A | N/A | - | - | 1.52k req/s | 90.13 ms | 36.0% lower | 119.6% higher | 6.59k req/s | 27.21 ms | 85.2% lower | 627.6% higher |
222
- | HTTP/2 | CPU | 6.61k req/s | 31.98 ms | N/A | N/A | - | - | 8.05k req/s | 19.23 ms | 17.9% lower | 66.3% higher | 8.09k req/s | 45.04 ms | 18.3% lower | 29.0% lower |
215
+ | Protocol | Workload | Raptor req/s | Raptor p95 | Puma req/s | Puma p95 | vs Puma req/s | vs Puma p95 | Falcon req/s | Falcon p95 | vs Falcon req/s | vs Falcon p95 |
216
+ | --------------------- | -------- | ------------ | ---------- | ----------- | --------- | ------------- | ------------ | ------------ | ---------- | --------------- | ------------- |
217
+ | HTTP/1.1 | IO | 3.35k req/s | 59.60 ms | 1.51k req/s | 126.70 ms | 121.9% higher | 53.0% lower | 12.19k req/s | 14.10 ms | 72.6% lower | 322.7% higher |
218
+ | HTTP/1.1 | CPU | 8.31k req/s | 25.00 ms | 8.63k req/s | 21.10 ms | 3.7% lower | 18.5% higher | 6.57k req/s | 27.40 ms | 26.4% higher | 8.8% lower |
219
+ | HTTP/1.1 (keep-alive) | IO | 3.22k req/s | 46.90 ms | 1.47k req/s | 113.80 ms | 119.9% higher | 58.8% lower | 6.23k req/s | 28.20 ms | 48.2% lower | 66.3% higher |
220
+ | HTTP/1.1 (keep-alive) | CPU | 8.30k req/s | 23.30 ms | 8.41k req/s | 22.10 ms | 1.3% lower | 5.4% higher | 6.95k req/s | 32.30 ms | 19.5% higher | 27.9% lower |
221
+ | HTTP/2 | IO | 0.98k req/s | 196.28 ms | N/A | N/A | - | - | 6.55k req/s | 27.71 ms | 85.1% lower | 608.4% higher |
222
+ | HTTP/2 | CPU | 6.23k req/s | 33.11 ms | N/A | N/A | - | - | 6.92k req/s | 42.94 ms | 10.0% lower | 22.9% lower |
223
223
 
224
224
  > ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [aarch64-linux]
225
- > 10 worker processes; Raptor, Puma, and PumaPlus run 3 threads per worker, Falcon runs unbounded fibers per worker;
225
+ > 10 worker processes; Raptor and Puma run 3 threads per worker, Falcon runs unbounded fibers per worker;
226
226
  > 120 concurrent HTTP/1.1 client connections; 40 concurrent HTTP/2 client connections × 3 streams each
227
227
 
228
228
  See [bin/benchmark](bin/benchmark) for more details.
@@ -37,7 +37,7 @@ _[luma.com/t3l24s8h](https://luma.com/t3l24s8h)_
37
37
  - <big>I built a Ruby web server over the last few months</big>
38
38
  - <big>It's called Raptor</big>
39
39
  - <big>It runs Rack apps, like Puma and Falcon do</big>
40
- - <big>It's built around Ruby 4's Ractors</big>
40
+ - <big>It's built around a Ractor-parallel HTTP parser</big>
41
41
  - <big>It's fast enough that the numbers are interesting</big>
42
42
  - <big>It's not production-ready. Nobody's Rails app should be behind it yet.</big>
43
43
 
@@ -1,6 +1,6 @@
1
1
  # Raptor vs Puma: A Design Comparison
2
2
 
3
- Raptor is a Ruby web server designed around Ruby 4's Ractors. On CPU-bound HTTP/1.1 it holds a real edge over the latest Puma release on the same hardware regardless of keep-alive; on IO-heavy work fibers still win, and Falcon is the server to compare to there rather than Raptor or Puma. Raptor also speaks HTTP/2 natively, which Puma does not. This document is a systems-design walkthrough of how it manages both.
3
+ Raptor is a Ruby web server designed around a Ractor-parallel HTTP parser. On CPU-bound HTTP/1.1 it holds a real edge over the latest Puma release on the same hardware regardless of keep-alive; on IO-heavy work fibers still win, and Falcon is the server to compare to there rather than Raptor or Puma. Raptor also speaks HTTP/2 natively, which Puma does not. This document is a systems-design walkthrough of how it manages both.
4
4
 
5
5
  ## Why this document exists
6
6
 
@@ -20,7 +20,7 @@ This document focuses on Puma because Puma is the incumbent that any new Ruby we
20
20
 
21
21
  Raptor is a research project. It hasn't run production traffic. The numbers in the README come from a repeatable microbenchmark, not from a real deployment. The benchmark measures only the **server** work: accepting connections, parsing, dispatching, and writing responses. It does not measure your application. In a typical Rails app where most of a request's time goes to ActiveRecord and downstream services, the server accounts for maybe 5 to 15 percent of the total, so a +N% number in this table will show up as a much smaller improvement in production. The gap is real, but it isn't what a Rails app against a real database will report.
22
22
 
23
- The Raptor README carries the [current head-to-head numbers](../README.md#micro-benchmarks) against the latest Puma and Falcon releases, run on the same hardware with the same Rack app on a recent Ruby with YJIT enabled. All servers run one worker process per available CPU; Raptor, Puma, and PumaPlus use three threads per worker, and Falcon uses unbounded fibers per worker. Load generators use four client connections per app thread, so on the 10-core machine that produced the current numbers that's 120 concurrent HTTP/1.1 client connections and 40 h2 connections × 3 streams each.
23
+ The Raptor README carries the [current head-to-head numbers](../README.md#micro-benchmarks) against the latest Puma and Falcon releases, run on the same hardware with the same Rack app on a recent Ruby with YJIT enabled. All servers run one worker process per available CPU; Raptor and Puma use three threads per worker, and Falcon uses unbounded fibers per worker. Load generators use four client connections per app thread, so on the 10-core machine that produced the current numbers that's 120 concurrent HTTP/1.1 client connections and 40 h2 connections × 3 streams each.
24
24
 
25
25
  Two workload profiles are measured. **IO-bound** is a GET endpoint that does 5 to 10 short sleeps interleaved with small CPU work per request, simulating a read path that makes several DB or cache calls throughout its lifetime. **CPU-bound** is a POST endpoint with a small JSON body that builds a JSON response in 3 to 5 chunks interleaved with sub-100µs sleeps, simulating a write path that does most of its work in Ruby with a few near-zero-cost cache hits. The workloads are interleaved rather than a single bulk sleep or single bulk serialise so a fiber-per-connection server like Falcon doesn't look artificially good from one-shot IO, and the CPU-bound workload is heavily CPU-dominated by design (roughly 95% CPU / 5% IO by wall time) so it actually measures CPU work rather than smuggling in enough IO for fibers to multiplex.
26
26
 
@@ -2,7 +2,6 @@
2
2
  #include <bpf/bpf_helpers.h>
3
3
 
4
4
  #define MAX_WORKERS 64
5
- #define LOAD_TIE_TOLERANCE 1
6
5
 
7
6
  // Per-worker listening sockets, keyed by worker index.
8
7
  struct {
@@ -20,14 +19,12 @@ struct {
20
19
  __uint(max_entries, MAX_WORKERS + 1);
21
20
  } loads SEC(".maps");
22
21
 
23
- // Routes each incoming connection to the least-loaded worker. When the
24
- // spread between the busiest and idlest worker is within
25
- // `LOAD_TIE_TOLERANCE`, all workers are treated as tied and the connection
26
- // is placed by 4-tuple hash so bursts of accepts spread across workers
27
- // instead of clustering on whichever worker most recently reported the
28
- // lowest load.
22
+ // Uses the power-of-two-choices strategy and reserves the selected worker's
23
+ // slot before returning. Sampling two hash-selected workers avoids
24
+ // concentrating variable-cost requests, while the reservation prevents a
25
+ // burst from repeatedly selecting the same reported load.
29
26
  SEC("sk_reuseport")
30
- int select_least_loaded(struct sk_reuseport_md *ctx) {
27
+ int select_less_loaded(struct sk_reuseport_md *ctx) {
31
28
  __u32 count_key = 0;
32
29
  __u32 *count_ptr = bpf_map_lookup_elem(&loads, &count_key);
33
30
  if (!count_ptr || *count_ptr == 0) {
@@ -38,29 +35,23 @@ int select_least_loaded(struct sk_reuseport_md *ctx) {
38
35
  num_workers = MAX_WORKERS;
39
36
  }
40
37
 
41
- __u32 min_load = ~0u;
42
- __u32 max_load = 0;
43
- __u32 min_idx = 0;
44
-
45
- for (__u32 worker_idx = 0; worker_idx < MAX_WORKERS; worker_idx++) {
46
- if (worker_idx >= num_workers) {
47
- break;
48
- }
49
- __u32 worker_key = worker_idx + 1;
50
- __u32 *load_ptr = bpf_map_lookup_elem(&loads, &worker_key);
51
- if (!load_ptr) {
52
- continue;
53
- }
54
- if (*load_ptr < min_load) {
55
- min_load = *load_ptr;
56
- min_idx = worker_idx;
57
- }
58
- if (*load_ptr > max_load) {
59
- max_load = *load_ptr;
38
+ __u32 chosen_idx = ctx->hash % num_workers;
39
+ __u32 chosen_key = chosen_idx + 1;
40
+ __u32 *chosen_load = bpf_map_lookup_elem(&loads, &chosen_key);
41
+ if (num_workers > 1) {
42
+ __u32 alternate_idx = (chosen_idx + 1 + ((ctx->hash >> 16) % (num_workers - 1))) % num_workers;
43
+ __u32 alternate_key = alternate_idx + 1;
44
+ __u32 *alternate_load = bpf_map_lookup_elem(&loads, &alternate_key);
45
+ if (chosen_load && alternate_load && *alternate_load < *chosen_load) {
46
+ chosen_idx = alternate_idx;
47
+ chosen_load = alternate_load;
60
48
  }
61
49
  }
62
50
 
63
- __u32 chosen_idx = (max_load - min_load <= LOAD_TIE_TOLERANCE) ? (ctx->hash % num_workers) : min_idx;
51
+ if (chosen_load) {
52
+ __sync_fetch_and_add(chosen_load, 1);
53
+ }
54
+
64
55
  bpf_sk_select_reuseport(ctx, &socks, &chosen_idx, 0);
65
56
  return SK_PASS;
66
57
  }
@@ -37,6 +37,8 @@ typedef struct raptor_parser {
37
37
  #define FLAG_HAS_BODY 0x2
38
38
  #define FLAG_FINISHED 0x4
39
39
 
40
+ #define MAX_CHUNK_OVERHEAD (16 * 1024)
41
+
40
42
  static VALUE eHttpParserError;
41
43
  static VALUE global_request_method;
42
44
  static VALUE global_request_uri;
@@ -44,6 +46,10 @@ static VALUE global_query_string;
44
46
  static VALUE global_server_protocol;
45
47
  static VALUE global_request_path;
46
48
  static VALUE global_fragment;
49
+ static ID id_complete;
50
+ static ID id_incomplete;
51
+ static ID id_malformed;
52
+ static ID id_too_large;
47
53
 
48
54
  struct common_field {
49
55
  const char *name;
@@ -1316,6 +1322,200 @@ static VALUE parser_body(VALUE self) {
1316
1322
  return parser->body;
1317
1323
  }
1318
1324
 
1325
+ static const char *find_crlf(const char *buf, size_t len) {
1326
+ if (len < 2) return NULL;
1327
+ const char *end = buf + len - 1;
1328
+ for (const char *p = buf; p < end; p++) {
1329
+ if (*p == '\r' && *(p + 1) == '\n') return p;
1330
+ }
1331
+ return NULL;
1332
+ }
1333
+
1334
+ static VALUE parser_decode_chunked(int argc, VALUE *argv, VALUE self) {
1335
+ (void)self;
1336
+ VALUE buffer, max_size;
1337
+ rb_scan_args(argc, argv, "11", &buffer, &max_size);
1338
+ Check_Type(buffer, T_STRING);
1339
+
1340
+ const char *buf = RSTRING_PTR(buffer);
1341
+ size_t len = (size_t)RSTRING_LEN(buffer);
1342
+ size_t offset = 0;
1343
+ size_t overhead = 0;
1344
+ int has_max = !NIL_P(max_size);
1345
+ size_t max_bytes = has_max ? NUM2SIZET(max_size) : 0;
1346
+
1347
+ VALUE decoded = rb_str_buf_new((long)len);
1348
+
1349
+ while (offset < len) {
1350
+ const char *crlf = find_crlf(buf + offset, len - offset);
1351
+ if (!crlf) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
1352
+
1353
+ size_t line_len = (size_t)(crlf - (buf + offset));
1354
+ size_t size_len = line_len;
1355
+ for (size_t i = 0; i < line_len; i++) {
1356
+ if (buf[offset + i] == ';') { size_len = i; break; }
1357
+ }
1358
+ if (size_len == 0) return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
1359
+
1360
+ uint64_t chunk_size = 0;
1361
+ for (size_t i = 0; i < size_len; i++) {
1362
+ char c = buf[offset + i];
1363
+ uint64_t digit;
1364
+ if (c >= '0' && c <= '9') digit = (uint64_t)(c - '0');
1365
+ else if (c >= 'a' && c <= 'f') digit = (uint64_t)(c - 'a' + 10);
1366
+ else if (c >= 'A' && c <= 'F') digit = (uint64_t)(c - 'A' + 10);
1367
+ else return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
1368
+ if (chunk_size > (UINT64_MAX >> 4)) return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
1369
+ chunk_size = (chunk_size << 4) | digit;
1370
+ }
1371
+ if (chunk_size > (uint64_t)(SIZE_MAX / 2)) return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
1372
+
1373
+ size_t crlf_offset = offset + line_len;
1374
+
1375
+ if (chunk_size == 0) {
1376
+ size_t trailer_offset = crlf_offset + 2;
1377
+ while (1) {
1378
+ if (trailer_offset >= len) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
1379
+ const char *tcrlf = find_crlf(buf + trailer_offset, len - trailer_offset);
1380
+ if (!tcrlf) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
1381
+ if (tcrlf == buf + trailer_offset) return rb_ary_new_from_args(2, decoded, ID2SYM(id_complete));
1382
+ trailer_offset = (size_t)(tcrlf - buf) + 2;
1383
+ }
1384
+ }
1385
+
1386
+ size_t decoded_len = (size_t)RSTRING_LEN(decoded);
1387
+ if (has_max && (decoded_len + (size_t)chunk_size) > max_bytes) {
1388
+ return rb_ary_new_from_args(2, decoded, ID2SYM(id_too_large));
1389
+ }
1390
+
1391
+ overhead += line_len + 4;
1392
+ if (overhead > (decoded_len + (size_t)chunk_size + MAX_CHUNK_OVERHEAD)) {
1393
+ return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
1394
+ }
1395
+
1396
+ size_t data_offset = crlf_offset + 2;
1397
+ size_t available = len > data_offset ? len - data_offset : 0;
1398
+ size_t bytes_to_copy = (size_t)chunk_size < available ? (size_t)chunk_size : available;
1399
+ rb_str_cat(decoded, buf + data_offset, (long)bytes_to_copy);
1400
+
1401
+ offset = data_offset + (size_t)chunk_size + 2;
1402
+ }
1403
+
1404
+ return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
1405
+ }
1406
+
1407
+ static uint8_t illegal_header_key_bitmap[32];
1408
+ static uint8_t illegal_header_value_bitmap[32];
1409
+
1410
+ static void bitmap_set(uint8_t *bitmap, unsigned char byte) {
1411
+ bitmap[byte >> 3] |= (uint8_t)(1u << (byte & 7));
1412
+ }
1413
+
1414
+ static int contains_illegal_byte(const uint8_t *bitmap, const char *str, size_t len) {
1415
+ for (size_t i = 0; i < len; i++) {
1416
+ unsigned char byte = (unsigned char)str[i];
1417
+ if ((bitmap[byte >> 3] >> (byte & 7)) & 1) return 1;
1418
+ }
1419
+ return 0;
1420
+ }
1421
+
1422
+ static VALUE parser_illegal_header_key_p(VALUE self, VALUE key) {
1423
+ (void)self;
1424
+ Check_Type(key, T_STRING);
1425
+ return contains_illegal_byte(illegal_header_key_bitmap, RSTRING_PTR(key), (size_t)RSTRING_LEN(key)) ? Qtrue : Qfalse;
1426
+ }
1427
+
1428
+ static VALUE parser_illegal_header_value_p(VALUE self, VALUE value) {
1429
+ (void)self;
1430
+ Check_Type(value, T_STRING);
1431
+ return contains_illegal_byte(illegal_header_value_bitmap, RSTRING_PTR(value), (size_t)RSTRING_LEN(value)) ? Qtrue : Qfalse;
1432
+ }
1433
+
1434
+ static void emit_header_line(VALUE buffer, const char *name, long name_len,
1435
+ const char *value, long value_len) {
1436
+ if (value_len == 0) return;
1437
+ if (contains_illegal_byte(illegal_header_value_bitmap, value, (size_t)value_len)) return;
1438
+ rb_str_cat(buffer, name, name_len);
1439
+ rb_str_cat(buffer, ": ", 2);
1440
+ rb_str_cat(buffer, value, value_len);
1441
+ rb_str_cat(buffer, "\r\n", 2);
1442
+ }
1443
+
1444
+ static void emit_header(VALUE buffer, const char *name, long name_len, VALUE value) {
1445
+ VALUE str = RB_TYPE_P(value, T_STRING) ? value : rb_funcall(value, rb_intern("to_s"), 0);
1446
+ const char *ptr = RSTRING_PTR(str);
1447
+ long len = RSTRING_LEN(str);
1448
+ if (len == 0) return;
1449
+
1450
+ if (!memchr(ptr, '\n', (size_t)len)) {
1451
+ emit_header_line(buffer, name, name_len, ptr, len);
1452
+ return;
1453
+ }
1454
+
1455
+ long line_start = 0;
1456
+ for (long i = 0; i < len; i++) {
1457
+ if (ptr[i] == '\n') {
1458
+ emit_header_line(buffer, name, name_len, ptr + line_start, i - line_start);
1459
+ line_start = i + 1;
1460
+ }
1461
+ }
1462
+ if (line_start < len) {
1463
+ emit_header_line(buffer, name, name_len, ptr + line_start, len - line_start);
1464
+ }
1465
+ }
1466
+
1467
+ static int format_header_iter(VALUE key, VALUE value, VALUE data) {
1468
+ VALUE buffer = (VALUE)data;
1469
+ if (!RB_TYPE_P(key, T_STRING)) return ST_CONTINUE;
1470
+ const char *name = RSTRING_PTR(key);
1471
+ long name_len = RSTRING_LEN(key);
1472
+ if (contains_illegal_byte(illegal_header_key_bitmap, name, (size_t)name_len)) return ST_CONTINUE;
1473
+
1474
+ if (RB_TYPE_P(value, T_ARRAY)) {
1475
+ long entries = RARRAY_LEN(value);
1476
+ for (long i = 0; i < entries; i++) {
1477
+ emit_header(buffer, name, name_len, RARRAY_AREF(value, i));
1478
+ }
1479
+ } else {
1480
+ emit_header(buffer, name, name_len, value);
1481
+ }
1482
+ return ST_CONTINUE;
1483
+ }
1484
+
1485
+ static VALUE parser_format_headers(VALUE self, VALUE buffer, VALUE headers) {
1486
+ (void)self;
1487
+ Check_Type(buffer, T_STRING);
1488
+ Check_Type(headers, T_HASH);
1489
+ rb_hash_foreach(headers, format_header_iter, buffer);
1490
+ return buffer;
1491
+ }
1492
+
1493
+ static VALUE parser_chunked_encode(VALUE self, VALUE buffer, VALUE chunk) {
1494
+ (void)self;
1495
+ Check_Type(buffer, T_STRING);
1496
+ Check_Type(chunk, T_STRING);
1497
+ long chunk_len = RSTRING_LEN(chunk);
1498
+ if (chunk_len == 0) return buffer;
1499
+
1500
+ char hex_buf[17];
1501
+ int hex_len = snprintf(hex_buf, sizeof(hex_buf), "%lx", (unsigned long)chunk_len);
1502
+ rb_str_cat(buffer, hex_buf, hex_len);
1503
+ rb_str_cat(buffer, "\r\n", 2);
1504
+ rb_str_cat(buffer, RSTRING_PTR(chunk), chunk_len);
1505
+ rb_str_cat(buffer, "\r\n", 2);
1506
+ return buffer;
1507
+ }
1508
+
1509
+ static void init_illegal_header_bitmaps(void) {
1510
+ for (int byte = 0x00; byte <= 0x20; byte++) bitmap_set(illegal_header_key_bitmap, (unsigned char)byte);
1511
+ const char *key_specials = "()<>@,;:\\\"/[]?={}";
1512
+ for (const char *p = key_specials; *p; p++) bitmap_set(illegal_header_key_bitmap, (unsigned char)*p);
1513
+ bitmap_set(illegal_header_key_bitmap, 0x7F);
1514
+
1515
+ for (int byte = 0x00; byte <= 0x08; byte++) bitmap_set(illegal_header_value_bitmap, (unsigned char)byte);
1516
+ for (int byte = 0x0A; byte <= 0x1F; byte++) bitmap_set(illegal_header_value_bitmap, (unsigned char)byte);
1517
+ }
1518
+
1319
1519
  RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
1320
1520
  rb_ext_ractor_safe(true);
1321
1521
 
@@ -1337,6 +1537,13 @@ RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
1337
1537
  global_request_path = rb_str_new2("PATH_INFO");
1338
1538
  global_fragment = rb_str_new2("FRAGMENT");
1339
1539
 
1540
+ id_complete = rb_intern("complete");
1541
+ id_incomplete = rb_intern("incomplete");
1542
+ id_malformed = rb_intern("malformed");
1543
+ id_too_large = rb_intern("too_large");
1544
+
1545
+ init_illegal_header_bitmaps();
1546
+
1340
1547
  for (size_t i = 0; i < NUM_COMMON_FIELDS; i++) {
1341
1548
  common_fields[i].interned = rb_enc_interned_str(common_fields[i].name, common_fields[i].len, rb_utf8_encoding());
1342
1549
  rb_global_variable(&common_fields[i].interned);
@@ -1351,4 +1558,10 @@ RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
1351
1558
  rb_define_method(cHttpParser, "nread", parser_nread, 0);
1352
1559
  rb_define_method(cHttpParser, "reset", parser_reset, 0);
1353
1560
  rb_define_method(cHttpParser, "body", parser_body, 0);
1561
+ rb_define_singleton_method(cHttpParser, "decode_chunked", parser_decode_chunked, -1);
1562
+ rb_define_singleton_method(cHttpParser, "illegal_header_key?", parser_illegal_header_key_p, 1);
1563
+ rb_define_singleton_method(cHttpParser, "illegal_header_value?", parser_illegal_header_value_p, 1);
1564
+ rb_define_singleton_method(cHttpParser, "format_headers", parser_format_headers, 2);
1565
+ rb_define_singleton_method(cHttpParser, "chunked_encode", parser_chunked_encode, 2);
1566
+ rb_define_const(cHttpParser, "MAX_CHUNK_OVERHEAD", INT2FIX(MAX_CHUNK_OVERHEAD));
1354
1567
  }
@@ -599,6 +599,65 @@ static VALUE h2_encode_headers(VALUE self, VALUE headers) {
599
599
  return hpack_encode_header_block(headers);
600
600
  }
601
601
 
602
+ static const char *const HOP_BY_HOP_HEADERS[] = {
603
+ "connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection"
604
+ };
605
+ #define NUM_HOP_BY_HOP_HEADERS 5
606
+
607
+ static int is_hop_by_hop(const char *name, long name_len) {
608
+ for (size_t i = 0; i < NUM_HOP_BY_HOP_HEADERS; i++) {
609
+ size_t hbh_len = strlen(HOP_BY_HOP_HEADERS[i]);
610
+ if ((long)hbh_len == name_len && memcmp(name, HOP_BY_HOP_HEADERS[i], hbh_len) == 0) return 1;
611
+ }
612
+ return 0;
613
+ }
614
+
615
+ static VALUE ascii_lowercase_str(VALUE str) {
616
+ long len = RSTRING_LEN(str);
617
+ VALUE lower = rb_str_new(RSTRING_PTR(str), len);
618
+ char *ptr = RSTRING_PTR(lower);
619
+ for (long i = 0; i < len; i++) {
620
+ if (ptr[i] >= 'A' && ptr[i] <= 'Z') ptr[i] = (char)(ptr[i] | 0x20);
621
+ }
622
+ return lower;
623
+ }
624
+
625
+ static int response_headers_iter(VALUE key, VALUE value, VALUE data) {
626
+ VALUE pairs = data;
627
+ if (!RB_TYPE_P(key, T_STRING)) return ST_CONTINUE;
628
+
629
+ VALUE lowered = ascii_lowercase_str(key);
630
+ const char *lname = RSTRING_PTR(lowered);
631
+ long lname_len = RSTRING_LEN(lowered);
632
+
633
+ if (lname_len >= 5 && memcmp(lname, "rack.", 5) == 0) return ST_CONTINUE;
634
+ if (is_hop_by_hop(lname, lname_len)) return ST_CONTINUE;
635
+
636
+ if (RB_TYPE_P(value, T_ARRAY)) {
637
+ long entries = RARRAY_LEN(value);
638
+ for (long i = 0; i < entries; i++) {
639
+ VALUE entry = RARRAY_AREF(value, i);
640
+ VALUE str = RB_TYPE_P(entry, T_STRING) ? entry : rb_obj_as_string(entry);
641
+ rb_ary_push(pairs, rb_ary_new_from_args(2, lowered, str));
642
+ }
643
+ } else {
644
+ VALUE str = RB_TYPE_P(value, T_STRING) ? value : rb_obj_as_string(value);
645
+ rb_ary_push(pairs, rb_ary_new_from_args(2, lowered, str));
646
+ }
647
+ return ST_CONTINUE;
648
+ }
649
+
650
+ static VALUE h2_encode_response_headers(VALUE self, VALUE status, VALUE headers) {
651
+ (void)self;
652
+ Check_Type(headers, T_HASH);
653
+
654
+ VALUE pairs = rb_ary_new();
655
+ rb_ary_push(pairs, rb_ary_new_from_args(2, rb_str_new_lit(":status"), rb_obj_as_string(status)));
656
+ rb_hash_foreach(headers, response_headers_iter, pairs);
657
+
658
+ return hpack_encode_header_block(pairs);
659
+ }
660
+
602
661
  static VALUE h2_parse_frame(VALUE self, VALUE buffer) {
603
662
  (void)self;
604
663
  Check_Type(buffer, T_STRING);
@@ -765,6 +824,7 @@ RUBY_FUNC_EXPORTED void Init_raptor_http2(void) {
765
824
  rb_define_method(cHttp2Parser, "parse_frame", h2_parse_frame, 1);
766
825
  rb_define_method(cHttp2Parser, "parse_headers", h2_parse_headers, 2);
767
826
  rb_define_method(cHttp2Parser, "encode_headers", h2_encode_headers, 1);
827
+ rb_define_method(cHttp2Parser, "encode_response_headers", h2_encode_response_headers, 2);
768
828
  rb_define_method(cHttp2Parser, "parse_settings", h2_parse_settings, 1);
769
829
  rb_define_method(cHttp2Parser, "build_settings", h2_build_settings, 1);
770
830
  rb_define_method(cHttp2Parser, "build_frame", h2_build_frame, 4);
@@ -43,7 +43,6 @@ module Rackup
43
43
  "Host=HOST" => "Hostname to listen on (default: #{DEFAULT_OPTIONS[:Host]})",
44
44
  "Port=PORT" => "Port to listen on (default: #{DEFAULT_OPTIONS[:Port]})",
45
45
  "Workers=NUM" => "Number of worker processes (default: available processor count)",
46
- "Ractors=NUM" => "Number of pipeline ractors per worker (default: 1)",
47
46
  "Threads=NUM" => "Number of threads per worker (default: 3)",
48
47
  "Config=PATH" => "Load additional configuration from PATH"
49
48
  }
@@ -46,11 +46,11 @@ module Raptor
46
46
  # `[1, HTTP1_RACTOR_COUNT_CAP]`.
47
47
  #
48
48
  # @param worker_count [Integer] the configured worker count
49
+ # @param cores [Integer] the available processor count
49
50
  # @return [Integer]
50
51
  #
51
- # @rbs (Integer worker_count) -> Integer
52
- def self.default_http1_ractor_count(worker_count)
53
- cores = Integer(Concurrent.available_processor_count)
52
+ # @rbs (Integer worker_count, ?cores: Integer) -> Integer
53
+ def self.default_http1_ractor_count(worker_count, cores: Integer(Concurrent.available_processor_count))
54
54
  (cores.to_f / worker_count).round.clamp(1, HTTP1_RACTOR_COUNT_CAP)
55
55
  end
56
56
 
@@ -60,11 +60,11 @@ module Raptor
60
60
  # `[1, HTTP2_RACTOR_COUNT_CAP]`.
61
61
  #
62
62
  # @param worker_count [Integer] the configured worker count
63
+ # @param cores [Integer] the available processor count
63
64
  # @return [Integer]
64
65
  #
65
- # @rbs (Integer worker_count) -> Integer
66
- def self.default_http2_ractor_count(worker_count)
67
- cores = Integer(Concurrent.available_processor_count)
66
+ # @rbs (Integer worker_count, ?cores: Integer) -> Integer
67
+ def self.default_http2_ractor_count(worker_count, cores: Integer(Concurrent.available_processor_count))
68
68
  (cores.to_f / worker_count).round.clamp(1, HTTP2_RACTOR_COUNT_CAP)
69
69
  end
70
70