raptor 0.9.0 → 0.10.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: 6dcf0e8beb3e5b74822eff407c51fbb0e83445ab54f75c37dad2cab2f9113ec5
4
- data.tar.gz: 9131a6153b10e93e2e0b717355645fe485a1db0af52c3472f07a35fa4f95d64f
3
+ metadata.gz: 1b7d4b6790a256864f0c2b4e8d061231c372a110493ef2846f547f4566232d5d
4
+ data.tar.gz: dc6454ad796395d9eb7ab81327410ddd2004f0e1dd0bb03857636c508e84f7bc
5
5
  SHA512:
6
- metadata.gz: 8a020f0e5c42eb4755a69c790f891aa334369d2ec58c063516893a53785574290feca84ebdd9d072b11601129f720eb2d82f52a7abbd7e0aa7bf79b17c57705c
7
- data.tar.gz: 3b90235078097337dcade23681c4b3ae638d04094a40a5c21e81f8aac127bfa547dd8d07e7dac1adda539299ab62c3883d15d84a692942da2a1b69eed7946035
6
+ metadata.gz: 6c0198c8697a569b7caf4492cf80c64edfbe7e6d0c1b4d7be7880175655bfa36b89040d3ab5b9597db995ac469eed26a1c74b7bb7c0c3206c9c47193b3930a48
7
+ data.tar.gz: 6a73d6a927a118dea756b8012108dbdaf1f2462ea9ea5a6fbd4dabd56cfffa51a23f059f2c51c39cd5a4f41a3fc0e752022cd1963115edacecb3cd53e56b83fe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.10.0] - 2026-07-07
4
+
5
+ - Memoize the server port string for the Rack env
6
+ - Parse the Host header without regex
7
+ - Apply `TCP_NODELAY` on the listener rather than each accepted socket
8
+ - Buffer chunked body writes up to 512KB before flushing
9
+ - Add `QUERY_STRING` to the Rack env template
10
+ - Skip intermediate array allocations when formatting response headers
11
+ - Intern common HTTP header keys in the parser
12
+ - Reuse a Rack env template across HTTP/1.1 requests
13
+
3
14
  ## [0.9.0] - 2026-07-07
4
15
 
5
16
  - Reuse a per-thread response buffer for status lines and headers
data/README.md CHANGED
@@ -187,16 +187,17 @@ Worker 1 (phase 0): pid=91351, requests=1199, busy=1/3, backlog=0, booted, last_
187
187
 
188
188
  ## (Micro) Benchmarks
189
189
 
190
- Raptor 0.9.0 vs Puma 8.0.2, median of 3 runs across two workload profiles: IO-bound (sleep for a random 5-50ms then return small JSON) and CPU-bound (serialise a JSON array of 20-200 items).
191
-
192
- | Protocol | Workload | Raptor | Puma | +/- vs Puma |
193
- | --------------------- | -------- | ----------- | ---------- | ----------- |
194
- | HTTP/1.1 | IO | 0.4k req/s | 0.4k req/s | +3.4% |
195
- | HTTP/1.1 | CPU | 11.5k req/s | 9k req/s | +27.6% |
196
- | HTTP/1.1 (keep-alive) | IO | 0.4k req/s | 0.4k req/s | +1.5% |
197
- | HTTP/1.1 (keep-alive) | CPU | 28.8k req/s | 26k req/s | +10.9% |
198
- | HTTP/2 | IO | 0.3k req/s | N/A | - |
199
- | HTTP/2 | CPU | 29.6k req/s | N/A | - |
190
+ Raptor 0.10.0 vs Puma 8.0.2, median of 3 runs across two workload profiles: IO-bound (sleep for a random 5-50ms then
191
+ return small JSON) and CPU-bound (serialise a JSON array of 20-200 items).
192
+
193
+ | Protocol | Workload | Raptor | Puma | +/- vs Puma |
194
+ | --------------------- | -------- | ------------ | ------------ | ----------- |
195
+ | HTTP/1.1 | IO | 0.43k req/s | 0.41k req/s | +4.0% |
196
+ | HTTP/1.1 | CPU | 10.99k req/s | 9.22k req/s | +19.1% |
197
+ | HTTP/1.1 (keep-alive) | IO | 0.41k req/s | 0.40k req/s | +2.6% |
198
+ | HTTP/1.1 (keep-alive) | CPU | 27.42k req/s | 25.75k req/s | +6.5% |
199
+ | HTTP/2 | IO | 0.31k req/s | N/A | - |
200
+ | HTTP/2 | CPU | 26.28k req/s | N/A | - |
200
201
 
