Class: Iodine::Http::Response

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

Overview

this class handles Http responses.

The response can be sent in stages but should complete within the scope of the connecton’s message. Please notice that headers and status cannot be changed once the response started sending data.

Constant Summary collapse

/[\x00-\x20\(\)\<\>@,;:\\\"\/\[\]\?\=\{\}\s]/
STATUS_CODES =

response status codes, as defined.

{100=>"Continue".freeze,
	101=>"Switching Protocols".freeze,
	102=>"Processing".freeze,
	200=>"OK".freeze,
	201=>"Created".freeze,
	202=>"Accepted".freeze,
	203=>"Non-Authoritative Information".freeze,
	204=>"No Content".freeze,
	205=>"Reset Content".freeze,
	206=>"Partial Content".freeze,
	207=>"Multi-Status".freeze,
	208=>"Already Reported".freeze,
	226=>"IM Used".freeze,
	300=>"Multiple Choices".freeze,
	301=>"Moved Permanently".freeze,
	302=>"Found".freeze,
	303=>"See Other".freeze,
	304=>"Not Modified".freeze,
	305=>"Use Proxy".freeze,
	306=>"(Unused)".freeze,
	307=>"Temporary Redirect".freeze,
	308=>"Permanent Redirect".freeze,
	400=>"Bad Request".freeze,
	401=>"Unauthorized".freeze,
	402=>"Payment Required".freeze,
	403=>"Forbidden".freeze,
	404=>"Not Found".freeze,
	405=>"Method Not Allowed".freeze,
	406=>"Not Acceptable".freeze,
	407=>"Proxy Authentication Required".freeze,
	408=>"Request Timeout".freeze,
	409=>"Conflict".freeze,
	410=>"Gone".freeze,
	411=>"Length Required".freeze,
	412=>"Precondition Failed".freeze,
	413=>"Payload Too Large".freeze,
	414=>"URI Too Long".freeze,
	415=>"Unsupported Media Type".freeze,
	416=>"Range Not Satisfiable".freeze,
	417=>"Expectation Failed".freeze,
	422=>"Unprocessable Entity".freeze,
	423=>"Locked".freeze,
	424=>"Failed Dependency".freeze,
	426=>"Upgrade Required".freeze,
	428=>"Precondition Required".freeze,
	429=>"Too Many Requests".freeze,
	431=>"Request Header Fields Too Large".freeze,
	500=>"Internal Server Error".freeze,
	501=>"Not Implemented".freeze,
	502=>"Bad Gateway".freeze,
	503=>"Service Unavailable".freeze,
	504=>"Gateway Timeout".freeze,
	505=>"HTTP Version Not Supported".freeze,
	506=>"Variant Also Negotiates".freeze,
	507=>"Insufficient Storage".freeze,
	508=>"Loop Detected".freeze,
	510=>"Not Extended".freeze,
	511=>"Network Authentication Required".freeze
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(request, status = 200, headers = {}, content = nil) ⇒ Response

the response object responds to a specific request on a specific io. hence, to initialize a response object, a request must be set.

use, at the very least ‘HTTPResponse.new request`



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/iodine/http/response.rb', line 28

def initialize request, status = 200, headers = {}, content = nil
	@request = request
	@status = status
	@headers = headers
	@body = content || []
	@request.cookies.set_response self
	@cookies = {}
	@bytes_written = 0
	@keep_alive = @http_sblocks_count = false
	# propegate flash object
	@flash = Hash.new do |hs,k|
		hs["magic_flash_#{k.to_s}".to_sym] if hs.has_key? "magic_flash_#{k.to_s}".to_sym
	end
	request.cookies.each do |k,v|
		@flash[k] = v if k.to_s.start_with? 'magic_flash_'
	end
end

Instance Attribute Details

#bodyObject

the response’s body buffer container (an array). This object is removed once the headers are sent and all write operations hang after that point.



16
17
18
# File 'lib/iodine/http/response.rb', line 16

def body
  @body
end

#bytes_writtenObject

Logs the number of bytes written.



20
21
22
# File 'lib/iodine/http/response.rb', line 20

def bytes_written
  @bytes_written
end

#flashObject (readonly)

the flash cookie-jar (single-use cookies, that survive only one request).



14
15
16
# File 'lib/iodine/http/response.rb', line 14

def flash
  @flash
end

#headersObject (readonly)

the response’s headers



12
13
14
# File 'lib/iodine/http/response.rb', line 12

def headers
  @headers
end

#keep_aliveObject

forces the connection to remain alive if this flag is set to ‘true` (otherwise follows Http and optimization guidelines).



22
23
24
# File 'lib/iodine/http/response.rb', line 22

def keep_alive
  @keep_alive
end

#requestObject

the request.



18
19
20
# File 'lib/iodine/http/response.rb', line 18

def request
  @request
end

#statusObject

the response’s status code



10
11
12
# File 'lib/iodine/http/response.rb', line 10

def status
  @status
end

Instance Method Details

#<<(str) ⇒ Object

pushes data to the buffer of the response. this is the preferred way to add data to the response.

If the headers were already sent, this will also send the data and hang until the data was sent.



154
155
156
157
# File 'lib/iodine/http/response.rb', line 154

def << str
	( @body ? @body.push(str) : ( (@body = str.dup) && request[:io].stream_response(self) ) ) if str
	self
end

#[](header) ⇒ Object

returns a response header, if set.



160
161
162
163
# File 'lib/iodine/http/response.rb', line 160

def [] header
	header.is_a?(String) ? (header.frozen? ? header : header.downcase!) : (header.is_a?(Symbol) ? (header = header.to_s.downcase) : (return false))
	headers[header]
end

#[]=(header, value) ⇒ Object

Sets a response header. response headers should be a downcase String (not a symbol or any other object).

this is the prefered to set a header.

Be aware that HTTP/2 will treat a header name with an upper-case letter as an Error! (while HTTP/1.1 ignores the letter case)

returns the value set for the header.

see HTTP response headers for valid headers and values: en.wikipedia.org/wiki/List_of_HTTP_header_fields



174
175
176
177
178
179
# File 'lib/iodine/http/response.rb', line 174

def []= header, value
	raise 'Cannot set headers after the headers had been sent.' if headers_sent?
	return (@headers.delete(header) && nil) if header.nil?
	header.is_a?(String) ? (header.frozen? ? header : header.downcase!) : (header.is_a?(Symbol) ? (header = header.to_s.downcase) : (return false))
	headers[header]	= value
end

#clearObject

clears the response object, unless headers were already sent (the response is already on it’s way, at least in part).

returns false if the response was already sent.



234
235
236
237
238
# File 'lib/iodine/http/response.rb', line 234

def clear
	return false if @headers.frozen?
	@status, @body, @headers, @cookies = 200, [], {}, {}
	self
end

#cookiesObject

Returns a writable combined hash of the request’s cookies and the response cookie values.

Any cookies writen to this hash (‘response.cookies = value` will be set using default values).

It’s also possible to use this combined hash to delete cookies, using: response.cookies = nil



140
141
142
# File 'lib/iodine/http/response.rb', line 140

def cookies
	@request.cookies
end

deletes a cookie (actually calls ‘set_cookie name, nil`)



227
228
229
# File 'lib/iodine/http/response.rb', line 227

def delete_cookie name
	set_cookie name, nil
end

#extract_bodyObject

This will return the Body object as a String… And set the body to ‘nil` (seeing as it was extracted from the response).



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/iodine/http/response.rb', line 312

def extract_body
	if @body.is_a?(Array)
		return (@body = nil) if body.empty?
		@body = @body.join
		extract_body
	elsif @body.is_a?(String)
		return (@body = nil) if body.empty?
		tmp = @body
		@body = nil
		tmp
	elsif body.nil?
		nil
	elsif body.respond_to? :each
		tmp = ''
		body.each {|s| tmp << s}
		body.close if body.respond_to? :close
		@body = nil
		return nil if tmp.empty? 
		tmp
	end
end

#extract_cookiesObject

This will return an array of cookie settings to be appended to ‘set-cookie` headers.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/iodine/http/response.rb', line 335

def extract_cookies
	unless @cookies.frozen?
		# remove old flash cookies
		@request.cookies.keys.each do |k|
			if k.to_s.start_with? 'magic_flash_'.freeze
				set_cookie k, nil
				flash.delete k
			end
		end
		#set new flash cookies
		@flash.each do |k,v|
			set_cookie "magic_flash_#{k.to_s}", v
		end
		@cookies.freeze
		# response.cookies.set_response nil
		@flash.freeze
	end
	[].tap do |arr|
		@cookies.each {|k, v| arr << "#{k.to_s}=#{v.to_s}"}
	end
end

#finishObject

attempts to write a non-streaming response to the IO. This can be done only once and will quitely fail subsequently.



241
242
243
# File 'lib/iodine/http/response.rb', line 241

def finish
	request[:io].send_response self
end

#headers_sent?Boolean

returns true if headers were already sent

Returns:

  • (Boolean)


52
53
54
# File 'lib/iodine/http/response.rb', line 52

def headers_sent?
	@headers.frozen?
end

#ioObject

returns the active protocol for the request.



47
48
49
# File 'lib/iodine/http/response.rb', line 47

def io
	@request[:io]
end

#raw_cookiesObject

Returns the response’s encoded cookie hash.

This method allows direct editing of the cookies about to be set.



147
148
149
# File 'lib/iodine/http/response.rb', line 147

def raw_cookies
	@cookies
end

#sessionHash like storage

Creates and returns the session storage object.

By default and for security reasons, session id’s created on a secure connection will NOT be available on a non secure connection (SSL/TLS).

Since this method renews the session_id’s cookie’s validity (update’s it’s times-stump), it must be called for the first time BEFORE the headers are sent.

After the session object was created using this method call, it should be safe to continue updating the session data even after the headers were sent and this method would act as an accessor for the already existing session object.

Returns:

  • (Hash like storage)

    creates and returns the session storage object with all the data from a previous connection.



128
129
130
131
132
133
# File 'lib/iodine/http/response.rb', line 128

def session
	return @session if @session
	id = request.cookies[::Iodine::Http.session_token.to_sym] || SecureRandom.uuid
	set_cookie ::Iodine::Http.session_token, id, expires: (Time.now+86_400), secure:  @request.ssl?
	@request[:session] = @session = ::Iodine::Http::SessionManager.get(id)
end

Sets/deletes cookies when headers are sent.

Accepts:

name

the cookie’s name

value

the cookie’s value

parameters

a parameters Hash for cookie creation.

Parameters accept any of the following Hash keys and values:

expires

a Time object with the expiration date. defaults to 10 years in the future.

max_age

a Max-Age HTTP cookie string.

path

the path from which the cookie is acessible. defaults to ‘/’.

domain

the domain for the cookie (best used to manage subdomains). defaults to the active domain (sub-domain limitations might apply).

secure

if set to ‘true`, the cookie will only be available over secure connections. defaults to false.

http_only

if true, the HttpOnly flag will be set (not accessible to javascript). defaults to false.

Setting the request’s coockies (‘request.cookies = value`) will automatically call this method with default parameters.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/iodine/http/response.rb', line 202

def set_cookie name, value, params = {}
	raise 'Cannot set cookies after the headers had been sent.' if headers_sent?
	name = name.to_s
	raise 'Illegal cookie name' if name =~ COOKIE_NAME_REGEXP
	if value.nil?
		params[:expires] = (Time.now - 315360000)
		value = 'deleted'.freeze					
	else
		params[:expires] ||= (Time.now + 315360000) unless params[:max_age]
	end
	params[:path] ||= '/'.freeze
	value = Iodine::Http::Request.encode_url(value)
	if params[:max_age]
		value << ('; Max-Age=%s' % params[:max_age])
	else
		value << ('; Expires=%s' % params[:expires].httpdate)
	end
	value << "; Path=#{params[:path]}"
	value << "; Domain=#{params[:domain]}" if params[:domain]
	value << '; Secure'.freeze if params[:secure]
	value << '; HttpOnly'.freeze if params[:http_only]
	@cookies[name.to_sym] = value
end

#stream_array(enum, &block) ⇒ true, Exception

Creates nested streaming blocks for an Array or another Enumerable object. Once all streaming blocks are done, the response will automatically finish.

Since streaming blocks might run in parallel, nesting the streaming blocks is important…

However, manually nesting hundreds of nesting blocks is time consuming and error prone.

sream_enum allows you to stream an enumerable knowing that Plezi will nest the streaming blocks dynamically.

Accepts:

enum

an Enumerable or an object that answers to the ‘to_a` method (the array will be used to stream the )

If an Array is passed to the enumerable, it will be changed and emptied as the streaming progresses. So, if preserving the array is important, please create a shallow copy of the array first using the ‘.dup` method.

i.e.:

data = "Hello world!".chars
response.stream_enum(data.each_with_index) {|c, i| response << c; sleep i/10.0 }

Returns:

  • (true, Exception)

    The method returns immidiatly with a value of true unless it is impossible to stream the response (an exception will be raised) or a block wasn’t supplied.



109
110
111
112
113
114
115
116
117
# File 'lib/iodine/http/response.rb', line 109

def stream_array enum, &block
	enum = enum.to_a
	return if enum.empty?
	stream_async do
		args = enum.shift
		block.call(*args)
		stream_array enum, &block
	end
end

#stream_async(&block) ⇒ true, Exception

Creates a streaming block. Once all streaming blocks are done, the response will automatically finish.

This avoids manualy handling #start_streaming, #finish_streaming and asynchronously tasking.

Every time data is sent the timout is reset. Responses longer than timeout will not be sent (but they will be processed).

Since Iodine is likely to be multi-threading (depending on your settings and architecture), it is important that streaming blocks are nested rather than chained. Chained streaming blocks might be executed in parallel and suffer frome race conditions that might lead to the response being corrupted.

Accepts a required block. i.e.

response.stream_async {sleep 1; response << "Hello Streaming"}
# OR, you can nest (but not chain) the streaming calls
response.stream_async do
  sleep 1
  response << "Hello Streaming"
  response.stream_async do
      sleep 1
      response << "\r\nGoodbye Streaming"
  end
end

Returns:

  • (true, Exception)

    The method returns immidiatly with a value of true unless it is impossible to stream the response (an exception will be raised) or a block wasn’t supplied.



80
81
82
83
84
85
86
# File 'lib/iodine/http/response.rb', line 80

def stream_async &block
	raise "Block required." unless block
	start_streaming unless @http_sblocks_count
	@http_sblocks_count += 1
	@stream_proc ||= Proc.new { |block| raise "IO closed. Streaming failed." if request[:io].io.closed?; block.call; @http_sblocks_count -= 1; finish_streaming }
	Iodine.run block, &@stream_proc
end

#uuidObject

Returns the connection’s UUID.



246
247
248
# File 'lib/iodine/http/response.rb', line 246

def uuid
	request[:io].id
end