Class: EventMachine::HttpResponse

Inherits:
Object
  • Object
show all
Defined in:
lib/evma_httpserver/response.rb

Overview

This class provides a wide variety of features for generating and dispatching HTTP responses. It allows you to conveniently generate headers and content (including chunks and multiparts), and dispatch responses (including deferred or partially-complete responses).

Although HttpResponse is coded as a class, it’s not complete as it stands. It assumes that it has certain of the behaviors of EventMachine::Connection. You must add these behaviors, either by subclassing HttpResponse, or using the alternate version of this class, DelegatedHttpResponse. See the test cases for current information on which behaviors you have to add.

TODO, someday it would be nice to provide a version of this functionality that is coded as a Module, so it can simply be mixed into an instance of EventMachine::Connection.

Direct Known Subclasses

DelegatedHttpResponse

Constant Summary collapse

STATUS_CODES =
{
  100 => "100 Continue",
  101 => "101 Switching Protocols",
  200 => "200 OK",
  201 => "201 Created",
  202 => "202 Accepted",
  203 => "203 Non-Authoritative Information",
  204 => "204 No Content",
  205 => "205 Reset Content",
  206 => "206 Partial Content",
  300 => "300 Multiple Choices",
  301 => "301 Moved Permanently",
  302 => "302 Found",
  303 => "303 See Other",
  304 => "304 Not Modified",
  305 => "305 Use Proxy",
  307 => "307 Temporary Redirect",
  400 => "400 Bad Request",
  401 => "401 Unauthorized",
  402 => "402 Payment Required",
  403 => "403 Forbidden",
  404 => "404 Not Found",
  405 => "405 Method Not Allowed",
  406 => "406 Not Acceptable",
  407 => "407 Proxy Authentication Required",
  408 => "408 Request Timeout",
  409 => "409 Conflict",
  410 => "410 Gone",
  411 => "411 Length Required",
  412 => "412 Precondition Failed",
  413 => "413 Request Entity Too Large",
  414 => "414 Request-URI Too Long",
  415 => "415 Unsupported Media Type",
  416 => "416 Requested Range Not Satisfiable",
  417 => "417 Expectation Failed",
  500 => "500 Internal Server Error",
  501 => "501 Not Implemented",
  502 => "502 Bad Gateway",
  503 => "503 Service Unavailable",
  504 => "504 Gateway Timeout",
  505 => "505 HTTP Version Not Supported"
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeHttpResponse

Returns a new instance of HttpResponse.



97
98
99
# File 'lib/evma_httpserver/response.rb', line 97

def initialize
  @headers = {}
end

Instance Attribute Details

#chunksObject

Returns the value of attribute chunks.



95
96
97
# File 'lib/evma_httpserver/response.rb', line 95

def chunks
  @chunks
end

#headersObject

Returns the value of attribute headers.



95
96
97
# File 'lib/evma_httpserver/response.rb', line 95

def headers
  @headers
end

#multipartsObject

Returns the value of attribute multiparts.



95
96
97
# File 'lib/evma_httpserver/response.rb', line 95

def multiparts
  @multiparts
end

#statusObject

Returns the value of attribute status.



95
96
97
# File 'lib/evma_httpserver/response.rb', line 95

def status
  @status
end

Class Method Details

.concoct_multipart_boundaryObject

TODO, this is going to be way too slow. Cache up the uuidgens.



320
321
322
323
324
325
326
327
328
329
# File 'lib/evma_httpserver/response.rb', line 320

def self.concoct_multipart_boundary
  @multipart_index ||= 0
  @multipart_index += 1
  if @multipart_index >= 1000
    @multipart_index = 0
    @multipart_guid = nil
  end
  @multipart_guid ||= `uuidgen -r`.chomp.gsub(/\-/,"")
  "#{@multipart_guid}#{@multipart_index}"
end

Instance Method Details

Sugaring for Set-Cookie headers. These are a pain because there can easily and legitimately be more than one. So we use an ugly verb to signify that. #add_set_cookies does NOT disturb the Set-Cookie headers which may have been added on a prior call. #set_cookie clears them out first.



126
127
128
129
130
131
# File 'lib/evma_httpserver/response.rb', line 126

def add_set_cookie *ck
  if ck.length > 0
    h = (@headers["Set-Cookie"] ||= [])
    ck.each {|c| h << c}
  end
end

#chunk(text) ⇒ Object

add a chunk to go to the output. Will cause the headers to pick up “content-transfer-encoding” Add the chunk to a list. Calling #send_chunks will send out the available chunks and clear the chunk list WITHOUT closing the connection, so it can be called any number of times. TODO!!! Per RFC2616, we may not send chunks to an HTTP/1.0 client. Raise an exception here if our user tries to do so. Chunked transfer coding is defined in RFC2616 pgh 3.6.1. The argument can be a string or a hash. The latter allows for sending chunks with extensions (someday).



260
261
262
263
# File 'lib/evma_httpserver/response.rb', line 260

def chunk text
  @chunks ||= []
  @chunks << text
end

#contentObject



102
# File 'lib/evma_httpserver/response.rb', line 102

def content()       @content || ''        end

#content=(value) ⇒ Object



101
# File 'lib/evma_httpserver/response.rb', line 101

def content=(value) @content = value.to_s end

#content?Boolean

Returns:

  • (Boolean)


103
# File 'lib/evma_httpserver/response.rb', line 103

def content?()      !!@content            end

#content_type(*mime) ⇒ Object

sugarings for headers



114
115
116
117
118
119
120
# File 'lib/evma_httpserver/response.rb', line 114

def content_type *mime
  if mime.length > 0
    @headers["Content-Type"] = mime.first.to_s
  else
    @headers["Content-Type"]
  end
end

#fixup_headersObject

Examine the content type and data and other things, and perform a final fixup of the header array. We expect this to be called just before sending headers to the remote peer. In the case of multiparts, we ASSUME we will get called before any content gets sent out, because the multipart boundary is created here.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/evma_httpserver/response.rb', line 194

def fixup_headers
  if @content
    @headers["Content-Length"] = @content.bytesize
  elsif @chunks
    @headers["Transfer-Encoding"] = "chunked"
    # Might be nice to ENSURE there is no content-length header,
    # but how to detect all the possible permutations of upper/lower case?
  elsif @multiparts
    @multipart_boundary = self.class.concoct_multipart_boundary
    @headers["Content-Type"] = "multipart/x-mixed-replace; boundary=\"#{@multipart_boundary}\""
  else
    @headers["Content-Length"] = 0
  end
end

#keep_connection_open(arg = true) ⇒ Object



105
106
107
# File 'lib/evma_httpserver/response.rb', line 105

def keep_connection_open arg=true
  @keep_connection_open = arg
end

#multipart(arg) ⇒ Object

To add a multipart to the outgoing response, specify the headers and the body. If only a string is given, it’s treated as the body (in this case, the header is assumed to be empty).



291
292
293
294
295
296
297
298
299
300
# File 'lib/evma_httpserver/response.rb', line 291

def multipart arg
  vals = if arg.is_a?(String)
    {:body => arg, :headers => {}}
  else
    arg
  end

  @multiparts ||= []
  @multiparts << vals
end

#send_bodyObject

we send either content, chunks, or multiparts. Content can only be sent once. Chunks and multiparts can be sent any number of times. DO NOT close the connection or send any goodbye kisses. This method can be called multiple times to send out chunks or multiparts.



213
214
215
216
217
218
219
220
221
# File 'lib/evma_httpserver/response.rb', line 213

def send_body
  if @chunks
    send_chunks
  elsif @multiparts
    send_multiparts
  else
    send_content
  end
end

#send_chunksObject

send the contents of the chunk list and clear it out. ASSUMES that headers have been sent. Does NOT close the connection. Can be called multiple times. According to RFC2616, phg 3.6.1, the last chunk will be zero length. But some caller could accidentally set a zero-length chunk in the middle of the stream. If that should happen, raise an exception. The reason for supporting chunks that are hashes instead of just strings is to enable someday supporting chunk-extension codes (cf the RFC). TODO!!! We’re not supporting the final entity-header that may be transmitted after the last (zero-length) chunk.



277
278
279
280
281
282
283
284
285
# File 'lib/evma_httpserver/response.rb', line 277

def send_chunks
  send_headers unless @sent_headers
  while chunk = @chunks.shift
    raise "last chunk already sent" if @last_chunk_sent
    text = chunk.is_a?(Hash) ? chunk[:text] : chunk.to_s
    send_data "#{format("%x", text.length).upcase}\r\n#{text}\r\n"
    @last_chunk_sent = true if text.length == 0
  end
end

#send_contentObject



243
244
245
246
247
# File 'lib/evma_httpserver/response.rb', line 243

def send_content
  raise "sent content already" if @sent_content
  @sent_content = true
  send_data(content)
end

#send_headersObject

Send the headers out in alpha-sorted order. This will degrade performance to some degree, and is intended only to simplify the construction of unit tests.



158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/evma_httpserver/response.rb', line 158

def send_headers
  raise "sent headers already" if @sent_headers
  @sent_headers = true

  fixup_headers

  ary = []
  ary << "HTTP/1.1 #{@status || '200 OK'}\r\n"
  ary += generate_header_lines(@headers)
  ary << "\r\n"

  send_data ary.join
end

#send_multipartsObject

Multipart syntax is defined in RFC 2046, pgh 5.1.1 et seq. The CRLF which introduces the boundary line of each part (content entity) is defined as being part of the boundary, not of the preceding part. So we don’t need to mess with interpreting the last bytes of a part to ensure they are CRLF-terminated.



308
309
310
311
312
313
314
315
316
# File 'lib/evma_httpserver/response.rb', line 308

def send_multiparts
  send_headers unless @sent_headers
  while part = @multiparts.shift
    send_data "\r\n--#{@multipart_boundary}\r\n"
    send_data( generate_header_lines( part[:headers] || {} ).join)
    send_data "\r\n"
    send_data part[:body].to_s
  end
end

#send_redirect(location) ⇒ Object



331
332
333
334
335
# File 'lib/evma_httpserver/response.rb', line 331

def send_redirect location
  @status = 302 # TODO, make 301 available by parameter
  @headers["Location"] = location
  send_response
end

#send_responseObject

This is intended to send a complete HTTP response, including closing the connection if appropriate at the end of the transmission. Don’t use this method to send partial or iterated responses. This method will send chunks and multiparts provided they are all available when we get here. Note that the default @status is 200 if the value doesn’t exist.



148
149
150
151
152
153
# File 'lib/evma_httpserver/response.rb', line 148

def send_response
  send_headers
  send_body
  send_trailer
  close_connection_after_writing unless (@keep_connection_open and (@status || "200 OK") == "200 OK")
end

#send_trailerObject

send a trailer which depends on the type of body we’re dealing with. The assumption is that we’re about to end the transmission of this particular HTTP response. (A connection-close may or may not follow.)



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/evma_httpserver/response.rb', line 227

def send_trailer
  send_headers unless @sent_headers
  if @chunks
    unless @last_chunk_sent
      chunk ""
      send_chunks
    end
  elsif @multiparts
    # in the lingo of RFC 2046/5.1.1, we're sending an "epilog"
    # consisting of a blank line. I really don't know how that is
    # supposed to interact with the case where we leave the connection
    # open after transmitting the multipart response.
    send_data "\r\n--#{@multipart_boundary}--\r\n\r\n"
  end
end


132
133
134
135
136
137
138
139
140
# File 'lib/evma_httpserver/response.rb', line 132

def set_cookie *ck
  h = (@headers["Set-Cookie"] ||= [])
  if ck.length > 0
    h.clear
    add_set_cookie *ck
  else
    h
  end
end