201
202
  > ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [aarch64-linux]
202
203
  > 4 workers, 3 threads, 24 concurrent connections
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  #include "ruby.h"
7
+ #include "ruby/encoding.h"
7
8
  #include <assert.h>
8
9
  #include <string.h>
9
10
  #include <ctype.h>
@@ -44,6 +45,75 @@ static VALUE global_server_protocol;
44
45
  static VALUE global_request_path;
45
46
  static VALUE global_fragment;
46
47
 
48
+ struct common_field {
49
+ const char *name;
50
+ size_t len;
51
+ VALUE interned;
52
+ };
53
+
54
+ #define FIELD(name) { name, sizeof(name) - 1, Qnil }
55
+
56
+ static struct common_field common_fields[] = {
57
+ FIELD("HTTP_HOST"),
58
+ FIELD("HTTP_USER_AGENT"),
59
+ FIELD("HTTP_CONNECTION"),
60
+ FIELD("HTTP_ACCEPT"),
61
+ FIELD("HTTP_ACCEPT_ENCODING"),
62
+ FIELD("HTTP_ACCEPT_LANGUAGE"),
63
+ FIELD("HTTP_ACCEPT_CHARSET"),
64
+ FIELD("HTTP_COOKIE"),
65
+ FIELD("HTTP_REFERER"),
66
+ FIELD("HTTP_CACHE_CONTROL"),
67
+ FIELD("HTTP_PRAGMA"),
68
+
69
+ FIELD("CONTENT_LENGTH"),
70
+ FIELD("CONTENT_TYPE"),
71
+ FIELD("HTTP_TRANSFER_ENCODING"),
72
+
73
+ FIELD("HTTP_AUTHORIZATION"),
74
+ FIELD("HTTP_ORIGIN"),
75
+ FIELD("HTTP_EXPECT"),
76
+
77
+ FIELD("HTTP_IF_MATCH"),
78
+ FIELD("HTTP_IF_NONE_MATCH"),
79
+ FIELD("HTTP_IF_MODIFIED_SINCE"),
80
+ FIELD("HTTP_IF_UNMODIFIED_SINCE"),
81
+ FIELD("HTTP_IF_RANGE"),
82
+ FIELD("HTTP_RANGE"),
83
+
84
+ FIELD("HTTP_UPGRADE"),
85
+ FIELD("HTTP_UPGRADE_INSECURE_REQUESTS"),
86
+
87
+ FIELD("HTTP_SEC_FETCH_DEST"),
88
+ FIELD("HTTP_SEC_FETCH_MODE"),
89
+ FIELD("HTTP_SEC_FETCH_SITE"),
90
+ FIELD("HTTP_SEC_FETCH_USER"),
91
+ FIELD("HTTP_SEC_CH_UA"),
92
+ FIELD("HTTP_SEC_CH_UA_MOBILE"),
93
+ FIELD("HTTP_SEC_CH_UA_PLATFORM"),
94
+ FIELD("HTTP_DNT"),
95
+
96
+ FIELD("HTTP_X_FORWARDED_FOR"),
97
+ FIELD("HTTP_X_FORWARDED_HOST"),
98
+ FIELD("HTTP_X_FORWARDED_PROTO"),
99
+ FIELD("HTTP_X_FORWARDED_SCHEME"),
100
+ FIELD("HTTP_X_FORWARDED_SSL"),
101
+ FIELD("HTTP_X_REAL_IP")
102
+ };
103
+
104
+ #undef FIELD
105
+
106
+ #define NUM_COMMON_FIELDS (sizeof(common_fields) / sizeof(common_fields[0]))
107
+
108
+ static VALUE raptor_http_intern_field(const char *buf, size_t len) {
109
+ for (size_t i = 0; i < NUM_COMMON_FIELDS; i++) {
110
+ if (common_fields[i].len == len && memcmp(common_fields[i].name, buf, len) == 0) {
111
+ return common_fields[i].interned;
112
+ }
113
+ }
114
+ return rb_enc_interned_str(buf, len, rb_utf8_encoding());
115
+ }
116
+
47
117
  static inline void upcase_header_char(char *c) {
48
118
  if (*c >= 'a' && *c <= 'z')
49
119
  *c &= ~0x20;
@@ -294,16 +364,19 @@ tr26:
294
364
  else if (parser->field_len == 12 && memcmp(field_ptr, "CONTENT_TYPE", 12) == 0)
295
365
  needs_http_prefix = 0;
296
366
 
367
+ size_t key_len;
297
368
  if (needs_http_prefix) {
298
369
  memcpy(parser->buf, "HTTP_", 5);
299
370
  memcpy(parser->buf + 5, field_ptr, parser->field_len);
300
- parser->buf[5 + parser->field_len] = '\0';
371
+ key_len = 5 + parser->field_len;
372
+ parser->buf[key_len] = '\0';
301
373
  } else {
302
374
  memcpy(parser->buf, field_ptr, parser->field_len);
303
- parser->buf[parser->field_len] = '\0';
375
+ key_len = parser->field_len;
376
+ parser->buf[key_len] = '\0';
304
377
  }
305
378
 
306
- VALUE key = rb_str_new2(parser->buf);
379
+ VALUE key = raptor_http_intern_field(parser->buf, key_len);
307
380
  VALUE value = rb_str_new(PTR_TO(mark), value_len);
308
381
 
309
382
  char *value_ptr = RSTRING_PTR(value);
@@ -351,16 +424,19 @@ tr29:
351
424
  else if (parser->field_len == 12 && memcmp(field_ptr, "CONTENT_TYPE", 12) == 0)
352
425
  needs_http_prefix = 0;
353
426
 
427
+ size_t key_len;
354
428
  if (needs_http_prefix) {
355
429
  memcpy(parser->buf, "HTTP_", 5);
356
430
  memcpy(parser->buf + 5, field_ptr, parser->field_len);
357
- parser->buf[5 + parser->field_len] = '\0';
431
+ key_len = 5 + parser->field_len;
432
+ parser->buf[key_len] = '\0';
358
433
  } else {
359
434
  memcpy(parser->buf, field_ptr, parser->field_len);
360
- parser->buf[parser->field_len] = '\0';
435
+ key_len = parser->field_len;
436
+ parser->buf[key_len] = '\0';
361
437
  }
362
438
 
363
- VALUE key = rb_str_new2(parser->buf);
439
+ VALUE key = raptor_http_intern_field(parser->buf, key_len);
364
440
  VALUE value = rb_str_new(PTR_TO(mark), value_len);
365
441
 
366
442
  char *value_ptr = RSTRING_PTR(value);
@@ -1237,6 +1313,11 @@ RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
1237
1313
  global_request_path = rb_str_new2("PATH_INFO");
1238
1314
  global_fragment = rb_str_new2("FRAGMENT");
1239
1315
 
1316
+ for (size_t i = 0; i < NUM_COMMON_FIELDS; i++) {
1317
+ common_fields[i].interned = rb_enc_interned_str(common_fields[i].name, common_fields[i].len, rb_utf8_encoding());
1318
+ rb_global_variable(&common_fields[i].interned);
1319
+ }
1320
+
1240
1321
  rb_define_alloc_func(cHttpParser, parser_alloc);
1241
1322
  rb_define_method(cHttpParser, "execute", parser_execute, 3);
1242
1323
  rb_define_method(cHttpParser, "finished?", parser_finished_p, 0);
data/lib/raptor/binder.rb CHANGED
@@ -258,6 +258,7 @@ module Raptor
258
258
  socket = Socket.new(addrinfo.afamily, Socket::SOCK_STREAM, 0)
259
259
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
260
260
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?(:SO_REUSEPORT)
261
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
261
262
  socket.bind(addrinfo)
262
263
  socket.listen(@socket_backlog)
263
264
 
data/lib/raptor/http1.rb CHANGED
@@ -19,6 +19,7 @@ module Raptor
19
19
  #
20
20
  class Http1
21
21
  BODY_BUFFER_THRESHOLD = 256 * 1024
22
+ CHUNKED_WRITE_THRESHOLD = 512 * 1024
22
23
  FILE_CHUNK_SIZE = 64 * 1024
23
24
  MAX_CHUNK_OVERHEAD = 16 * 1024
24
25
  READ_BUFFER_SIZE = 64 * 1024
@@ -123,6 +124,7 @@ module Raptor
123
124
 
124
125
  # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
125
126
  # @rbs @server_port: Integer
127
+ # @rbs @server_port_string: String
126
128
  # @rbs @write_timeout: Integer
127
129
  # @rbs @max_body_size: Integer?
128
130
  # @rbs @body_spool_threshold: Integer?
@@ -130,6 +132,7 @@ module Raptor
130
132
  # @rbs @access_log_io: IO?
131
133
  # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
132
134
  # @rbs @running: AtomicBoolean
135
+ # @rbs @env_template: Hash[String, untyped]
133
136
 
134
137
  # Creates a new Http1 handler.
135
138
  #
@@ -149,6 +152,7 @@ module Raptor
149
152
  def initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, on_error: nil)
