raptor 0.21.0 → 0.22.1
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 +12 -0
- data/README.md +37 -71
- data/docs/raptor-vs-puma.md +13 -17
- data/lib/raptor/cli.rb +5 -4
- data/lib/raptor/cluster.rb +5 -4
- data/lib/raptor/control_server.rb +36 -17
- data/lib/raptor/detached_body.rb +336 -0
- data/lib/raptor/http.rb +10 -0
- data/lib/raptor/http1.rb +111 -17
- data/lib/raptor/http2.rb +127 -41
- data/lib/raptor/reactor.rb +966 -34
- data/lib/raptor/thread_locals.rb +4 -2
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/control_server.rbs +17 -5
- data/sig/generated/raptor/detached_body.rbs +171 -0
- data/sig/generated/raptor/http.rbs +8 -0
- data/sig/generated/raptor/http1.rbs +46 -4
- data/sig/generated/raptor/http2.rbs +68 -17
- data/sig/generated/raptor/reactor.rbs +422 -13
- data/sig/generated/raptor/thread_locals.rbs +3 -1
- metadata +3 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "atomic-ruby/atom"
|
|
5
|
+
|
|
6
|
+
module Raptor
|
|
7
|
+
# A bounded response body that may be written after its application thread
|
|
8
|
+
# has returned.
|
|
9
|
+
#
|
|
10
|
+
class DetachedBody
|
|
11
|
+
DEFAULT_MAX_BUFFER_SIZE = 64 * 1024
|
|
12
|
+
|
|
13
|
+
# Tracks buffered bytes shared by a group of detached bodies.
|
|
14
|
+
#
|
|
15
|
+
class Budget
|
|
16
|
+
# @rbs @max_size: Integer
|
|
17
|
+
# @rbs @size: Atom
|
|
18
|
+
|
|
19
|
+
# Creates a shared budget with the given byte limit.
|
|
20
|
+
#
|
|
21
|
+
# @param max_size [Integer] maximum bytes that may be reserved
|
|
22
|
+
# @return [void]
|
|
23
|
+
#
|
|
24
|
+
# @rbs (Integer max_size) -> void
|
|
25
|
+
def initialize(max_size)
|
|
26
|
+
@max_size = max_size
|
|
27
|
+
@size = Atom.new(0)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Reserves bytes when they fit within the shared limit.
|
|
31
|
+
#
|
|
32
|
+
# @param bytes [Integer] number of bytes to reserve
|
|
33
|
+
# @return [Boolean] whether the bytes were reserved
|
|
34
|
+
#
|
|
35
|
+
# @rbs (Integer bytes) -> bool
|
|
36
|
+
def reserve(bytes)
|
|
37
|
+
reserved = false
|
|
38
|
+
@size.swap do |size|
|
|
39
|
+
reserved = size + bytes <= @max_size
|
|
40
|
+
reserved ? size + bytes : size
|
|
41
|
+
end
|
|
42
|
+
reserved
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Releases bytes previously charged to the shared limit.
|
|
46
|
+
#
|
|
47
|
+
# @param bytes [Integer] number of bytes to release
|
|
48
|
+
# @return [void]
|
|
49
|
+
#
|
|
50
|
+
# @rbs (Integer bytes) -> void
|
|
51
|
+
def release(bytes)
|
|
52
|
+
@size.swap { |size| size - bytes }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @rbs @max_buffer_size: Integer
|
|
57
|
+
# @rbs @state: Atom
|
|
58
|
+
# @rbs @budgets: Array[Budget]
|
|
59
|
+
# @rbs @wake: ^() -> void
|
|
60
|
+
# @rbs @dispatch: ^(Proc) -> void
|
|
61
|
+
# @rbs @finished: ^(Symbol) -> void
|
|
62
|
+
# @rbs @on_open: (^(DetachedBody) -> void)?
|
|
63
|
+
# @rbs @on_close: (^(Symbol) -> void)?
|
|
64
|
+
|
|
65
|
+
# Creates a detached body with a bounded byte buffer.
|
|
66
|
+
#
|
|
67
|
+
# @param max_buffer_size [Integer] maximum body bytes waiting to be written
|
|
68
|
+
# @return [void]
|
|
69
|
+
#
|
|
70
|
+
# @rbs (?max_buffer_size: Integer) -> void
|
|
71
|
+
def initialize(max_buffer_size: DEFAULT_MAX_BUFFER_SIZE)
|
|
72
|
+
raise ArgumentError, "max_buffer_size must be positive" unless max_buffer_size.positive?
|
|
73
|
+
|
|
74
|
+
@max_buffer_size = max_buffer_size
|
|
75
|
+
@state = Atom.new({ chunks: [], size: 0, closing: false, closed: false, notified: false, trailers: {} })
|
|
76
|
+
@budgets = []
|
|
77
|
+
@wake = proc {}
|
|
78
|
+
@dispatch = proc { |callback| callback.call }
|
|
79
|
+
@finished = proc {}
|
|
80
|
+
@on_open = nil
|
|
81
|
+
@on_close = nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Registers a callback that runs on the request thread once the server
|
|
85
|
+
# accepts the response stream.
|
|
86
|
+
#
|
|
87
|
+
# @yieldparam stream [DetachedBody] the opened response stream
|
|
88
|
+
# @return [DetachedBody]
|
|
89
|
+
#
|
|
90
|
+
# @rbs () { (DetachedBody stream) -> void } -> DetachedBody
|
|
91
|
+
def on_open(&block)
|
|
92
|
+
@on_open = block
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Registers a callback for when the response stream closes.
|
|
97
|
+
#
|
|
98
|
+
# @yieldparam reason [Symbol] why the stream closed
|
|
99
|
+
# @return [DetachedBody]
|
|
100
|
+
#
|
|
101
|
+
# @rbs () { (Symbol reason) -> void } -> DetachedBody
|
|
102
|
+
def on_close(&block)
|
|
103
|
+
@on_close = block
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Adds body bytes without waiting for socket or flow-control capacity.
|
|
108
|
+
#
|
|
109
|
+
# @param chunk [String] response body bytes
|
|
110
|
+
# @return [Symbol] `:accepted`, `:full`, or `:closed`
|
|
111
|
+
#
|
|
112
|
+
# @rbs (String chunk) -> Symbol
|
|
113
|
+
def try_write(chunk)
|
|
114
|
+
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
115
|
+
|
|
116
|
+
chunk = chunk.dup.freeze
|
|
117
|
+
if chunk.empty?
|
|
118
|
+
state = @state.value
|
|
119
|
+
return state[:closed] || state[:closing] ? :closed : :accepted
|
|
120
|
+
end
|
|
121
|
+
return :full unless reserve(chunk.bytesize)
|
|
122
|
+
|
|
123
|
+
result = :closed
|
|
124
|
+
wake = false
|
|
125
|
+
@state.swap do |state|
|
|
126
|
+
if state[:closed] || state[:closing]
|
|
127
|
+
result = :closed
|
|
128
|
+
wake = false
|
|
129
|
+
state
|
|
130
|
+
elsif state[:size] + chunk.bytesize > @max_buffer_size
|
|
131
|
+
result = :full
|
|
132
|
+
wake = false
|
|
133
|
+
state
|
|
134
|
+
else
|
|
135
|
+
result = :accepted
|
|
136
|
+
wake = !state[:notified]
|
|
137
|
+
state.merge(
|
|
138
|
+
chunks: state[:chunks] + [chunk],
|
|
139
|
+
size: state[:size] + chunk.bytesize,
|
|
140
|
+
notified: true
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
if result == :accepted
|
|
146
|
+
@wake.call if wake
|
|
147
|
+
else
|
|
148
|
+
release(chunk.bytesize)
|
|
149
|
+
end
|
|
150
|
+
result
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Finishes the body after all accepted bytes have been written.
|
|
154
|
+
#
|
|
155
|
+
# @param trailers [Hash] trailing response headers
|
|
156
|
+
# @return [void]
|
|
157
|
+
#
|
|
158
|
+
# @rbs (?trailers: Hash[String, String | Array[String]]) -> void
|
|
159
|
+
def close(trailers: {})
|
|
160
|
+
trailers = trailers.to_h do |name, value|
|
|
161
|
+
value = value.is_a?(Array) ? value.map { _1.dup.freeze }.freeze : value.dup.freeze
|
|
162
|
+
[name.dup.freeze, value]
|
|
163
|
+
end.freeze
|
|
164
|
+
wake = false
|
|
165
|
+
@state.swap do |state|
|
|
166
|
+
if state[:closed] || state[:closing]
|
|
167
|
+
wake = false
|
|
168
|
+
next state
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
wake = !state[:notified]
|
|
172
|
+
state.merge(closing: true, notified: true, trailers: trailers)
|
|
173
|
+
end
|
|
174
|
+
@wake.call if wake
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns whether the stream has closed.
|
|
178
|
+
#
|
|
179
|
+
# @return [Boolean]
|
|
180
|
+
#
|
|
181
|
+
# @rbs () -> bool
|
|
182
|
+
def closed?
|
|
183
|
+
@state.value[:closed]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Connects the body to its reactor-owned response stream.
|
|
187
|
+
#
|
|
188
|
+
# @param budgets [Array<Budget>] buffer budgets shared with other bodies
|
|
189
|
+
# @param wake [Proc] called when buffered data or a close is ready to write
|
|
190
|
+
# @param dispatch [Proc] schedules application callbacks off the reactor thread
|
|
191
|
+
# @param finished [Proc] called with the close reason after `on_close`
|
|
192
|
+
# @return [Boolean] false when the bytes already buffered exceed a budget
|
|
193
|
+
#
|
|
194
|
+
# @rbs (Array[Budget] budgets, ^() -> void wake, ^(Proc) -> void dispatch, ^(Symbol) -> void finished) -> bool
|
|
195
|
+
def attach(budgets, wake, dispatch, finished)
|
|
196
|
+
@wake = wake
|
|
197
|
+
@dispatch = dispatch
|
|
198
|
+
@finished = finished
|
|
199
|
+
size = @state.value[:size]
|
|
200
|
+
reserved = []
|
|
201
|
+
budgets.each do |budget|
|
|
202
|
+
unless budget.reserve(size)
|
|
203
|
+
reserved.each { |held| held.release(size) }
|
|
204
|
+
finish(:full)
|
|
205
|
+
return false
|
|
206
|
+
end
|
|
207
|
+
reserved << budget
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
@budgets = budgets
|
|
211
|
+
true
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Notifies the application that its response stream is ready.
|
|
215
|
+
#
|
|
216
|
+
# @return [void]
|
|
217
|
+
#
|
|
218
|
+
# @rbs () -> void
|
|
219
|
+
def open
|
|
220
|
+
@on_open&.call(self)
|
|
221
|
+
@wake.call if @state.value[:notified]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Returns the size of the next buffered chunk, 0 when closing, or nil
|
|
225
|
+
# while waiting for more data.
|
|
226
|
+
#
|
|
227
|
+
# @return [Integer, nil]
|
|
228
|
+
#
|
|
229
|
+
# @rbs () -> Integer?
|
|
230
|
+
def next_size
|
|
231
|
+
state = @state.value
|
|
232
|
+
state[:chunks].first&.bytesize || (0 if state[:closing])
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Removes up to `max_bytes` from the next buffered chunk.
|
|
236
|
+
#
|
|
237
|
+
# @param max_bytes [Integer] the largest chunk to return
|
|
238
|
+
# @return [String, nil] the removed bytes, or nil when nothing is buffered
|
|
239
|
+
#
|
|
240
|
+
# @rbs (Integer max_bytes) -> String?
|
|
241
|
+
def shift(max_bytes)
|
|
242
|
+
chunk = nil
|
|
243
|
+
@state.swap do |state|
|
|
244
|
+
chunk = nil
|
|
245
|
+
first = state[:chunks].first
|
|
246
|
+
next state unless first
|
|
247
|
+
|
|
248
|
+
size = max_bytes < first.bytesize ? max_bytes : first.bytesize
|
|
249
|
+
chunk = first.byteslice(0, size)
|
|
250
|
+
chunks = if size == first.bytesize
|
|
251
|
+
state[:chunks].drop(1)
|
|
252
|
+
else
|
|
253
|
+
[first.byteslice(size..-1).freeze] + state[:chunks].drop(1)
|
|
254
|
+
end
|
|
255
|
+
state.merge(
|
|
256
|
+
chunks: chunks,
|
|
257
|
+
size: state[:size] - size,
|
|
258
|
+
notified: !chunks.empty? || state[:closing]
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
release(chunk.bytesize) if chunk
|
|
262
|
+
chunk
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Returns the response trailers supplied when the body closed.
|
|
266
|
+
#
|
|
267
|
+
# @return [Hash] trailing response headers
|
|
268
|
+
#
|
|
269
|
+
# @rbs () -> Hash[String, String | Array[String]]
|
|
270
|
+
def trailers
|
|
271
|
+
@state.value[:trailers]
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Closes the stream and invokes its callback exactly once.
|
|
275
|
+
#
|
|
276
|
+
# @param reason [Symbol] why the stream closed
|
|
277
|
+
# @return [void]
|
|
278
|
+
#
|
|
279
|
+
# @rbs (Symbol reason) -> void
|
|
280
|
+
def finish(reason)
|
|
281
|
+
callback = false
|
|
282
|
+
remaining = 0
|
|
283
|
+
@state.swap do |state|
|
|
284
|
+
if state[:closed]
|
|
285
|
+
callback = false
|
|
286
|
+
remaining = 0
|
|
287
|
+
state
|
|
288
|
+
else
|
|
289
|
+
callback = true
|
|
290
|
+
remaining = state[:size]
|
|
291
|
+
state.merge(chunks: [], size: 0, closed: true, notified: false)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
return unless callback
|
|
295
|
+
|
|
296
|
+
release(remaining)
|
|
297
|
+
@dispatch.call(proc do
|
|
298
|
+
begin
|
|
299
|
+
@on_close&.call(reason)
|
|
300
|
+
ensure
|
|
301
|
+
@finished.call(reason)
|
|
302
|
+
end
|
|
303
|
+
end)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
private
|
|
307
|
+
|
|
308
|
+
# Reserves bytes from every budget attached to this body.
|
|
309
|
+
#
|
|
310
|
+
# @param bytes [Integer] number of bytes to reserve
|
|
311
|
+
# @return [Boolean] whether every budget accepted the reservation
|
|
312
|
+
#
|
|
313
|
+
# @rbs (Integer bytes) -> bool
|
|
314
|
+
def reserve(bytes)
|
|
315
|
+
reserved = []
|
|
316
|
+
@budgets.each do |budget|
|
|
317
|
+
unless budget.reserve(bytes)
|
|
318
|
+
reserved.each { |held| held.release(bytes) }
|
|
319
|
+
return false
|
|
320
|
+
end
|
|
321
|
+
reserved << budget
|
|
322
|
+
end
|
|
323
|
+
true
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Releases bytes from every budget attached to this body.
|
|
327
|
+
#
|
|
328
|
+
# @param bytes [Integer] number of bytes to release
|
|
329
|
+
# @return [void]
|
|
330
|
+
#
|
|
331
|
+
# @rbs (Integer bytes) -> void
|
|
332
|
+
def release(bytes)
|
|
333
|
+
@budgets.each { |budget| budget.release(bytes) }
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
data/lib/raptor/http.rb
CHANGED
|
@@ -27,6 +27,16 @@ module Raptor
|
|
|
27
27
|
def message = "could not write response"
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Returns whether an HTTP status forbids an entity body.
|
|
31
|
+
#
|
|
32
|
+
# @param status [Integer] the response status code
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
#
|
|
35
|
+
# @rbs (Integer status) -> bool
|
|
36
|
+
def self.no_entity_body_status?(status)
|
|
37
|
+
(status >= 100 && status < 200) || status == 204 || status == 304
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
# Writes `string` in full, retrying on partial writes. Bounded by
|
|
31
41
|
# `timeout` so a slow client can't pin the writing thread.
|
|
32
42
|
#
|
data/lib/raptor/http1.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "tempfile"
|
|
|
8
8
|
require "atomic-ruby/atomic_boolean"
|
|
9
9
|
require "rack"
|
|
10
10
|
|
|
11
|
+
require_relative "detached_body"
|
|
11
12
|
require_relative "http"
|
|
12
13
|
require_relative "raptor_http"
|
|
13
14
|
require_relative "thread_locals"
|
|
@@ -97,6 +98,34 @@ module Raptor
|
|
|
97
98
|
false
|
|
98
99
|
end
|
|
99
100
|
|
|
101
|
+
# Encodes one HTTP/1.1 response chunk.
|
|
102
|
+
#
|
|
103
|
+
# @param chunk [String] response body bytes
|
|
104
|
+
# @return [String] the encoded chunk
|
|
105
|
+
#
|
|
106
|
+
# @rbs (String chunk) -> String
|
|
107
|
+
def self.encode_chunk(chunk)
|
|
108
|
+
HttpParser.chunked_encode(String.new, chunk)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Encodes the final HTTP/1.1 response chunk and its trailers.
|
|
112
|
+
#
|
|
113
|
+
# @param trailers [Hash] trailing response headers
|
|
114
|
+
# @return [String] the final chunk followed by the trailer section
|
|
115
|
+
#
|
|
116
|
+
# @rbs (Hash[String, String | Array[String]] trailers) -> String
|
|
117
|
+
def self.encode_trailers(trailers)
|
|
118
|
+
normalized = trailers.each_with_object({}) do |(key, value), headers|
|
|
119
|
+
name = key.match?(/[A-Z]/) ? key.downcase : key
|
|
120
|
+
next if name.start_with?(RACK_HEADER_PREFIX) || name == "status"
|
|
121
|
+
|
|
122
|
+
headers[name] = value
|
|
123
|
+
end
|
|
124
|
+
response = +"0\r\n"
|
|
125
|
+
HttpParser.format_headers(response, normalized)
|
|
126
|
+
response << "\r\n"
|
|
127
|
+
end
|
|
128
|
+
|
|
100
129
|
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
101
130
|
# bytes. A complete well-formed request returns with `:complete` set
|
|
102
131
|
# plus populated `:env` and `:body`; malformed or oversized input flips
|
|
@@ -492,7 +521,7 @@ module Raptor
|
|
|
492
521
|
#
|
|
493
522
|
# @rbs (TCPSocket socket, Integer id, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
494
523
|
def process_client(socket, id, env, parse_data, body, reactor, thread_pool, request_count, remote_addr, url_scheme)
|
|
495
|
-
keep_alive = process_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
524
|
+
keep_alive = process_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
496
525
|
eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme) if keep_alive
|
|
497
526
|
end
|
|
498
527
|
|
|
@@ -501,20 +530,22 @@ module Raptor
|
|
|
501
530
|
# for another request.
|
|
502
531
|
#
|
|
503
532
|
# @param socket [TCPSocket] the client socket
|
|
533
|
+
# @param id [Integer] unique client identifier
|
|
504
534
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
505
535
|
# @param parse_data [Hash] metadata from the parsing pass
|
|
506
536
|
# @param body [String, nil] decoded request body
|
|
537
|
+
# @param reactor [Reactor] the reactor managing the client connection
|
|
507
538
|
# @param request_count [Integer] number of requests handled on this connection
|
|
508
539
|
# @param remote_addr [String] client IP address
|
|
509
540
|
# @param url_scheme [String] "http" or "https"
|
|
510
541
|
# @return [Boolean] true if the connection should be kept alive
|
|
511
542
|
#
|
|
512
|
-
# @rbs (TCPSocket socket, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
513
|
-
def process_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
543
|
+
# @rbs (TCPSocket socket, Integer id, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Reactor reactor, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
544
|
+
def process_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
514
545
|
if @clean_fiber_locals
|
|
515
|
-
Fiber.new { perform_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme) }.resume
|
|
546
|
+
Fiber.new { perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme) }.resume
|
|
516
547
|
else
|
|
517
|
-
perform_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
548
|
+
perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
518
549
|
end
|
|
519
550
|
ensure
|
|
520
551
|
ThreadLocals.clear if @clean_thread_locals
|
|
@@ -522,13 +553,25 @@ module Raptor
|
|
|
522
553
|
|
|
523
554
|
# Calls the Rack app and writes its response for one request.
|
|
524
555
|
#
|
|
525
|
-
# @
|
|
526
|
-
|
|
556
|
+
# @param socket [TCPSocket] the client socket
|
|
557
|
+
# @param id [Integer] unique client identifier
|
|
558
|
+
# @param env [Hash] partial env hash from the HTTP parser
|
|
559
|
+
# @param parse_data [Hash] metadata from the parsing pass
|
|
560
|
+
# @param body [String, nil] decoded request body
|
|
561
|
+
# @param reactor [Reactor] the reactor managing the client connection
|
|
562
|
+
# @param request_count [Integer] number of requests handled on this connection
|
|
563
|
+
# @param remote_addr [String] client IP address
|
|
564
|
+
# @param url_scheme [String] "http" or "https"
|
|
565
|
+
# @return [Boolean] true if the connection should be kept alive
|
|
566
|
+
#
|
|
567
|
+
# @rbs (TCPSocket socket, Integer id, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Reactor reactor, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
568
|
+
def perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
527
569
|
rack_env = nil
|
|
528
570
|
status = nil
|
|
529
571
|
headers = nil
|
|
530
572
|
hijacked = false
|
|
531
573
|
keep_alive = false
|
|
574
|
+
detached = false
|
|
532
575
|
response_started = false
|
|
533
576
|
|
|
534
577
|
begin
|
|
@@ -540,16 +583,40 @@ module Raptor
|
|
|
540
583
|
body.close if body.respond_to?(:close)
|
|
541
584
|
else
|
|
542
585
|
hijacked = headers.is_a?(Hash) && !!headers[Rack::RACK_HIJACK]
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
586
|
+
no_body = rack_env[Rack::REQUEST_METHOD] == "HEAD" || Http.no_entity_body_status?(status)
|
|
587
|
+
detached = body.is_a?(DetachedBody) && !no_body
|
|
588
|
+
if detached
|
|
589
|
+
chunked = rack_env[Rack::SERVER_PROTOCOL] == HTTP_11
|
|
590
|
+
keep_alive = chunked && keep_alive?(rack_env, request_count)
|
|
591
|
+
response_started = true
|
|
592
|
+
write_detached_response(socket, status, headers, chunked: chunked, keep_alive: keep_alive)
|
|
593
|
+
finished = proc do |reason|
|
|
594
|
+
error = IOError.new("detached response closed") unless reason == :closed
|
|
595
|
+
write_access_log(rack_env, status, "-", remote_addr) if @access_log_io
|
|
596
|
+
Http.call_response_finished(rack_env, status, headers, error)
|
|
597
|
+
end
|
|
598
|
+
reactor.attach_http1_body(
|
|
599
|
+
socket,
|
|
600
|
+
id,
|
|
601
|
+
body,
|
|
602
|
+
{ chunked: chunked, keep_alive: keep_alive, request_count: request_count, remote_addr: remote_addr, url_scheme: url_scheme },
|
|
603
|
+
finished
|
|
604
|
+
)
|
|
605
|
+
else
|
|
606
|
+
streaming = body.respond_to?(:call) && !body.respond_to?(:each)
|
|
607
|
+
keep_alive = (hijacked || streaming) ? false : keep_alive?(rack_env, request_count)
|
|
608
|
+
response_size = response_size(headers, body) if @access_log_io && !hijacked
|
|
609
|
+
response_started = true
|
|
610
|
+
write_response(socket, rack_env, status, headers, body, keep_alive: keep_alive)
|
|
611
|
+
body.finish(:closed) if body.is_a?(DetachedBody)
|
|
612
|
+
end
|
|
548
613
|
end
|
|
549
614
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
615
|
+
unless detached
|
|
616
|
+
write_access_log(rack_env, status, response_size, remote_addr) if @access_log_io && !hijacked
|
|
617
|
+
Http.call_response_finished(rack_env, status, headers, nil)
|
|
618
|
+
end
|
|
619
|
+
keep_alive && !hijacked && !detached
|
|
553
620
|
rescue => error
|
|
554
621
|
keep_alive = false
|
|
555
622
|
handle_app_error(socket, rack_env, status, headers, error, response_started: response_started, hijacked: hijacked)
|
|
@@ -557,7 +624,7 @@ module Raptor
|
|
|
557
624
|
rack_input = rack_env && rack_env[Rack::RACK_INPUT]
|
|
558
625
|
rack_input.close! rescue nil if rack_input.respond_to?(:close!)
|
|
559
626
|
|
|
560
|
-
unless hijacked || keep_alive
|
|
627
|
+
unless hijacked || keep_alive || detached
|
|
561
628
|
socket.close rescue nil
|
|
562
629
|
end
|
|
563
630
|
end
|
|
@@ -666,9 +733,11 @@ module Raptor
|
|
|
666
733
|
|
|
667
734
|
keep_alive = process_request(
|
|
668
735
|
socket,
|
|
736
|
+
id,
|
|
669
737
|
env,
|
|
670
738
|
parse_data,
|
|
671
739
|
body,
|
|
740
|
+
reactor,
|
|
672
741
|
request_count,
|
|
673
742
|
remote_addr,
|
|
674
743
|
url_scheme
|
|
@@ -895,6 +964,31 @@ module Raptor
|
|
|
895
964
|
socket_write(socket, response)
|
|
896
965
|
end
|
|
897
966
|
|
|
967
|
+
# Starts a detached response, chunked on HTTP/1.1 and ended by closing
|
|
968
|
+
# the connection on HTTP/1.0.
|
|
969
|
+
#
|
|
970
|
+
# @param socket [TCPSocket] the client socket to write to
|
|
971
|
+
# @param status [Integer] HTTP status code
|
|
972
|
+
# @param headers [Hash] response headers from the Rack application
|
|
973
|
+
# @param chunked [Boolean] whether to use chunked transfer encoding
|
|
974
|
+
# @param keep_alive [Boolean] whether to send a keep-alive connection header
|
|
975
|
+
# @return [void]
|
|
976
|
+
#
|
|
977
|
+
# @rbs (TCPSocket socket, Integer status, Hash[String, String | Array[String]] headers, chunked: bool, keep_alive: bool) -> void
|
|
978
|
+
def write_detached_response(socket, status, headers, chunked:, keep_alive:)
|
|
979
|
+
validate_status(status)
|
|
980
|
+
headers = normalize_headers(headers)
|
|
981
|
+
validate_headers(headers, status, false)
|
|
982
|
+
headers.delete(Rack::CONTENT_LENGTH)
|
|
983
|
+
headers[Rack::TRANSFER_ENCODING] = TRANSFER_ENCODING_CHUNKED if chunked
|
|
984
|
+
headers["connection"] = keep_alive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE
|
|
985
|
+
|
|
986
|
+
response = build_status_line(chunked ? HTTP_11 : HTTP_10, status)
|
|
987
|
+
HttpParser.format_headers(response, headers)
|
|
988
|
+
response << "\r\n"
|
|
989
|
+
socket_write(socket, response)
|
|
990
|
+
end
|
|
991
|
+
|
|
898
992
|
# Writes a complete HTTP response to the socket.
|
|
899
993
|
#
|
|
900
994
|
# @param socket [TCPSocket] the client socket to write to
|
|
@@ -910,7 +1004,7 @@ module Raptor
|
|
|
910
1004
|
validate_status(status)
|
|
911
1005
|
response_hijack = headers.is_a?(Hash) ? headers.delete(Rack::RACK_HIJACK) : nil
|
|
912
1006
|
headers = normalize_headers(headers)
|
|
913
|
-
no_entity_body = (status
|
|
1007
|
+
no_entity_body = Http.no_entity_body_status?(status)
|
|
914
1008
|
validate_headers(headers, status, no_entity_body)
|
|
915
1009
|
|
|
916
1010
|
headers["connection"] = keep_alive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE
|