-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
response.cr
244 lines (200 loc) · 6.27 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
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.
# The block must invoke `close` afterwards, the server won't do it in this case.
# 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:
def initialize(@io : IO, @version = "HTTP/1.1")
@headers = Headers.new
@status = :ok
@wrote_headers = false
@upgraded = 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
@upgraded = 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)
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 yieling the connection `IO` (a socket) to the given block.
# The block must invoke `close` afterwards, the server won't do it in this case.
# This is useful to implement protocol upgrades, such as websockets.
def upgrade
@upgraded = true
write_headers
flush
yield @io
end
# :nodoc:
def upgraded?
@upgraded
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
# Generates an error response using *message* and *code*.
#
# Calls `reset` and then writes the given message.
def respond_with_error(message = "Internal Server Error", code = 500)
reset
@status = HTTP::Status.new(code)
message ||= @status.description
self.content_type = "text/plain"
self << @status.code << ' ' << message << '\n'
flush
end
protected def write_headers
@io << @version << ' ' << @status.code << ' ' << @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
end
def reset
@in_buffer_rem = Bytes.empty
@out_count = 0
@sync = 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(16, @io)
@io << "\r\n"
@io.write(slice)
@io << "\r\n"
else
@io.write(slice)
end
end
def closed?
@closed
end
def close
return if closed?
unless response.wrote_headers?
response.content_length = @out_count
end
ensure_headers_written
super
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
@io << "0\r\n\r\n" if @chunked
@closed = true
end
private def unbuffered_rewind
raise "Can't rewind to HTTP::Server::Response"
end
private def unbuffered_flush
@io.flush
end
end
end
end