150
153
  @app = app
151
154
  @server_port = server_port
155
+ @server_port_string = server_port.to_s.freeze
152
156
  @write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
153
157
  @max_body_size = connection_options[:max_body_size]
154
158
  @body_spool_threshold = connection_options[:body_spool_threshold]
@@ -156,6 +160,13 @@ module Raptor
156
160
  @access_log_io = access_log_io
157
161
  @on_error = on_error
158
162
  @running = AtomicBoolean.new(true)
163
+ @env_template = {
164
+ Rack::RACK_VERSION => Rack::VERSION,
165
+ Rack::RACK_IS_HIJACK => true,
166
+ Rack::SCRIPT_NAME => "",
167
+ Rack::QUERY_STRING => "",
168
+ Http::SERVER_SOFTWARE => Http::SERVER_SOFTWARE_VALUE
169
+ }.freeze
159
170
  end
160
171
 
161
172
  # Instance-level wrapper around {Http.socket_write} that applies the
@@ -230,7 +241,7 @@ module Raptor
230
241
  end
231
242
 
232
243
  parser = HttpParser.new
233
- env = {}
244
+ env = @env_template.dup
234
245
  nread = begin
235
246
  parser.execute(env, buffer, 0)
236
247
  rescue HttpParserError
@@ -291,12 +302,13 @@ module Raptor
291
302
  # @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
