Class: GRHttp::HTTPResponse

Inherits:
Object
  • Object
show all
Defined in:
lib/grhttp/http_response.rb,
lib/grhttp/rack_support.rb

Overview

this class handles HTTP response.

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

STATUS_CODES =

response status codes, as defined.

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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

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`


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/grhttp/http_response.rb', line 27

def initialize request, status = 200, headers = {}, content = nil
  @request = request
  @status = status
  @headers = headers
  @body = content || []
  @io = request[:io]
  @request.cookies.set_response self
  @http_version = 'HTTP/1.1' # request.version
  @bytes_sent = 0
  @finished = @streaming = false
  @cookies = {}
  @quite = false
  @chunked = 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 (readonly)

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.


15
16
17
# File 'lib/grhttp/http_response.rb', line 15

def body
  @body
end

#bytes_sentObject (readonly)

bytes sent to the asynchronous que so far - excluding headers (only the body object).


17
18
19
# File 'lib/grhttp/http_response.rb', line 17

def bytes_sent
  @bytes_sent
end

#flashObject (readonly)

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


13
14
15
# File 'lib/grhttp/http_response.rb', line 13

def flash
  @flash
end

#headersObject (readonly)

the response's headers


11
12
13
# File 'lib/grhttp/http_response.rb', line 11

def headers
  @headers
end

#ioObject (readonly)

the io through which the response will be sent.


19
20
21
# File 'lib/grhttp/http_response.rb', line 19

def io
  @io
end

#requestObject

the request.


21
22
23
# File 'lib/grhttp/http_response.rb', line 21

def request
  @request
end

#statusObject

the response's status code


9
10
11
# File 'lib/grhttp/http_response.rb', line 9

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.


126
127
128
129
130
# File 'lib/grhttp/http_response.rb', line 126

def << str
  @body ? @body.push(str) : (request.head? ? false :  send_body(str))
  self
  # send if streaming?
end

#[](header) ⇒ Object

returns a response header, if set.


133
134
135
# File 'lib/grhttp/http_response.rb', line 133

def [] header
  headers[header] # || @cookies[header]
end

#[]=(header, value) ⇒ Object

sets a response header. response headers should be a down-case String or Symbol.

this is the prefered to set a header.

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


144
145
146
147
148
149
# File 'lib/grhttp/http_response.rb', line 144

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.downcase! : (header.is_a?(Symbol) ? (header = header.to_s.downcase.to_sym) : (return false))
  headers[header]  = value
end

#cancel!Object

Forces the `finished` response's flag to true - use this to avoide sending a response or before manualy responding using the IO object.


61
62
63
# File 'lib/grhttp/http_response.rb', line 61

def cancel!
  @finished = true
end

#clearObject

clears the response object, unless headers were already sent.

returns false if the response was already sent.


198
199
200
201
202
# File 'lib/grhttp/http_response.rb', line 198

def clear
  return false if headers.frozen? || @finished
  @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


114
115
116
# File 'lib/grhttp/http_response.rb', line 114

def cookies
  @request.cookies
end

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


191
192
193
# File 'lib/grhttp/http_response.rb', line 191

def delete_cookie name
  set_cookie name, nil
end

#finishObject

Sends the response and flags the response as complete. Future data should not be sent. Your code might attempt sending data (which would probbaly be ignored by the client or raise an exception).


218
219
220
221
222
223
224
225
226
# File 'lib/grhttp/http_response.rb', line 218

def finish
  raise "Response already sent" if @finished
  @headers['content-length'] ||= (@body = @body.join).bytesize if !headers_sent? && @body.is_a?(Array)
  self.write
  @io.write "0\r\n\r\n" if @chunked
  @finished = true
  @io.close unless @io[:keep_alive]
  finished_log
end

#finished?Boolean

returns true if the response is already finished (the client isn't expecting any more data).


55
56
57
# File 'lib/grhttp/http_response.rb', line 55

def finished?
  @finished
end

#headers_sent?Boolean

returns true if headers were already sent


50
51
52
# File 'lib/grhttp/http_response.rb', line 50

def headers_sent?
  @headers.frozen?
end

#quite!Object

supresses logging on success.


119
120
121
# File 'lib/grhttp/http_response.rb', line 119

def quite!
  @quite = true
end

#run_rack(app) ⇒ Object


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
# File 'lib/grhttp/rack_support.rb', line 41

def run_rack app
  res = app.call(rack_env)
  raise "Rack app returned an unexpected value: #{res.to_s}" unless res && res.is_a?(Array)
  @status = res[0]
  @finished = true
  @body = nil
  @headers.clear
  @headers.freeze

  # fix connection header
  if res[1]['Connection'].to_s.match(/^k/i) || (@request[:version].to_f > 1 && @request['connection'].nil?) || @request['connection'].to_s.match(/^k/i)
    @io[:keep_alive] = true
    res[1]['Connection'] ||= "Keep-Alive\r\nKeep-Alive: timeout=5"
  end

  # @io[:keep_alive] = true if res[1]['Connection'].to_s.match(/^k/i)
  # res[1]['Connection'] ||= "close"

  # Send Rack's headers
  out = ''
  out << "#{@http_version} #{@status} #{STATUS_CODES[@status] || 'unknown'}\r\n" #"Date: #{Time.now.httpdate}\r\n"
  out << ('Set-Cookie: ' + (res[1].delete('Set-Cookie').split("\n")).join("\r\nSet-Cookie: ") + "\r\n") if res[1]['Set-Cookie']
  res[1].each {|h, v| out << "#{h.to_s}: #{v}\r\n"}

  out << "\r\n"
  @io.write out
  out.clear

  # send body using Rack's rendering 
  res[2].each {|d| @io.write d }
  res[2].close if res[2].respond_to? :close
  @io.close unless @io[:keep_alive]
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.


102
103
104
105
106
107
# File 'lib/grhttp/http_response.rb', line 102

def session
  return @session if @session
  id = request.cookies[GRHttp.session_token.to_sym] || SecureRandom.uuid
  set_cookie GRHttp.session_token, id, expires: (Time.now+86_400), secure:  @request.ssl?
  @session = GRHttp::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.


169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/grhttp/http_response.rb', line 169

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.match(/[\x00-\x20\(\)<>@,;:\\\"\/\[\]\?\=\{\}\s]/)
  params[:expires] = (Time.now - 315360000) unless value
  value ||= 'deleted'
  params[:expires] ||= (Time.now + 315360000) unless params[:max_age]
  params[:path] ||= '/'
  value = HTTP.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' if params[:secure]
  value << '; HttpOnly' if params[:http_only]
  @cookies[name.to_sym] = value
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).

Accepts a required block. i.e.

response.stream_async {sleep 1; response << "Hello Streaming"}
# OR, you can chain the streaming calls
response.stream_async do
  sleep 1
  response << "Hello Streaming"
  response.stream_async do
      sleep 1
      response << "Goodbye Streaming"
  end
end

85
86
87
88
89
90
91
# File 'lib/grhttp/http_response.rb', line 85

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

#try_finishObject

Danger Zone (internally used method, use with care): attempts to finish the response - if it was not flaged as streaming or completed.


229
230
231
# File 'lib/grhttp/http_response.rb', line 229

def try_finish
  finish unless @finished
end

#write(str = nil) ⇒ Object Also known as: send

sends the response object. headers will be frozen (they can only be sent at the head of the response).

the response will remain open for more data to be sent through (using `response << data` and `response.write`).


207
208
209
210
211
212
213
214
# File 'lib/grhttp/http_response.rb', line 207

def write(str = nil)
  raise 'HTTPResponse IO MISSING: cannot send http response without an io.' unless @io
  @body << str if @body && str
  return if send_headers
  return if request.head?
  send_body(str)
  self
end