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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +11 -10
- data/ext/raptor_http/raptor_http.c +87 -6
- data/lib/raptor/binder.rb +1 -0
- data/lib/raptor/http1.rb +83 -32
- data/lib/raptor/reuseport_bpf.rb +1 -0
- data/lib/raptor/server.rb +0 -1
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/http1.rbs +26 -8
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b7d4b6790a256864f0c2b4e8d061231c372a110493ef2846f547f4566232d5d
|
|
4
|
+
data.tar.gz: dc6454ad796395d9eb7ab81327410ddd2004f0e1dd0bb03857636c508e84f7bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
| HTTP/1.1 |
|
|
196
|
-
| HTTP/1.1
|
|
197
|
-
| HTTP/1.1 (keep-alive) |
|
|
198
|
-
| HTTP/
|
|
199
|
-
| HTTP/2 |
|
|
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
|
-
|
|
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->
|
|
375
|
+
key_len = parser->field_len;
|
|
376
|
+
parser->buf[key_len] = '\0';
|
|
304
377
|
}
|
|
305
378
|
|
|
306
|
-
VALUE key =
|
|
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
|
-
|
|
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->
|
|
435
|
+
key_len = parser->field_len;
|
|
436
|
+
parser->buf[key_len] = '\0';
|
|
361
437
|
}
|
|
362
438
|
|
|
363
|
-
VALUE key =
|
|
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" : @
|
|
732
|
+
default_port = behind_tls_proxy ? "443" : @server_port_string
|
|
729
733
|
|
|
730
734
|
http_host = env[Rack::HTTP_HOST]
|
|
731
|
-
|
|
735
|
+
host = nil
|
|
736
|
+
port = nil
|
|
737
|
+
if http_host && !http_host.empty?
|
|
732
738
|
if http_host.start_with?("[")
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1103
|
+
buffer = response
|
|
1093
1104
|
while (chunk = file.read(FILE_CHUNK_SIZE))
|
|
1094
|
-
|
|
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 <<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
result
|
|
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
|
data/lib/raptor/reuseport_bpf.rb
CHANGED
|
@@ -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
data/lib/raptor/version.rb
CHANGED
|
@@ -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
|
-
@
|
|
93
|
+
@app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
|
|
92
94
|
|
|
93
|
-
@
|
|
95
|
+
@server_port: Integer
|
|
94
96
|
|
|
95
|
-
@
|
|
97
|
+
@server_port_string: String
|
|
96
98
|
|
|
97
|
-
@
|
|
99
|
+
@write_timeout: Integer
|
|
100
|
+
|
|
101
|
+
@max_body_size: Integer?
|
|
98
102
|
|
|
99
103
|
@body_spool_threshold: Integer?
|
|
100
104
|
|
|
101
|
-
@
|
|
105
|
+
@max_keepalive_requests: Integer
|
|
102
106
|
|
|
103
|
-
@
|
|
107
|
+
@access_log_io: IO?
|
|
104
108
|
|
|
105
|
-
@
|
|
109
|
+
@on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
106
110
|
|
|
107
|
-
@
|
|
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
|