292
303
  def http_parser_worker
293
304
  max_body_size = @max_body_size
305
+ env_template = @env_template
294
306
 
295
307
  proc do |data|
296
308
  next Raptor::Http2.process_frames(data) if data[:protocol] == :http2
297
309
 
298
310
  parser = Raptor::HttpParser.new
299
- env = {}
311
+ env = env_template.dup
300
312
  nread = begin
301
313
  parser.execute(env, data[:buffer], 0)
302
314
  rescue Raptor::HttpParserError
@@ -560,7 +572,7 @@ module Raptor
560
572
  end
561
573
 
562
574
  parser = HttpParser.new
563
- env = {}
575
+ env = @env_template.dup
564
576
  nread = begin
565
577
  parser.execute(env, buffer, 0)
566
578
  rescue HttpParserError
@@ -694,53 +706,54 @@ module Raptor
694
706
  #
695
707
  # @rbs (Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, TCPSocket socket, ?remote_addr: String, ?url_scheme: String) -> Hash[String, untyped]
696
708
  def build_rack_env(env, parse_data, body, socket, remote_addr: Server::DEFAULT_REMOTE_ADDR, url_scheme: Server::HTTP_SCHEME)
697
- env[Rack::RACK_VERSION] = Rack::VERSION
698
709
  env[Rack::RACK_INPUT] = build_rack_input(body)
