/
response.cr
269 lines (219 loc) · 7.02 KB
/
response.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
require "http/headers"
require "http/status"
require "http/cookie"
class HTTP::Server
# The response to configure and write to in an `HTTP::Server` handler.
#
# The response `status` and `headers` must be configured before writing
# the response body. Once response output is written, changing the `status`
# and `headers` properties has no effect.
#
# The `HTTP::Server::Response` is a write-only `IO`, so all `IO` methods are available
# in it.
#
# A response can be upgraded with the `upgrade` method. Once invoked, headers
# are written and the connection `IO` (a socket) is yielded to the given block.
# This is useful to implement protocol upgrades, such as websockets.
class Response < IO
# The response headers (`HTTP::Headers`). These must be set before writing to the response.
getter headers : HTTP::Headers
# The version of the HTTP::Request that created this response.
getter version : String
# The `IO` to which output is written. This can be changed/wrapped to filter
# the response body (for example to compress the output).
property output : IO
# :nodoc:
setter version : String
# The status code of this response, which must be set before writing the response
# body. If not set, the default value is 200 (OK).
property status : HTTP::Status
# :nodoc:
property upgrade_handler : (IO ->)?
@cookies : HTTP::Cookies?
# :nodoc:
def initialize(@io : IO, @version = "HTTP/1.1")
@headers = Headers.new
@status = :ok
@wrote_headers = false
@output = output = @original_output = Output.new(@io)
output.response = self
end
# :nodoc:
def reset
# This method is called by RequestProcessor to avoid allocating a new instance for each iteration.
@headers.clear
@cookies = nil
@status = :ok
@wrote_headers = false
@output = @original_output
@original_output.reset
end
# Convenience method to set the `Content-Type` header.
def content_type=(content_type : String)
headers["Content-Type"] = content_type
end
# Convenience method to set the `Content-Length` header.
def content_length=(content_length : Int)
headers["Content-Length"] = content_length.to_s
end
# Convenience method to retrieve the HTTP status code.
def status_code
status.code
end
# Convenience method to set the HTTP status code.
def status_code=(status_code : Int32)
self.status = HTTP::Status.new(status_code)
status_code
end
# See `IO#write(slice)`.
def write(slice : Bytes) : Nil
return if slice.empty?
@output.write(slice)
end
# Convenience method to set cookies, see `HTTP::Cookies`.
def cookies
@cookies ||= HTTP::Cookies.new
end
# :nodoc:
def read(slice : Bytes)
raise "Can't read from HTTP::Server::Response"
end
# Upgrades this response, writing headers and yielding the connection `IO` (a socket) to the given block.
# This is useful to implement protocol upgrades, such as websockets.
def upgrade(&block : IO ->)
write_headers
@upgrade_handler = block
end
# Flushes the output. This method must be implemented if wrapping the response output.
def flush
@output.flush
end
# Closes this response, writing headers and body if not done yet.
# This method must be implemented if wrapping the response output.
def close
return if closed?
@output.close
end
# Returns `true` if this response has been closed.
def closed?
@output.closed?
end
@status_message : String?
# Sends *status* and *message* as response.
#
# This method calls `#reset` to remove any previous settings and writes the
# given *status* and *message* to the response IO. Finally, it closes the
# response.
#
# If *message* is `nil`, the default message for *status* is used provided
# by `HTTP::Status#description`.
def respond_with_status(status : HTTP::Status, message : String? = nil)
reset
@status = status
@status_message = message ||= @status.description
self.content_type = "text/plain"
self << @status.code << ' ' << message << '\n'
close
end
# :ditto:
def respond_with_status(status : Int, message : String? = nil)
respond_with_status(HTTP::Status.new(status), message)
end
protected def write_headers
@io << @version << ' ' << @status.code << ' ' << (@status_message || @status.description) << "\r\n"
headers.each do |name, values|
values.each do |value|
@io << name << ": " << value << "\r\n"
end
end
@io << "\r\n"
@wrote_headers = true
end
protected def wrote_headers?
@wrote_headers
end
protected def has_cookies?
!@cookies.nil?
end
# :nodoc:
class Output < IO
include IO::Buffered
property! response : Response
@chunked : Bool
@io : IO
def initialize(@io)
@chunked = false
@closed = false
end
def reset
@in_buffer_rem = Bytes.empty
@out_count = 0
@sync = false
@flush_on_newline = false
@chunked = false
@closed = false
end
private def unbuffered_read(slice : Bytes)
raise "Can't read from HTTP::Server::Response"
end
private def unbuffered_write(slice : Bytes)
return if slice.empty?
unless response.wrote_headers?
if response.version != "HTTP/1.0" && !response.headers.has_key?("Content-Length")
response.headers["Transfer-Encoding"] = "chunked"
@chunked = true
end
end
ensure_headers_written
if @chunked
slice.size.to_s(@io, 16)
@io << "\r\n"
@io.write(slice)
@io << "\r\n"
else
@io.write(slice)
end
rescue ex : IO::Error
unbuffered_close
raise ClientError.new("Error while writing data to the client", ex)
end
def closed? : Bool
@closed
end
def close
return if closed?
if !response.wrote_headers? && !response.headers.has_key?("Content-Length")
response.content_length = @out_count
end
ensure_headers_written
super
if @chunked
@io << "0\r\n\r\n"
@io.flush
end
end
private def ensure_headers_written
unless response.wrote_headers?
if response.has_cookies?
response.cookies.add_response_headers(response.headers)
end
response.write_headers
end
end
private def unbuffered_close
@closed = true
end
private def unbuffered_rewind
raise "Can't rewind to HTTP::Server::Response"
end
private def unbuffered_flush
@io.flush
rescue ex : IO::Error
unbuffered_close
raise ClientError.new("Error while flushing data to the client", ex)
end
end
end
class ClientError < Exception
end
end