serverside 0.3.1 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -1,9 +1,9 @@
1
- = about ServerSide
1
+ == ServerSide: A Fast HTTP Server for Ruby
2
2
 
3
- ServerSide is an HTTP server framework designed to be as fast as possible, and
4
- as easy as possible to use. ServerSide includes a full-featured HTTP server, a
5
- controller-view system and a bunch of other tools to easily create servers and
6
- clusters of servers.
3
+ ServerSide is an HTTP server based on EventMachine, a fast event processing
4
+ library that offers superior scalability. ServerSide offers features such as
5
+ persistent connections, HTTP streaming, HTTP caching, and more. ServerSide
6
+ also includes tools to easily serve static files and create clusters.
7
7
 
8
8
  == Resources
9
9
 
@@ -16,6 +16,10 @@ To check out the source code:
16
16
 
17
17
  svn co http://serverside.googlecode.com/svn/trunk
18
18
 
19
+ === Contact
20
+
21
+ If you have any comments or suggestions please send an email to ciconia at gmail.com and I'll get back to you.
22
+
19
23
  == Installation
20
24
 
21
25
  sudo gem install serverside
@@ -34,7 +38,7 @@ To run the server without forking, use the 'serve' command:
34
38
 
35
39
  serverside serve .
36
40
 
37
- == Serving ERb Templates
41
+ === Serving ERb Templates
38
42
 
39
43
  ServerSide can render ERb[http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/]
40
44
  templates in a fashion similar to PHP. You can store templates in .rhtml files,
@@ -42,15 +46,15 @@ and ServerSide takes care of all the rest. ServerSide is also smart enough to
42
46
  allow you to use nice looking URL's with your templates, and automatically adds
43
47
  the .rhtml extension if the file is there.
44
48
 
45
- == Serving Dynamic Content
49
+ === Serving Dynamic Content
46
50
 
47
51
  By default ServerSide serves static files, but you can change the behavior by
48
52
  creating custom {routing rules}[classes/ServerSide/Connection/Router.html].
49
53
  Here's a simple routing rule:
50
54
 
51
- ServerSide::Router.route(:path => '/hello/:name') {
52
- send_response(200, 'text', "Hello #{@parameters[:name]}!")
53
- }
55
+ route :path => '/hello/:name' do
56
+ send_response(200, 'text', "Hello #{@params[:name]}")
57
+ end
54
58
 
55
59
  The ServerSide framework also lets you route requests based on any attribute of
56
60
  incoming requests, such as host name, path, URL parameters etc.
@@ -60,7 +64,7 @@ or tell serverside to explicitly load a specific file:
60
64
 
61
65
  serverside start ~/myapp/myapp.rb
62
66
 
63
- == Running a Cluster of Servers
67
+ === Running a Cluster of Servers
64
68
 
65
69
  ServerSide makes it easy to control a cluster of servers. Just supply a range of
66
70
  ports instead of a single port:
data/Rakefile CHANGED
@@ -6,9 +6,9 @@ require 'fileutils'
6
6
  include FileUtils
7
7
 
8
8
  NAME = "serverside"
9
- VERS = "0.3.1"
9
+ VERS = "0.4.1"
10
10
  CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