699
710
  env[Rack::RACK_ERRORS] = $stderr
700
711
  env[Rack::RACK_RESPONSE_FINISHED] = []
701
-
702
- env[Rack::RACK_IS_HIJACK] = true
703
712
  env[Rack::RACK_HIJACK] = proc do
704
713
  env[RACK_HIJACKED] = true
705
714
  env[RACK_HIJACK_IO] = socket
706
715
  socket
707
716
  end
708
-
709
717
  env[Rack::RACK_EARLY_HINTS] = proc do |hints|
710
718
  send_early_hints(socket, hints) rescue nil
711
719
  end
712
720
 
713
- env[Rack::SCRIPT_NAME] = "" unless env.key?(Rack::SCRIPT_NAME)
714
721
  env[Rack::PATH_INFO] = env.delete(Rack::REQUEST_PATH) if env.key?(Rack::REQUEST_PATH)
715
722
  env[Rack::PATH_INFO] = "" unless env.key?(Rack::PATH_INFO)
716
- env[Rack::QUERY_STRING] = "" unless env.key?(Rack::QUERY_STRING)
717
-
718
723
  if (content_length = parse_data[:content_length]).positive?
719
724
  env[Http::CONTENT_LENGTH] = content_length.to_s
720
725
  end
721
726
 
722
727
  env[Http::REMOTE_ADDR] = remote_addr
723
- env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
724
728
  env[Http::HTTP_VERSION] = env[Rack::SERVER_PROTOCOL]
725
729
 
726
730
  behind_tls_proxy = (url_scheme == Server::HTTP_SCHEME) && forwarded_https?(env)
727
731
  env[Rack::RACK_URL_SCHEME] = behind_tls_proxy ? Server::HTTPS_SCHEME : url_scheme
728
- default_port = behind_tls_proxy ? "443" : @server_port.to_s
732
+ default_port = behind_tls_proxy ? "443" : @server_port_string
729
733
 
730
734
  http_host = env[Rack::HTTP_HOST]
731
- if http_host
735
+ host = nil
736
+ port = nil
737
+ if http_host && !http_host.empty?
732
738
  if http_host.start_with?("[")
733
- host = http_host[/\A\[([^\]]+)\]/, 1]
734
- port = http_host[/\]:(\d+)\z/, 1]
739
+ bracket_end = http_host.index("]")
740
+ if bracket_end
741
+ host = http_host.byteslice(1, bracket_end - 1)
742
+ port_colon = http_host.index(":", bracket_end + 1)
743
+ port = port_colon && http_host.byteslice(port_colon + 1, http_host.bytesize - port_colon - 1)
744
+ end
735
745
  else
736
- host, port = http_host.split(":", 2)
746
+ colon = http_host.index(":")
747
+ if colon
748
+ host = http_host.byteslice(0, colon)
749
+ port = http_host.byteslice(colon + 1, http_host.bytesize - colon - 1)
750
+ else
751
+ host = http_host
752
+ end
737
753
  end
738
- env[Rack::SERVER_NAME] ||= host
739
- env[Rack::SERVER_PORT] ||= port || default_port
740
- else
741
- env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
742
- env[Rack::SERVER_PORT] ||= default_port
743
754
  end
755
+ env[Rack::SERVER_NAME] ||= host || Server::DEFAULT_SERVER_NAME
756
+ env[Rack::SERVER_PORT] ||= port || default_port
744
757
 
745
758
  env
746
759
  end
@@ -1048,8 +1061,6 @@ module Raptor
1048
1061
  else
