polyphony 0.17 → 0.19
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 +15 -0
- data/Gemfile.lock +11 -3
- data/README.md +18 -18
- data/TODO.md +5 -21
- data/examples/core/channel_echo.rb +3 -3
- data/examples/core/enumerator.rb +1 -1
- data/examples/core/fork.rb +1 -1
- data/examples/core/genserver.rb +1 -1
- data/examples/core/lock.rb +3 -3
- data/examples/core/multiple_spawn.rb +2 -2
- data/examples/core/nested_async.rb +1 -1
- data/examples/core/nested_multiple_spawn.rb +3 -3
- data/examples/core/resource_cancel.rb +1 -1
- data/examples/core/sleep_spawn.rb +2 -2
- data/examples/core/spawn.rb +1 -1
- data/examples/core/spawn_cancel.rb +1 -1
- data/examples/core/spawn_error.rb +4 -4
- data/examples/core/supervisor.rb +1 -1
- data/examples/core/supervisor_with_error.rb +1 -1
- data/examples/core/supervisor_with_manual_move_on.rb +1 -1
- data/examples/core/thread.rb +2 -2
- data/examples/core/thread_cancel.rb +2 -2
- data/examples/core/thread_pool.rb +1 -1
- data/examples/core/throttle.rb +3 -3
- data/examples/core/timeout.rb +10 -0
- data/examples/fs/read.rb +1 -1
- data/examples/http/http_client.rb +1 -1
- data/examples/http/http_get.rb +7 -0
- data/examples/http/http_parse_experiment.rb +118 -0
- data/examples/http/http_proxy.rb +81 -0
- data/examples/http/http_server.rb +15 -4
- data/examples/http/http_server_forked.rb +2 -2
- data/examples/http/http_server_throttled.rb +1 -1
- data/examples/http/http_ws_server.rb +2 -2
- data/examples/http/https_server.rb +5 -1
- data/examples/http/https_wss_server.rb +1 -1
- data/examples/http/rack_server_https_forked.rb +1 -1
- data/examples/interfaces/pg_client.rb +1 -1
- data/examples/interfaces/pg_pool.rb +1 -1
- data/examples/interfaces/redis_channels.rb +5 -5
- data/examples/interfaces/redis_pubsub.rb +2 -2
- data/examples/interfaces/redis_pubsub_perf.rb +3 -3
- data/examples/io/echo_client.rb +2 -2
- data/examples/io/echo_pipe.rb +17 -0
- data/examples/io/echo_server.rb +1 -1
- data/examples/io/echo_server_with_timeout.rb +1 -1
- data/examples/io/httparty.rb +10 -0
- data/examples/io/httparty_multi.rb +29 -0
- data/examples/io/httparty_threaded.rb +25 -0
- data/examples/io/irb.rb +15 -0
- data/examples/io/net-http.rb +15 -0
- data/examples/io/system.rb +1 -1
- data/examples/io/tcpsocket.rb +18 -0
- data/examples/performance/perf_multi_snooze.rb +2 -2
- data/examples/performance/perf_snooze.rb +17 -20
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +1 -1
- data/ext/ev/ev.h +9 -1
- data/ext/ev/ev_ext.c +4 -1
- data/ext/ev/ev_module.c +36 -22
- data/ext/ev/extconf.rb +1 -1
- data/ext/ev/io.c +23 -23
- data/ext/ev/signal.c +1 -1
- data/ext/ev/socket.c +161 -0
- data/lib/polyphony/core/coprocess.rb +1 -1
- data/lib/polyphony/core/fiber_pool.rb +2 -2
- data/lib/polyphony/core/supervisor.rb +2 -18
- data/lib/polyphony/extensions/io.rb +19 -6
- data/lib/polyphony/extensions/kernel.rb +17 -5
- data/lib/polyphony/extensions/socket.rb +40 -1
- data/lib/polyphony/http/agent.rb +56 -25
- data/lib/polyphony/http/http1_adapter.rb +254 -0
- data/lib/polyphony/http/http2_adapter.rb +157 -0
- data/lib/polyphony/http/{http2_request.rb → request.rb} +25 -22
- data/lib/polyphony/http/server.rb +19 -11
- data/lib/polyphony/net.rb +10 -6
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +6 -5
- data/test/test_coprocess.rb +9 -9
- data/test/test_core.rb +14 -14
- data/test/test_io.rb +4 -4
- data/test/test_kernel.rb +1 -1
- metadata +48 -23
- data/lib/polyphony/http/http1.rb +0 -124
- data/lib/polyphony/http/http1_request.rb +0 -83
- data/lib/polyphony/http/http2.rb +0 -65
@@ -34,6 +34,19 @@ class ::Socket
|
|
34
34
|
@write_watcher&.stop
|
35
35
|
end
|
36
36
|
|
37
|
+
def recv(maxlen, flags = 0, outbuf = nil)
|
38
|
+
outbuf ||= +''
|
39
|
+
loop do
|
40
|
+
result = recv_nonblock(maxlen, flags, outbuf, NO_EXCEPTION)
|
41
|
+
case result
|
42
|
+
when :wait_readable
|
43
|
+
read_watcher.await
|
44
|
+
else
|
45
|
+
return result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
37
50
|
def recvfrom(maxlen, flags = 0)
|
38
51
|
@read_buffer ||= +''
|
39
52
|
loop do
|
@@ -70,6 +83,32 @@ class ::Socket
|
|
70
83
|
end
|
71
84
|
end
|
72
85
|
|
86
|
+
class ::TCPSocket
|
87
|
+
NO_EXCEPTION = { exception: false }.freeze
|
88
|
+
|
89
|
+
def foo; :bar; end
|
90
|
+
|
91
|
+
def initialize(remote_host, remote_port, local_host=nil, local_port=nil)
|
92
|
+
@io = Socket.new Socket::AF_INET, Socket::SOCK_STREAM
|
93
|
+
if local_host && local_port
|
94
|
+
@io.bind(Addrinfo.tcp(local_host, local_port))
|
95
|
+
end
|
96
|
+
@io.connect(Addrinfo.tcp(remote_host, remote_port))
|
97
|
+
end
|
98
|
+
|
99
|
+
def close
|
100
|
+
@io.close
|
101
|
+
end
|
102
|
+
|
103
|
+
def setsockopt(*args)
|
104
|
+
@io.setsockopt(*args)
|
105
|
+
end
|
106
|
+
|
107
|
+
def closed?
|
108
|
+
@io.closed?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
73
112
|
class ::TCPServer
|
74
113
|
NO_EXCEPTION = { exception: false }.freeze
|
75
114
|
|
@@ -86,4 +125,4 @@ class ::TCPServer
|
|
86
125
|
ensure
|
87
126
|
@read_watcher&.stop
|
88
127
|
end
|
89
|
-
end
|
128
|
+
end
|
data/lib/polyphony/http/agent.rb
CHANGED
@@ -21,12 +21,12 @@ end
|
|
21
21
|
|
22
22
|
# Implements an HTTP agent
|
23
23
|
class Agent
|
24
|
-
def self.get(
|
25
|
-
default.get(
|
24
|
+
def self.get(*args)
|
25
|
+
default.get(*args)
|
26
26
|
end
|
27
27
|
|
28
|
-
def self.post(
|
29
|
-
default.post(
|
28
|
+
def self.post(*args)
|
29
|
+
default.post(*args)
|
30
30
|
end
|
31
31
|
|
32
32
|
def self.default
|
@@ -39,15 +39,16 @@ class Agent
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
|
43
|
-
|
42
|
+
OPTS_DEFAULT = {}.freeze
|
43
|
+
|
44
|
+
def get(url, opts = OPTS_DEFAULT)
|
45
|
+
request(url, opts.merge(method: :GET))
|
44
46
|
end
|
45
47
|
|
46
|
-
def post(url,
|
47
|
-
request(url, method: :POST
|
48
|
+
def post(url, opts = OPTS_DEFAULT)
|
49
|
+
request(url, opts.merge(method: :POST))
|
48
50
|
end
|
49
51
|
|
50
|
-
OPTS_DEFAULT = {}.freeze
|
51
52
|
|
52
53
|
def request(url, opts = OPTS_DEFAULT)
|
53
54
|
ctx = request_ctx(url, opts)
|
@@ -55,7 +56,7 @@ class Agent
|
|
55
56
|
response = do_request(ctx)
|
56
57
|
case response[:status_code]
|
57
58
|
when 301, 302
|
58
|
-
|
59
|
+
redirect(response[:headers]['Location'], ctx, opts)
|
59
60
|
when 200, 204
|
60
61
|
response.extend(ResponseMixin)
|
61
62
|
else
|
@@ -63,11 +64,31 @@ class Agent
|
|
63
64
|
end
|
64
65
|
end
|
65
66
|
|
67
|
+
def redirect(url, ctx, opts)
|
68
|
+
url = case url
|
69
|
+
when /^http(?:s)?\:\/\//
|
70
|
+
url
|
71
|
+
when /^\/\/(.+)$/
|
72
|
+
ctx[:uri].scheme + url
|
73
|
+
when /^\//
|
74
|
+
"%s://%s%s" % [
|
75
|
+
ctx[:uri].scheme,
|
76
|
+
ctx[:uri].host,
|
77
|
+
url
|
78
|
+
]
|
79
|
+
else
|
80
|
+
ctx[:uri] + url
|
81
|
+
end
|
82
|
+
|
83
|
+
request(url, opts)
|
84
|
+
end
|
85
|
+
|
66
86
|
def request_ctx(url, opts)
|
67
87
|
{
|
68
88
|
method: opts[:method] || :GET,
|
69
89
|
uri: url_to_uri(url, opts),
|
70
|
-
opts: opts
|
90
|
+
opts: opts,
|
91
|
+
retry: 0,
|
71
92
|
}
|
72
93
|
end
|
73
94
|
|
@@ -87,13 +108,20 @@ class Agent
|
|
87
108
|
def do_request(ctx)
|
88
109
|
key = uri_key(ctx[:uri])
|
89
110
|
@pools[key].acquire do |state|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
111
|
+
cancel_after(10) do
|
112
|
+
state[:socket] ||= connect(key)
|
113
|
+
state[:protocol_method] ||= protocol_method(state[:socket], ctx)
|
114
|
+
send(state[:protocol_method], state, ctx)
|
115
|
+
rescue => e
|
116
|
+
state[:socket]&.close rescue nil
|
117
|
+
state.clear
|
118
|
+
if ctx[:retry] < 3
|
119
|
+
ctx[:retry] += 1
|
120
|
+
do_request(ctx)
|
121
|
+
else
|
122
|
+
raise e
|
123
|
+
end
|
124
|
+
end
|
97
125
|
end
|
98
126
|
end
|
99
127
|
|
@@ -136,12 +164,13 @@ class Agent
|
|
136
164
|
stream = state[:http2_client].new_stream # allocate new stream
|
137
165
|
|
138
166
|
headers = {
|
139
|
-
':scheme' => ctx[:uri].scheme,
|
140
167
|
':method' => ctx[:method].to_s,
|
141
|
-
':
|
168
|
+
':scheme' => ctx[:uri].scheme,
|
142
169
|
':authority' => [ctx[:uri].host, ctx[:uri].port].join(':'),
|
170
|
+
':path' => ctx[:uri].request_uri,
|
143
171
|
}
|
144
172
|
headers.merge!(ctx[:opts][:headers]) if ctx[:opts][:headers]
|
173
|
+
puts "* proxy request headers: #{headers.inspect}"
|
145
174
|
|
146
175
|
if ctx[:opts][:payload]
|
147
176
|
stream.headers(headers, end_stream: false)
|
@@ -173,13 +202,17 @@ class Agent
|
|
173
202
|
(stream.close rescue nil) unless done
|
174
203
|
end
|
175
204
|
|
176
|
-
HTTP1_REQUEST = "%<method>s %<request>s HTTP/1.1\r\nHost: %<host>s\r\n\r\n"
|
205
|
+
HTTP1_REQUEST = "%<method>s %<request>s HTTP/1.1\r\nHost: %<host>s\r\n%<headers>s\r\n"
|
177
206
|
|
178
207
|
def format_http1_request(ctx)
|
208
|
+
headers = ctx[:opts][:headers] ? ctx[:opts][:headers].map { |k, v| "#{k}: #{v}\r\n"}.join : nil
|
209
|
+
puts "* proxy request headers: #{headers.inspect}"
|
210
|
+
|
179
211
|
HTTP1_REQUEST % {
|
180
212
|
method: ctx[:method],
|
181
213
|
request: ctx[:uri].request_uri,
|
182
|
-
host: ctx[:uri].host
|
214
|
+
host: ctx[:uri].host,
|
215
|
+
headers: headers
|
183
216
|
}
|
184
217
|
end
|
185
218
|
|
@@ -198,9 +231,7 @@ class Agent
|
|
198
231
|
when 'http'
|
199
232
|
Polyphony::Net.tcp_connect(key[:host], key[:port])
|
200
233
|
when 'https'
|
201
|
-
Polyphony::Net.tcp_connect(key[:host], key[:port], SECURE_OPTS)
|
202
|
-
socket.post_connection_check(key[:host])
|
203
|
-
end
|
234
|
+
Polyphony::Net.tcp_connect(key[:host], key[:port], SECURE_OPTS)
|
204
235
|
else
|
205
236
|
raise "Invalid scheme #{key[:scheme].inspect}"
|
206
237
|
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :HTTP1Adapter
|
4
|
+
|
5
|
+
require 'http/parser'
|
6
|
+
|
7
|
+
Request = import('./request')
|
8
|
+
HTTP2 = import('./http2_adapter')
|
9
|
+
|
10
|
+
# HTTP1 protocol implementation
|
11
|
+
class HTTP1Adapter
|
12
|
+
# Initializes a protocol adapter instance
|
13
|
+
def initialize(conn, opts)
|
14
|
+
@conn = conn
|
15
|
+
@opts = opts
|
16
|
+
@parser = HTTP::Parser.new(self)
|
17
|
+
@parse_fiber = Fiber.new { parse_loop }
|
18
|
+
end
|
19
|
+
|
20
|
+
def protocol
|
21
|
+
'http/1.1'
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parses incoming data, potentially firing parser callbacks. This loop runs on
|
25
|
+
# a separate fiber and is resumed only when the handler (client) loop asks for
|
26
|
+
# headers, or the request body, or waits for the request to be completed. The
|
27
|
+
# control flow is as follows (arrows represent control transfer between
|
28
|
+
# fibers):
|
29
|
+
#
|
30
|
+
# handler parse_loop
|
31
|
+
# get_headers --> ...
|
32
|
+
# @parser << @conn.readpartial(8192)
|
33
|
+
# ... <-- on_headers
|
34
|
+
#
|
35
|
+
# get_body --> ...
|
36
|
+
# ... <-- on_body
|
37
|
+
#
|
38
|
+
# consume_request --> ...
|
39
|
+
# @parser << @conn.readpartial(8192)
|
40
|
+
# ... <-- on_message_complete
|
41
|
+
#
|
42
|
+
def parse_loop
|
43
|
+
while (data = @conn.readpartial(8192))
|
44
|
+
break unless data
|
45
|
+
@parser << data
|
46
|
+
snooze
|
47
|
+
end
|
48
|
+
@calling_fiber.transfer nil
|
49
|
+
rescue SystemCallError, IOError => error
|
50
|
+
# ignore IO/system call errors
|
51
|
+
@calling_fiber.transfer nil
|
52
|
+
rescue Exception => error
|
53
|
+
# an error return value will be raised by the receiving fiber
|
54
|
+
@calling_fiber.transfer error
|
55
|
+
end
|
56
|
+
|
57
|
+
# request API
|
58
|
+
|
59
|
+
# Iterates over incoming requests. Requests are yielded once all headers have
|
60
|
+
# been received. It is left to the application to read the request body or
|
61
|
+
# diesregard it.
|
62
|
+
def each(&block)
|
63
|
+
can_upgrade = true
|
64
|
+
while @parse_fiber.alive? && (headers = get_headers)
|
65
|
+
if can_upgrade
|
66
|
+
# The connection can be upgraded only on the first request
|
67
|
+
return if upgrade_connection(headers, &block)
|
68
|
+
can_upgrade = false
|
69
|
+
end
|
70
|
+
|
71
|
+
@headers_sent = nil
|
72
|
+
block.(Request.new(headers, self))
|
73
|
+
|
74
|
+
if @parser.keep_alive?
|
75
|
+
@parsing = false
|
76
|
+
else
|
77
|
+
break
|
78
|
+
end
|
79
|
+
end
|
80
|
+
ensure
|
81
|
+
@conn.close rescue nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# Reads headers for the next request. Transfers control to the parse loop,
|
85
|
+
# and resumes once the parse_loop has fired the on_headers callback
|
86
|
+
def get_headers
|
87
|
+
@parsing = true
|
88
|
+
@calling_fiber = Fiber.current
|
89
|
+
@parse_fiber.safe_transfer
|
90
|
+
end
|
91
|
+
|
92
|
+
# Reads a body chunk for the current request. Transfers control to the parse
|
93
|
+
# loop, and resumes once the parse_loop has fired the on_body callback
|
94
|
+
def get_body_chunk
|
95
|
+
@calling_fiber = Fiber.current
|
96
|
+
@read_body = true
|
97
|
+
@parse_fiber.safe_transfer
|
98
|
+
end
|
99
|
+
|
100
|
+
# Waits for the current request to complete. Transfers control to the parse
|
101
|
+
# loop, and resumes once the parse_loop has fired the on_message_complete
|
102
|
+
# callback
|
103
|
+
def consume_request
|
104
|
+
return unless @parsing
|
105
|
+
|
106
|
+
@calling_fiber = Fiber.current
|
107
|
+
@read_body = false
|
108
|
+
@parse_fiber.safe_transfer while @parsing
|
109
|
+
end
|
110
|
+
|
111
|
+
# Upgrades the connection to a different protocol, if the 'Upgrade' header is
|
112
|
+
# given. By default the only supported upgrade protocol is HTTP2. Additional
|
113
|
+
# protocols, notably WebSocket, can be specified by passing a hash to the
|
114
|
+
# :upgrade option when starting a server:
|
115
|
+
#
|
116
|
+
# opts = {
|
117
|
+
# upgrade: {
|
118
|
+
# websocket: Polyphony::Websocket.handler(&method(:ws_handler))
|
119
|
+
# }
|
120
|
+
# }
|
121
|
+
# Polyphony::HTTP::Server.serve('0.0.0.0', 1234, opts) { |req| ... }
|
122
|
+
#
|
123
|
+
# @param headers [Hash] request headers
|
124
|
+
# @return [boolean] truthy if the connection has been upgraded
|
125
|
+
def upgrade_connection(headers, &block)
|
126
|
+
upgrade_protocol = headers['Upgrade']
|
127
|
+
return nil unless upgrade_protocol
|
128
|
+
|
129
|
+
if @opts[:upgrade] && @opts[:upgrade][upgrade_protocol.to_sym]
|
130
|
+
@opts[:upgrade][upgrade_protocol.to_sym].(@conn, headers)
|
131
|
+
return true
|
132
|
+
end
|
133
|
+
|
134
|
+
return nil unless upgrade_protocol == 'h2c'
|
135
|
+
|
136
|
+
# upgrade to HTTP/2
|
137
|
+
HTTP2.upgrade_each(@conn, @opts, http2_upgraded_headers(headers), &block)
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns headers for HTTP2 upgrade
|
142
|
+
# @param headers [Hash] request headers
|
143
|
+
# @return [Hash] headers for HTTP2 upgrade
|
144
|
+
def http2_upgraded_headers(headers)
|
145
|
+
headers.merge(
|
146
|
+
':scheme' => 'http',
|
147
|
+
':authority' => headers['Host'],
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
# HTTP parser callbacks, called in the context of @parse_fiber
|
152
|
+
|
153
|
+
# Resumes client fiber on receipt of all headers
|
154
|
+
# @param headers [Hash] request headers
|
155
|
+
# @return [void]
|
156
|
+
def on_headers_complete(headers)
|
157
|
+
headers[':path'] = @parser.request_url
|
158
|
+
headers[':method'] = @parser.http_method
|
159
|
+
@calling_fiber.transfer(headers)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Resumes client fiber on receipt of body chunk
|
163
|
+
# @param chunk [String] body chunk
|
164
|
+
# @return [void]
|
165
|
+
def on_body(chunk)
|
166
|
+
@calling_fiber.transfer(chunk) if @read_body
|
167
|
+
end
|
168
|
+
|
169
|
+
# Resumes client fiber on request completion
|
170
|
+
# @return [void]
|
171
|
+
def on_message_complete
|
172
|
+
@parsing = false
|
173
|
+
@calling_fiber.transfer nil
|
174
|
+
end
|
175
|
+
|
176
|
+
# response API
|
177
|
+
|
178
|
+
# Sends response including headers and body. Waits for the request to complete
|
179
|
+
# if not yet completed. The body is sent using chunked transfer encoding.
|
180
|
+
# @param chunk [String] response body
|
181
|
+
# @param headers
|
182
|
+
def respond(chunk, headers)
|
183
|
+
consume_request if @parsing
|
184
|
+
data = format_headers(headers, empty_response: !chunk)
|
185
|
+
if chunk
|
186
|
+
data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n0\r\n\r\n"
|
187
|
+
end
|
188
|
+
@conn << data
|
189
|
+
@headers_sent = true
|
190
|
+
end
|
191
|
+
|
192
|
+
DEFAULT_HEADERS_OPTS = {
|
193
|
+
empty_response: false,
|
194
|
+
consume_request: true
|
195
|
+
}
|
196
|
+
|
197
|
+
# Sends response headers. Waits for the request to complete if not yet
|
198
|
+
# completed. If empty_response is true(thy), the response status code will
|
199
|
+
# default to 204, otherwise to 200.
|
200
|
+
# @param headers [Hash] response headers
|
201
|
+
# @param empty_response [boolean] whether a response body will be sent
|
202
|
+
# @return [void]
|
203
|
+
def send_headers(headers, opts = DEFAULT_HEADERS_OPTS)
|
204
|
+
return if @headers_sent
|
205
|
+
|
206
|
+
consume_request if @parsing && opts[:consume_request]
|
207
|
+
@conn << format_headers(headers, opts[:empty_response])
|
208
|
+
@headers_sent = true
|
209
|
+
end
|
210
|
+
|
211
|
+
# Sends a response body chunk. If no headers were sent, default headers are
|
212
|
+
# sent using #send_headers. if the done option is true(thy), an empty chunk
|
213
|
+
# will be sent to signal response completion to the client.
|
214
|
+
# @param chunk [String] response body chunk
|
215
|
+
# @param done [boolean] whether the response is completed
|
216
|
+
# @return [void]
|
217
|
+
def send_body_chunk(chunk, done: false)
|
218
|
+
send_headers({}) unless @headers_sent
|
219
|
+
|
220
|
+
data = +"#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
221
|
+
data << "0\r\n\r\n" if done
|
222
|
+
@conn << data
|
223
|
+
end
|
224
|
+
|
225
|
+
# Finishes the response to the current request. If no headers were sent,
|
226
|
+
# default headers are sent using #send_headers.
|
227
|
+
# @return [void]
|
228
|
+
def finish
|
229
|
+
send_headers({}, true) unless @headers_sent
|
230
|
+
|
231
|
+
@conn << "0\r\n\r\n" if @body_sent
|
232
|
+
end
|
233
|
+
|
234
|
+
private
|
235
|
+
|
236
|
+
# Formats response headers. If empty_response is true(thy), the response
|
237
|
+
# status code will default to 204, otherwise to 200.
|
238
|
+
# @param headers [Hash] response headers
|
239
|
+
# @param empty_response [boolean] whether a response body will be sent
|
240
|
+
# @return [String] formatted response headers
|
241
|
+
def format_headers(headers, empty_response)
|
242
|
+
status = headers[':status'] || (empty_response ? 204 : 200)
|
243
|
+
data = empty_response ?
|
244
|
+
+"HTTP/1.1 #{status}\r\n" :
|
245
|
+
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
246
|
+
|
247
|
+
headers.each do |k, v|
|
248
|
+
next if k =~ /^:/
|
249
|
+
v.is_a?(Array) ?
|
250
|
+
v.each { |o| data << "#{k}: #{o}\r\n" } : data << "#{k}: #{v}\r\n"
|
251
|
+
end
|
252
|
+
data << "\r\n"
|
253
|
+
end
|
254
|
+
end
|