11
- RDOC_OPTS = ['--quiet', '--title', "ServerSide Documentation",
11
+ RDOC_OPTS = ['--quiet', '--title', "ServerSide: a Fast Ruby Web Framework",
12
12
  "--opname", "index.html",
13
13
  "--line-numbers",
14
14
  "--main", "README",
@@ -24,8 +24,8 @@ Rake::RDocTask.new do |rdoc|
24
24
  rdoc.rdoc_dir = 'doc/rdoc'
25
25
  rdoc.options += RDOC_OPTS
26
26
  rdoc.main = "README"
27
- rdoc.title = "ServerSide Documentation"
28
- rdoc.rdoc_files.add ['README', 'CHANGELOG', 'COPYING', 'lib/serverside.rb', 'lib/serverside/*.rb']
27
+ rdoc.title = "ServerSide: a Fast Ruby HTTP Server"
28
+ rdoc.rdoc_files.add ['README', 'COPYING', 'lib/serverside.rb', 'lib/serverside/**/*.rb']
29
29
  end
30
30
 
31
31
  spec = Gem::Specification.new do |s|
@@ -33,20 +33,22 @@ spec = Gem::Specification.new do |s|
33
33
  s.version = VERS
34
34
  s.platform = Gem::Platform::RUBY
35
35
  s.has_rdoc = true
36
- s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"]
36
+ s.extra_rdoc_files = ["README", "COPYING"]
37
37
  s.rdoc_options += RDOC_OPTS +
38
38
  ['--exclude', '^(examples|extras)\/', '--exclude', 'lib/serverside.rb']
39
- s.summary = "Performance-oriented web framework."
39
+ s.summary = "Fast Ruby HTTP Server."
40
40
  s.description = s.summary
41
41
  s.author = "Sharon Rosner"
42
42
  s.email = 'ciconia@gmail.com'
43
43
  s.homepage = 'http://code.google.com/p/serverside/'
44
44
  s.executables = ['serverside']
45
45
 
46
+ s.add_dependency('eventmachine')
47
+ s.add_dependency('erubis')
46
48
  s.add_dependency('metaid')
47
- s.required_ruby_version = '>= 1.8.2'
49
+ s.required_ruby_version = '>= 1.8.5'
48
50
 
49
- s.files = %w(COPYING README Rakefile) + Dir.glob("{bin,doc,spec,lib}/**/*")
51
+ s.files = %w(COPYING README Rakefile) + Dir.glob("{bin,doc,spec,lib}/**/*")
50
52
 
51
53
  s.require_path = "lib"
52
54
  s.bindir = "bin"
@@ -74,26 +76,24 @@ end
74
76
 
75
77
  require 'spec/rake/spectask'
76
78
 
77
- desc "Run specs with coverage"
79
+ desc "Run specs"
78
80
  Spec::Rake::SpecTask.new('spec') do |t|
79
81
  t.spec_files = FileList['spec/*_spec.rb']
80
- t.rcov = true
81
82
  end
82
83
 
83
- #require 'spec/rake/rcov_verify'
84
-
85
- #RCov::VerifyTask.new(:rcov_verify => :rcov) do |t|
86
- # t.threshold = 95.4 # Make sure you have rcov 0.7 or higher!
87
- # t.index_html = 'coverage/index.html'
88
- #end
84
+ desc "Run specs with coverage"
85
+ Spec::Rake::SpecTask.new('spec_coverage') do |t|
86
+ t.spec_files = FileList['spec/*_spec.rb']
87
+ t.rcov = true
88
+ end
89
89
 
90
90
  ##############################################################################
91
91
  # Statistics
92
92
  ##############################################################################
93
93
 
94
94
  STATS_DIRECTORIES = [
95
- %w(Code lib/),
96
- %w(Specification\ tests spec)
95
+ %w(Code lib/),
96
+ %w(Spec spec/)
97
97
  ].collect { |name, dir| [ name, "./#{dir}" ] }.select { |name, dir| File.directory?(dir) }
98
98
 
99
99
  desc "Report code statistics (KLOCs, etc) from the application"
data/bin/serverside CHANGED
@@ -10,10 +10,10 @@ $cmd_config = {
10
10
  }
11
11
 
12
12
  opts = OptionParser.new do |opts|
13
- opts.banner = "Usage: serverside start|stop|restart|serve [app1]"
13
+ opts.banner = "Usage: serverside start|stop|restart|serve [app_file]"
14
14
  opts.define_head "ServerSide, a fast and simple web framework for ruby."
15
15
  opts.separator ""
16
- opts.separator "The supplied app path can be directory or file references. \
16
+ opts.separator "The supplied application path can be directory or file references. \
17
17
  If the path refers to a directory the system will try to load serverside.rb in \
18
18
  that directory. If no path is given, the current working directory is assumed."
19
19
  opts.separator ""
@@ -57,15 +57,22 @@ unless %w(start stop restart serve).include?($cmd)
57
57
  exit
58
58
  end
59
59
 
60
- path = ARGV.shift || '.'
61
- if File.file?(path)
62
- require path
63
- else
64
- fn = File.join(path, 'serverside.rb')
65
- require fn if File.file?(fn)
60
+ $path = ARGV.shift || '.'
61
+ if File.file?($path)
62
+ app_code = IO.read($path)
63
+ elsif File.file?($path/'serverside.rb')
64
+ app_code = IO.read($path/'serverside.rb')
66
65
  end
67
- unless ServerSide::Router.routes_defined?
68
- ServerSide::Router.default_route {serve_static('.'/@path)}
66
+
67
+ $server = ServerSide::HTTP::Server.new do
68
+ if app_code
69
+ module_eval(app_code)
70
+ else
71
+ include ServerSide::HTTP::Static
72
+ def handle
73
+ serve_static($path/@uri)
74
+ end
75
+ end
69
76
  end
70
77
 
71
78
  if $cmd == 'serve'
@@ -75,18 +82,15 @@ if $cmd == 'serve'
75
82
  end
76
83
  puts "Serving at #{$cmd_config[:host]}:#{$cmd_config[:ports].begin}..."
77
84
  trap('INT') {exit}
78
- ServerSide::HTTP::Server.new($cmd_config[:host], $cmd_config[:ports].begin,
79
- ServerSide::Router).start
85
+
86
+ $server.start($cmd_config[:host], $cmd_config[:ports].begin)
80
87
  else
81
88
  daemon_class = Class.new(Daemon::Cluster) do
82
89
  meta_def(:pid_fn) {Daemon::WorkingDirectory/'serverside.pid'}
83
90
  meta_def(:server_loop) do |port|
84
- ServerSide::HTTP::Server.new(
85
- $cmd_config[:host], port, ServerSide::Router).start
91
+ $server.start($cmd_config[:host], port)
86
92
  end
87
93
  meta_def(:ports) {$cmd_config[:ports]}
88
94
  end
89
95
  Daemon.control(daemon_class, $cmd)
90
-
91
- # ServerSide::Application.daemonize($cmd_config, $cmd)
92
96
  end
@@ -4,32 +4,6 @@ module Daemon
4
4
  # Implements a cluster controlling daemon. The daemon itself itself forks
5
5
  # a child process for each port.
6
6
  class Cluster < Base
7
- # Stores and recalls pids for the child processes.
8
- class PidFile
9
- FN = 'serverside_cluster.pid'
10
-
11
- # Deletes the cluster's pid file.
12
- def self.delete
13
- FileUtils.rm(FN) if File.file?(FN)
14
- end
15
-
16
- # Stores a pid in the cluster's pid file.
17
- def self.store_pid(pid)
18
- File.open(FN, 'a') {|f| f.puts pid}
19
- end
20
-
21
- # Recalls all child pids.
22
- def self.recall_pids
23
- pids = []
24
- File.open(FN, 'r') do |f|
25
- while !f.eof?
26
- pids << f.gets.to_i
27
- end
28
- end
29
- pids
30
- end
31
- end
32
-
33
7
  # Forks a child process with a specific port.
34
8
  def self.fork_server(port)
35
9
  fork do
@@ -37,20 +11,17 @@ module Daemon
37
11
  server_loop(port)
38
12
  end
39
13
  end
14
+
15
+ @@pids = []
40
16
 
41
17
  # Starts child processes.
42
18
  def self.start_servers
43
- PidFile.delete
44
- ports.each do |p|
45
- PidFile.store_pid(fork_server(p))
46
- end
19
+ ports.each {|p| @@pids << fork_server(p)}
47
20
  end
48
21
 
49
22
  # Stops child processes.
50
23
  def self.stop_servers
51
- pids = PidFile.recall_pids
52
- pids.each {|pid| begin; Process.kill('TERM', pid); rescue; end}
53
- PidFile.delete
24
+ @@pids.each {|pid| Process.kill('TERM', pid) rescue nil}
54
25
  end
55
26
 
56
27
  # The main daemon loop. Does nothing for now.
@@ -1,4 +1,6 @@
1
- # String extension methods.
1
+ require 'md5'
2
+
3
+ # String extensions.
2
4
  class String
3
5
  # Encodes a normal string to a URI string.
4
6
  def uri_escape
@@ -11,7 +13,11 @@ class String
11
13
  tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%')].pack('H*')}
12
14
  end
13
15
 
14
- # Concatenates a path (do we really need this sugar?)
16
+ def html_escape
17
+ gsub(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
18
+ end
19
+
20
+ # Concatenates a path (purely sugar)
15
21
  def /(o)
16
22
  File.join(self, o.to_s)
17
23
  end
@@ -21,14 +27,45 @@ class String
21
27
  gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').
22
28
  tr("-", "_").downcase
23
29
  end
30
+
31
+ # Converts an underscored name into a camelized name
32
+ def camelize
33
+ gsub(/(^|_)(.)/) {$2.upcase}
34
+ end
35
+
36
+ LINE_RE = /^([^\r]*)\r\n/n.freeze
37
+ EMPTY_STRING = ''.freeze
38
+
39
+ def get_line
40
+ sub!(LINE_RE, EMPTY_STRING) ? $1 : nil
41
+ end
42
+
43
+ def get_up_to_boundary(boundary)
44
+ if i = index(boundary)
45
+ part = i > 0 ? self[0..(i - 1)] : ''
46
+ slice!(0..(i + boundary.size - 1))
47
+ part
48
+ end
49
+ end
50
+
51
+ def get_up_to_boundary_with_crlf(boundary)
52
+ if i = index(boundary)
53
+ part = i > 0 ? self[0..(i - 1)] : ''
54
+ slice!(0..(i + boundary.size + 1))
55
+ part
56
+ end
57
+ end
58
+
59
+ def etag
60
+ MD5.hexdigest(self)
61
+ end
24
62
  end
25
63
 
26
- # Symbol extensions and overrides.
64
+ # Symbol extensions.
27
65
  class Symbol
28
- # A faster to_s method. This is called a lot, and memoization gives us
29
- # performance between 10%-35% better.
30
- def to_s
31
- @_to_s || (@_to_s = id2name)
66
+ # Concatenates a path (purely sugar)
67
+ def /(o)
68
+ File.join(self, o.to_s)
32
69
  end
33
70
  end
34
71
 
@@ -46,3 +83,15 @@ class Object
46
83
  end
47
84
  end
48
85
 
86
+ # Coercion of boolean values to integer
87
+ def true.to_i; -1; end
88
+ def false.to_i; 0; end
89
+
90
+ # Process extensions.
91
+ module Process
92
+ # Checks for the existance of a process.
93
+ def self.exists?(pid)
94
+ getpgid(pid) && true rescue false
95
+ end
96
+ end
97
+
@@ -1,15 +1,5 @@
1
1
  require 'fileutils'
2
2
 
3
- class Object
4
- def backtrace
5
- raise RuntimeError
6
- rescue => e
7
- bt = e.backtrace.dup
8
- bt.shift
9
- bt
10
- end
11
- end
12
-
13
3
  # The Daemon module takes care of starting and stopping daemons.
14
4
  module Daemon
15
5
  WorkingDirectory = FileUtils.pwd
@@ -49,7 +39,11 @@ module Daemon
49
39
  when :stop
50
40
  stop(daemon)
51
41
  when :restart
52
- stop(daemon)
42
+ begin
43
+ stop(daemon)
44
+ sleep 2
45
+ rescue
46
+ end
53
47
  start(daemon)
54
48
  else
55
49
  raise 'Invalid command. Please specify start, stop or restart.'
@@ -64,9 +58,9 @@ module Daemon
64
58
  PidFile.store(daemon, Process.pid)
65
59
  Dir.chdir WorkingDirectory
66
60
  File.umask 0000
67
- # STDIN.reopen "/dev/null"
68
- # STDOUT.reopen "/dev/null", "a"
69
- # STDERR.reopen STDOUT
61
+ STDIN.reopen "/dev/null"
62
+ STDOUT.reopen "/dev/null", "a"
63
+ STDERR.reopen STDOUT
70
64
  trap("TERM") {daemon.stop; exit}
71
65
  daemon.start
72
66
  end
@@ -75,13 +69,12 @@ module Daemon
75
69
  # Stops the daemon by sending it a TERM signal.
76
70
  def self.stop(daemon)
77
71
  pid = PidFile.recall(daemon)
78
- pid && Process.kill("TERM", pid)
72
+ pid && Process.kill("TERM", pid) rescue nil
79
73
  PidFile.remove(daemon)
80
74
  end
81
75
 
82
76
  def self.alive?(daemon)
83
77
  pid = PidFile.recall(daemon) rescue nil
84
- return nil if !pid
85
- `ps #{pid}` =~ /#{pid}/ ? true : nil
78
+ pid ? Process.exists?(pid) : false
86
79
  end
87
80
  end
@@ -0,0 +1,79 @@
1
+ module ServerSide::HTTP
2
+ # HTTP Caching behavior
3
+ module Caching
4
+ # Sets caching-related headers and validates If-Modified-Since and
5
+ # If-None-Match headers. If a match is found, a 304 response is sent.
6
+ # Otherwise, the supplied block is invoked.
7
+ def cache(opts)
8
+ not_modified = false
9
+
10
+ # check etag
11
+ if etag = opts[:etag]
12
+ etag = "\"#{etag}\""
13
+ add_header(ETAG, etag) if etag
14
+ not_modified = etag_match(etag)
15
+ end
16
+
17
+ # check last_modified
18
+ if last_modified = opts[:last_modified]
19
+ add_header(LAST_MODIFIED, last_modified)
20
+ not_modified ||= modified_match(last_modified)
21
+ end
22
+
23
+ # check cache-control
24
+ remove_cache_control
25
+ if cache_control = opts[:cache_control]
26
+ add_header(CACHE_CONTROL, cache_control)
27
+ end
28
+
29
+ # add an expires header
30
+ if expires = opts[:expires]
31
+ add_header(EXPIRES, expires.httpdate)
32
+ elsif age = opts[:age]
33
+ add_header(EXPIRES, (Time.now + age).httpdate)
34
+ end
35
+
36
+ # if not modified, send a 304 response. Otherwise we yield to the
37
+ # supplied block.
38
+ not_modified ?
39
+ send_response(STATUS_NOT_MODIFIED, nil) : yield
40
+ end
41
+
42
+ COMMA = ','.freeze
43
+
44
+ # Matches the supplied etag against any of the entities in the
45
+ # If-None-Match header.
46
+ def etag_match(etag)
47
+ matches = @request_headers[IF_NONE_MATCH]
48
+ if matches
49
+ matches.split(COMMA).each do |e|
50
+
51
+ return true if e.strip == etag
52
+ end
53
+ end
54
+ false
55
+ end
56
+
57
+ # Matches the supplied last modified date against the If-Modified-Since
58
+ # header.
59
+ def modified_match(last_modified)
60
+ if modified_since = @request_headers[IF_MODIFIED_SINCE]
61
+ last_modified.to_i == Time.parse(modified_since).to_i
62
+ else
63
+ false
64
+ end
65
+ rescue => e
66
+ raise MalformedRequestError, "Invalid value in If-Modified-Since header"
67
+ end
68
+
69
+ # Sets the Cache-Control header.
70
+ def set_cache_control(directive)
71
+ add_header(CACHE_CONTROL, directive)
72
+ end
73
+
74
+ def remove_cache_control
75
+ @response_headers.reject! {|h| h =~ /^#{CACHE_CONTROL}/}
76
+ end
77
+ end
78
+ end
79
+
@@ -0,0 +1,69 @@
1
+ module ServerSide::HTTP
2
+ # HTTP versions
3
+ VERSION_1_0 = '1.0'.freeze
4
+ VERSION_1_1 = '1.1'.freeze
5
+
6
+ # maximum sizes
7
+ MAX_REQUEST_LINE_SIZE = 8192
8
+ MAX_HEADER_SIZE = 8192
9
+ MAX_HEADER_COUNT = 256 # should be enough methinks
10
+
11
+ # request body and response body
12
+ CONTENT_LENGTH = 'Content-Length'.freeze
13
+ CONTENT_TYPE = 'Content-Type'.freeze
14
+ MULTIPART_FORM_DATA_RE = /^multipart\/form-data; boundary=(.+)$/.freeze
15
+ CONTENT_DISPOSITION = 'Content-Disposition'.freeze
16
+ DISPOSITION_FORM_DATA_RE = /^form-data; name="([^"]+)"(; filename="([^"]+)")?$/.freeze
17
+ FORM_URL_ENCODED = 'application/x-www-form-urlencoded'.freeze
18
+
19
+ # connection
20
+ CONNECTION = 'Connection'.freeze
21
+ KEEP_ALIVE = 'keep-alive'.freeze
22
+ CLOSE = 'close'.freeze
23
+ CONNECTION_CLOSE = "Connection: close\r\n".freeze
24
+
25
+ # headers
26
+ HOST = 'Host'.freeze
27
+ X_FORWARDED_FOR = 'X-Forwarded-For'.freeze
28
+ DATE = 'Date'.freeze
29
+ LOCATION = 'Location'.freeze
30
+
31
+ # caching
32
+ IF_NONE_MATCH = 'If-None-Match'.freeze
33
+ IF_MODIFIED_SINCE = 'If-Modified-Since'.freeze
34
+ ETAG = 'ETag'.freeze
35
+ LAST_MODIFIED = 'Last-Modified'.freeze
36
+ CACHE_CONTROL = 'Cache-Control'.freeze
37
+ NO_CACHE = 'no-cache'.freeze
38
+ EXPIRES = 'Expires'.freeze
39
+
40
+ # response status
41
+ STATUS_OK = '200 OK'.freeze
42
+ STATUS_CREATED = '201 Created'.freeze
43
+ STATUS_ACCEPTED = '202 Accepted'.freeze
44
+ STATUS_NO_CONTENT = '204 No Content'.freeze
45
+
46
+ STATUS_MOVED_PERMANENTLY = '301 Moved Permanently'.freeze
47
+ STATUS_FOUND = '302 Found'.freeze
48
+ STATUS_NOT_MODIFIED = '304 Not Modified'.freeze
49
+
50
+ STATUS_BAD_REQUEST = '400 Bad Request'.freeze
51
+ STATUS_UNAUTHORIZED = '401 Unauthorized'.freeze
52
+ STATUS_FORBIDDEN = '403 Forbidden'.freeze
53
+ STATUS_NOT_FOUND = '404 Not Found'.freeze
54
+ STATUS_METHOD_NOT_ALLOWED = '405 Method Not Allowed'.freeze
55
+ STATUS_NOT_ACCEPTABLE = '406 Not Acceptable'.freeze
56
+ STATUS_CONFLICT = '409 Conflict'.freeze
57
+ STATUS_REQUEST_ENTITY_TOO_LARGE = '413 Request Entity Too Large'.freeze
58
+ STATUS_REQUEST_URI_TOO_LONG = '414 Request-URI Too Long'.freeze
59
+ STATUS_UNSUPPORTED_MEDIA_TYPE = '415 Unsupported Media Type'.freeze
60
+
61
+ STATUS_INTERNAL_SERVER_ERROR = '500 Internal Server Error'.freeze
62
+ STATUS_NOT_IMPLEMENTED = '501 Not Implemented'.freeze
63
+ STATUS_SERVICE_UNAVAILABLE = '503 Service Unavailable'.freeze
64
+
65
+ # cookies
66
+ COOKIE = 'Cookie'.freeze
67
+ SET_COOKIE = 'Set-Cookie'.freeze
68
+ COOKIE_EXPIRED_TIME = Time.at(0).freeze
69
+ end
@@ -0,0 +1,24 @@
1
+ # StandardError extensions.
2
+ class StandardError
3
+ # Returns the HTTP status code associated with the error class.
4
+ def http_status
5
+ ServerSide::HTTP::STATUS_INTERNAL_SERVER_ERROR
6
+ end
7
+
8
+ # Sets the HTTP status code associated with the error class.
9
+ def self.set_http_status(value)
10
+ define_method(:http_status) {value}
11
+ end
12
+ end
13
+
14
+ module ServerSide::HTTP
15
+ # This error is raised when a malformed request is encountered.
16
+ class MalformedRequestError < RuntimeError
17
+ set_http_status STATUS_BAD_REQUEST
18
+ end
19
+
20
+ # This error is raised when an invalid file is referenced.
21
+ class FileNotFoundError < RuntimeError
22
+ set_http_status STATUS_NOT_FOUND
23
+ end
24
+ end