1049
1062
  raise TypeError, "body must respond to each, to_ary, or to_path"
1050
1063
  end
1051
-
1052
- socket_write(socket, "0\r\n\r\n") if use_chunked
1053
1064
  end
1054
1065
 
1055
1066
  # Calculates content length from an array or file body without consuming it.
@@ -1089,10 +1100,16 @@ module Raptor
1089
1100
  def write_file_body(socket, response, path, content_length, use_chunked)
1090
1101
  File.open(path, "rb") do |file|
1091
1102
  if use_chunked
1092
- socket_write(socket, response)
1103
+ buffer = response
1093
1104
  while (chunk = file.read(FILE_CHUNK_SIZE))
1094
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
1105
+ buffer << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n"
1106
+ if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
1107
+ socket_write(socket, buffer)
1108
+ buffer = +""
1109
+ end
1095
1110
  end
1111
+ buffer << "0\r\n\r\n"
1112
+ socket_write(socket, buffer)
1096
1113
  elsif content_length && content_length < BODY_BUFFER_THRESHOLD
1097
1114
  response << file.read(content_length)
1098
1115
  socket_write(socket, response)
@@ -1136,7 +1153,7 @@ module Raptor
1136
1153
  raise TypeError, "body must yield String values" unless chunk.is_a?(String)
1137
1154
 
1138
1155
  if use_chunked
1139
- response << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
1156
+ response << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n0\r\n\r\n"
1140
1157
  socket_write(socket, response)
1141
1158
  else
1142
1159
  socket_writev(socket, [response, chunk])
@@ -1155,14 +1172,20 @@ module Raptor
1155
1172
  # @rbs (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
1156
1173
  def write_multiple_chunks(socket, response, body_array, use_chunked)
1157
1174
  if use_chunked
1158
- socket_write(socket, response)
1175
+ buffer = response
1159
1176
  body_array.each do |chunk|
1160
1177
  raise TypeError, "body must yield String values" unless chunk.is_a?(String)
1161
1178
 
1162
1179
  next if chunk.empty?
1163
1180
 
1164
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
1181
+ buffer << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n"
1182
+ if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
1183
+ socket_write(socket, buffer)
1184
+ buffer = +""
1185
+ end
1165
1186
  end
1187
+ buffer << "0\r\n\r\n"
1188
+ socket_write(socket, buffer)
1166
1189
  else
1167
1190
  body_array.each do |chunk|
1168
1191
  raise TypeError, "body must yield String values" unless chunk.is_a?(String)
@@ -1191,6 +1214,7 @@ module Raptor
1191
1214
 
1192
1215
  socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
1193
1216
  end
1217
+ socket_write(socket, "0\r\n\r\n")
1194
1218
  else
1195
1219
  body.each do |chunk|
1196
1220
  raise TypeError, "body must yield String values" unless chunk.is_a?(String)
@@ -1235,15 +1259,42 @@ module Raptor
1235
1259
  headers.each do |name, value|
1236
1260
  next if illegal_header_key?(name)
1237
1261
 
1238
- Array(value).flat_map { |entry| entry.to_s.split("\n") }.each do |header_value|
1239
- next if illegal_header_value?(header_value)
1240
-
1241
- result << "#{name}: #{header_value}\r\n"
1262
+ if value.is_a?(Array)
1263
+ value.each { |entry| append_header_value(result, name, entry) }
1264
+ else
1265
+ append_header_value(result, name, value)
1242
1266
  end
1243
1267
  end
1244
1268
  result
1245
1269
  end
1246
1270
 
