polyphony 0.17 → 0.19
Sign up to get free protection for your applications and to get access to all the features.
- 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
|