1271
+ # Appends one or more `name: value` header lines to `result`. Newline-
1272
+ # separated values are emitted as separate lines; empty values and values
1273
+ # with illegal characters are skipped silently.
1274
+ #
1275
+ # @param result [String] the buffer to append to
1276
+ # @param name [String] the header name
1277
+ # @param value [Object] the header value (any object responding to `to_s`)
1278
+ # @return [void]
1279
+ #
1280
+ # @rbs (String result, String name, untyped value) -> void
1281
+ def append_header_value(result, name, value)
1282
+ string_value = value.is_a?(String) ? value : value.to_s
1283
+ return if string_value.empty?
1284
+
1285
+ if string_value.include?("\n")
1286
+ string_value.split("\n").each do |line|
1287
+ next if line.empty? || illegal_header_value?(line)
1288
+
1289
+ result << name << ": " << line << "\r\n"
1290
+ end
1291
+ else
1292
+ return if illegal_header_value?(string_value)
1293
+
1294
+ result << name << ": " << string_value << "\r\n"
1295
+ end
1296
+ end
1297
+
1247
1298
  # Calls all rack.response_finished callbacks registered in the environment.
1248
1299
  #
1249
1300
  # Callbacks are called in reverse registration order. Individual callback
@@ -79,6 +79,7 @@ module Raptor
79
79
  socket = Socket.new(addrinfo.afamily, Socket::SOCK_STREAM, 0)
80
80
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
81
81
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
82
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
82
83
  socket.bind(addrinfo)
83
84
  socket.listen(socket_backlog)
84
85
 
data/lib/raptor/server.rb CHANGED
@@ -179,7 +179,6 @@ module Raptor
179
179
  end
180
180
 
181
181
  if tcp_client.is_a?(TCPSocket)
182
- tcp_client.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
183
182
  remote_addr = tcp_client.remote_address.ip_address
184
183
  else
185
184
  remote_addr = DEFAULT_REMOTE_ADDR
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.9.0"
5
+ VERSION = "0.10.0"
6
6
  end
@@ -8,6 +8,8 @@ module Raptor
8
8
  class Http1
9
9
  BODY_BUFFER_THRESHOLD: untyped
10
10
 
11
+ CHUNKED_WRITE_THRESHOLD: untyped
12
+
11
13
  FILE_CHUNK_SIZE: untyped
12
14
 
13
15
  MAX_CHUNK_OVERHEAD: untyped
@@ -88,23 +90,27 @@ module Raptor
88
90
  # @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
89
91
  def self.decode_chunked: (String buffer, ?Integer? max_size) -> [ String, Symbol ]
90
92
 
91
- @running: AtomicBoolean
93
+ @app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
92
94
 
93
- @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
95
+ @server_port: Integer
94
96
 
95
- @access_log_io: IO?
97
+ @server_port_string: String
96
98
 
97
- @max_keepalive_requests: Integer
99
+ @write_timeout: Integer
100
+
101
+ @max_body_size: Integer?
98
102
 
99
103
  @body_spool_threshold: Integer?
100
104
 
101
- @max_body_size: Integer?
105
+ @max_keepalive_requests: Integer
102
106
 
103
- @write_timeout: Integer
107
+ @access_log_io: IO?
104
108
 
105
- @server_port: Integer
109
+ @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
106
110
 
107
- @app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
111
+ @running: AtomicBoolean
112
+
113
+ @env_template: Hash[String, untyped]
108
114
 
109
115
  # Creates a new Http1 handler.
110
116
  #
@@ -576,6 +582,18 @@ module Raptor
576
582
  # @rbs (Hash[String, String | Array[String]] headers) -> String
577
583
  def format_headers: (Hash[String, String | Array[String]] headers) -> String
578
584
 
585
+ # Appends one or more `name: value` header lines to `result`. Newline-
586
+ # separated values are emitted as separate lines; empty values and values
587
+ # with illegal characters are skipped silently.
588
+ #
589
+ # @param result [String] the buffer to append to
590
+ # @param name [String] the header name
591
+ # @param value [Object] the header value (any object responding to `to_s`)
592
+ # @return [void]
593
+ #
594
+ # @rbs (String result, String name, untyped value) -> void
595
+ def append_header_value: (String result, String name, untyped value) -> void
596
+
579
597
  # Calls all rack.response_finished callbacks registered in the environment.
580
598
  #
581
599
  # Callbacks are called in reverse registration order. Individual callback
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raptor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young