thin 1.7.2 → 2.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of thin might be problematic. Click here for more details.

Files changed (107) hide show
  1. data/.gitignore +9 -0
  2. data/CHANGELOG +29 -107
  3. data/Gemfile +8 -0
  4. data/README.md +44 -78
  5. data/Rakefile +28 -18
  6. data/bin/thin +4 -4
  7. data/examples/async.ru +21 -0
  8. data/examples/thin.conf.rb +39 -0
  9. data/lib/thin.rb +2 -44
  10. data/lib/thin/async.rb +108 -0
  11. data/lib/thin/backends/prefork.rb +44 -0
  12. data/lib/thin/backends/single_process.rb +28 -0
  13. data/lib/thin/chunked_body.rb +28 -0
  14. data/lib/thin/configurator.rb +118 -0
  15. data/lib/thin/connection.rb +246 -172
  16. data/lib/thin/listener.rb +114 -0
  17. data/lib/thin/request.rb +94 -74
  18. data/lib/thin/response.rb +112 -45
  19. data/lib/thin/runner.rb +134 -197
  20. data/lib/thin/server.rb +203 -252
  21. data/lib/thin/system.rb +49 -0
  22. data/lib/thin/version.rb +14 -29
  23. data/man/index.txt +3 -0
  24. data/man/thin-conf.5.ronn +121 -0
  25. data/man/thin.1.ronn +105 -0
  26. data/site/.gitignore +2 -0
  27. data/site/README.md +21 -0
  28. data/site/Rakefile +20 -0
  29. data/site/config.ru +4 -0
  30. data/site/public/images/grid.png +0 -0
  31. data/site/public/javascripts/dd_belatedpng.js +13 -0
  32. data/site/public/javascripts/modernizr-1.6.min.js +30 -0
  33. data/site/public/man/thin-conf.5.html +220 -0
  34. data/site/public/man/thin.1.html +177 -0
  35. data/site/site/assets/javascripts/main.coffee +2 -0
  36. data/site/site/assets/stylesheets/_config.scss +55 -0
  37. data/site/site/assets/stylesheets/main.scss +24 -0
  38. data/site/site/helpers.rb +17 -0
  39. data/site/site/layouts/base.erb +55 -0
  40. data/site/site/layouts/default.erb +17 -0
  41. data/site/site/pages/about.md +5 -0
  42. data/site/site/pages/index.erb +10 -0
  43. data/site/site/partials/.gitkeep +0 -0
  44. data/test/fixtures/big.txt +1 -0
  45. data/test/fixtures/small.txt +1 -0
  46. data/test/fixtures/thin.conf.rb +15 -0
  47. data/test/integration/async_test.rb +35 -0
  48. data/test/integration/big_request_test.rb +30 -0
  49. data/test/integration/config.ru +57 -0
  50. data/test/integration/daemonize_test.rb +26 -0
  51. data/test/integration/env_test.rb +44 -0
  52. data/test/integration/error_test.rb +37 -0
  53. data/test/integration/file_sending_test.rb +24 -0
  54. data/test/integration/keep_alive_test.rb +35 -0
  55. data/test/integration/robustness_test.rb +37 -0
  56. data/test/integration/single_process_test.rb +15 -0
  57. data/test/integration/socket_family_test.rb +38 -0
  58. data/test/integration/worker_test.rb +22 -0
  59. data/test/test_helper.rb +195 -0
  60. data/test/unit/configurator_test.rb +43 -0
  61. data/test/unit/connection_test.rb +94 -0
  62. data/test/unit/listener_test.rb +74 -0
  63. data/test/unit/request_test.rb +74 -0
  64. data/test/unit/response_test.rb +90 -0
  65. data/test/unit/server_test.rb +29 -0
  66. data/test/unit/system_test.rb +17 -0
  67. data/thin.gemspec +26 -0
  68. data/v2.todo +21 -0
  69. metadata +134 -89
  70. checksums.yaml +0 -7
  71. data/example/adapter.rb +0 -32
  72. data/example/async_app.ru +0 -126
  73. data/example/async_chat.ru +0 -247
  74. data/example/async_tailer.ru +0 -100
  75. data/example/config.ru +0 -22
  76. data/example/monit_sockets +0 -20
  77. data/example/monit_unixsock +0 -20
  78. data/example/myapp.rb +0 -1
  79. data/example/ramaze.ru +0 -12
  80. data/example/thin.god +0 -80
  81. data/example/thin_solaris_smf.erb +0 -36
  82. data/example/thin_solaris_smf.readme.txt +0 -150
  83. data/example/vlad.rake +0 -72
  84. data/ext/thin_parser/common.rl +0 -59
  85. data/ext/thin_parser/ext_help.h +0 -14
  86. data/ext/thin_parser/extconf.rb +0 -6
  87. data/ext/thin_parser/parser.c +0 -1447
  88. data/ext/thin_parser/parser.h +0 -49
  89. data/ext/thin_parser/parser.rl +0 -152
  90. data/ext/thin_parser/thin.c +0 -435
  91. data/lib/rack/adapter/loader.rb +0 -75
  92. data/lib/rack/adapter/rails.rb +0 -178
  93. data/lib/thin/backends/base.rb +0 -167
  94. data/lib/thin/backends/swiftiply_client.rb +0 -56
  95. data/lib/thin/backends/tcp_server.rb +0 -34
  96. data/lib/thin/backends/unix_server.rb +0 -56
  97. data/lib/thin/command.rb +0 -53
  98. data/lib/thin/controllers/cluster.rb +0 -178
  99. data/lib/thin/controllers/controller.rb +0 -189
  100. data/lib/thin/controllers/service.rb +0 -76
  101. data/lib/thin/controllers/service.sh.erb +0 -39
  102. data/lib/thin/daemonizing.rb +0 -180
  103. data/lib/thin/headers.rb +0 -40
  104. data/lib/thin/logging.rb +0 -174
  105. data/lib/thin/stats.html.erb +0 -216
  106. data/lib/thin/stats.rb +0 -52
  107. data/lib/thin/statuses.rb +0 -44
@@ -0,0 +1,114 @@
1
+ require "socket"
2
+
3
+ module Thin
4
+ # A listener holding a socket and its configuration.
5
+ class Listener
6
+ # Hostname the socket will bind to in case of IPv4/6 addresses.
7
+ attr_reader :host
8
+
9
+ # Port the socket will bind to in case of IPv4/6 addresses.
10
+ attr_reader :port
11
+
12
+ # UNIX domain socket the socket will bind to.
13
+ attr_reader :socket_file
14
+
15
+ def initialize(address, options={})
16
+ case address
17
+ when Integer
18
+ @host = ""
19
+ @port = address
20
+ when /\A(\/.*)\z/, /\Aunix:(.*)\z/ # /file.sock or unix:file.sock
21
+ @socket_file = $1
22
+ when /\A(?:\*:)?(\d+)\z/ # *:port or "port"
23
+ @host = ""
24
+ @port = $1.to_i
25
+ when /\A((?:\d{1,3}\.){3}\d{1,3}):(\d+)\z/ # 0.0.0.0:port
26
+ @host = $1
27
+ @port = $2.to_i
28
+ when /\A\[([a-fA-F0-9:]+)\]:(\d+)\z/ # IPV6 address: [::]:port
29
+ @host = $1
30
+ @port = $2.to_i
31
+ else
32
+ raise ArgumentError, "Invalid address #{address.inspect}. " +
33
+ "Accepted formats are: 3000, *:3000, 0.0.0.0:3000, [::]:3000, /file.sock or unix:file.sock"
34
+ end
35
+
36
+ # Default values
37
+ options = {
38
+ # Same defaults as Unicorn
39
+ :tcp_no_delay => true,
40
+ :tcp_no_push => false,
41
+ :ipv6_only => false,
42
+ :backlog => 1024
43
+ }.merge(options)
44
+
45
+ @backlog = options[:backlog]
46
+ self.tcp_no_delay = options[:tcp_nodelay] || options[:tcp_no_delay]
47
+ self.tcp_no_push = options[:tcp_nopush] || options[:tcp_no_push]
48
+ self.ipv6_only = options[:ipv6_only]
49
+ end
50
+
51
+ # Creates the socket and binds it to the address.
52
+ def socket
53
+ return @socket if @socket
54
+
55
+ @socket = Socket.new(socket_family, Socket::SOCK_STREAM, 0)
56
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
57
+
58
+ @socket
59
+ end
60
+
61
+ # Returns the socket family: Socket::AF_*
62
+ def socket_family
63
+ return Socket::AF_UNIX if @socket_file
64
+ return Socket::AF_INET6 if @host.include?(":")
65
+ return Socket::AF_INET
66
+ end
67
+
68
+ # Returns +true+ if the socket is a UNIX domain one.
69
+ def unix?
70
+ socket_family == Socket::AF_UNIX
71
+ end
72
+
73
+ def ipv6_only=(value)
74
+ socket.ipv6only! if value && !unix?
75
+ end
76
+
77
+ def tcp_no_delay=(value)
78
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, value) unless unix?
79
+ end
80
+
81
+ def tcp_no_push=(value)
82
+ # Taken from Unicorn
83
+ if defined?(TCP_CORK) # Linux
84
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, value)
85
+ elsif defined?(TCP_NOPUSH) # TCP_NOPUSH is untested (FreeBSD)
86
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NOPUSH, value)
87
+ end
88
+ end
89
+
90
+ def listen
91
+ delete_socket_file!
92
+
93
+ socket.bind unix? ? Socket.pack_sockaddr_un(@socket_file) : # UNIX domain
94
+ Socket.pack_sockaddr_in(@port, @host) # IPv4/6
95
+
96
+ socket.listen(@backlog)
97
+ end
98
+
99
+ def close
100
+ socket.close if @socket
101
+ delete_socket_file!
102
+ end
103
+
104
+ def to_s
105
+ unix? ? @socket_file : "#{@host}:#{@port}"
106
+ end
107
+
108
+ private
109
+ def delete_socket_file!
110
+ File.delete(@socket_file) if @socket_file && File.socket?(@socket_file)
111
+ end
112
+
113
+ end
114
+ end
@@ -1,105 +1,94 @@
1
- require 'tempfile'
1
+ require "stringio"
2
+ require "tempfile"
2
3
 
3
4
  module Thin
4
- # Raised when an incoming request is not valid
5
- # and the server can not process it.
6
- class InvalidRequest < IOError; end
7
-
8
5
  # A request sent by the client to the server.
9
6
  class Request
10
7
  # Maximum request body size before it is moved out of memory
11
8
  # and into a tempfile for reading.
12
9
  MAX_BODY = 1024 * (80 + 32)
13
10
  BODY_TMPFILE = 'thin-body'.freeze
14
- MAX_HEADER = 1024 * (80 + 32)
15
11
 
16
- INITIAL_BODY = String.new
12
+ INITIAL_BODY = ''
17
13
  # Force external_encoding of request's body to ASCII_8BIT
18
- INITIAL_BODY.encode!(Encoding::ASCII_8BIT) if INITIAL_BODY.respond_to?(:encode!) && defined?(Encoding::ASCII_8BIT)
14
+ INITIAL_BODY.encode!(Encoding::ASCII_8BIT) if INITIAL_BODY.respond_to?(:encode!)
19
15
 
20
16
  # Freeze some HTTP header names & values
21
17
  SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
22
18
  SERVER_NAME = 'SERVER_NAME'.freeze
23
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
19
+ SERVER_PORT = 'SERVER_PORT'.freeze
20
+ DEFAULT_PORT = '80'.freeze
21
+ HTTP_HOST = 'HTTP_HOST'.freeze
24
22
  LOCALHOST = 'localhost'.freeze
25
23
  HTTP_VERSION = 'HTTP_VERSION'.freeze
24
+ SERVER_PROTOCOL = 'SERVER_PROTOCOL'.freeze
26
25
  HTTP_1_0 = 'HTTP/1.0'.freeze
27
26
  REMOTE_ADDR = 'REMOTE_ADDR'.freeze
27
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
28
+ CONTENT_TYPE_L = 'Content-Type'.freeze
28
29
  CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
29
- CONNECTION = 'HTTP_CONNECTION'.freeze
30
+ CONTENT_LENGTH_L = 'Content-Length'.freeze
31
+ SCRIPT_NAME = 'SCRIPT_NAME'.freeze
32
+ QUERY_STRING = 'QUERY_STRING'.freeze
33
+ PATH_INFO = 'PATH_INFO'.freeze
34
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
35
+ FRAGMENT = 'FRAGMENT'.freeze
36
+ HTTP = 'http'.freeze
37
+ EMPTY = ''.freeze
30
38
  KEEP_ALIVE_REGEXP = /\bkeep-alive\b/i.freeze
31
39
  CLOSE_REGEXP = /\bclose\b/i.freeze
32
- HEAD = 'HEAD'.freeze
33
40
 
34
41
  # Freeze some Rack header names
35
42
  RACK_INPUT = 'rack.input'.freeze
36
43
  RACK_VERSION = 'rack.version'.freeze
37
44
  RACK_ERRORS = 'rack.errors'.freeze
45
+ RACK_URL_SCHEME = 'rack.url_scheme'.freeze
38
46
  RACK_MULTITHREAD = 'rack.multithread'.freeze
39
47
  RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
40
48
  RACK_RUN_ONCE = 'rack.run_once'.freeze
41
49
  ASYNC_CALLBACK = 'async.callback'.freeze
42
- ASYNC_CLOSE = 'async.close'.freeze
43
50
 
44
51
  # CGI-like request environment variables
45
52
  attr_reader :env
46
53
 
47
- # Unparsed data of the request
48
- attr_reader :data
49
-
50
54
  # Request body
51
55
  attr_reader :body
52
56
 
53
57
  def initialize
54
- @parser = Thin::HttpParser.new
55
- @data = String.new
56
- @nparsed = 0
57
- @body = StringIO.new(INITIAL_BODY.dup)
58
- @env = {
58
+ @body = StringIO.new(INITIAL_BODY)
59
+ @env = {
59
60
  SERVER_SOFTWARE => SERVER,
60
61
  SERVER_NAME => LOCALHOST,
62
+ SCRIPT_NAME => EMPTY,
61
63
 
62
64
  # Rack stuff
63
65
  RACK_INPUT => @body,
66
+ RACK_URL_SCHEME => HTTP,
64
67
 
65
68
  RACK_VERSION => VERSION::RACK,
66
- RACK_ERRORS => STDERR,
69
+ RACK_ERRORS => $stderr,
67
70
 
68
- RACK_MULTITHREAD => false,
69
- RACK_MULTIPROCESS => false,
70
71
  RACK_RUN_ONCE => false
71
72
  }
73
+ @keep_alive = false
72
74
  end
73
75
 
74
- # Parse a chunk of data into the request environment
75
- # Raises an +InvalidRequest+ if invalid.
76
- # Returns +true+ if the parsing is complete.
77
- def parse(data)
78
- if @parser.finished? # Header finished, can only be some more body
79
- @body << data
80
- else # Parse more header using the super parser
81
- @data << data
82
- raise InvalidRequest, 'Header longer than allowed' if @data.size > MAX_HEADER
83
-
84
- @nparsed = @parser.execute(@env, @data, @nparsed)
85
-
86
- # Transfer to a tempfile if body is very big
87
- move_body_to_tempfile if @parser.finished? && content_length > MAX_BODY
76
+ def headers=(headers)
77
+ # TODO benchmark & optimize
78
+ headers.each_pair do |k, v|
79
+ # Convert to Rack headers
80
+ if k == CONTENT_TYPE_L
81
+ @env[CONTENT_TYPE] = v
82
+ elsif k == CONTENT_LENGTH_L
83
+ @env[CONTENT_LENGTH] = v
84
+ else
85
+ @env["HTTP_" + k.upcase.tr("-", "_")] = v
86
+ end
88
87
  end
89
88
 
90
-
91
- if finished? # Check if header and body are complete
92
- @data = nil
93
- @body.rewind
94
- true # Request is fully parsed
95
- else
96
- false # Not finished, need more data
97
- end
98
- end
99
-
100
- # +true+ if headers and body are finished parsing
101
- def finished?
102
- @parser.finished? && @body.size >= content_length
89
+ host, port = @env[HTTP_HOST].split(":") if @env.key?(HTTP_HOST)
90
+ @env[SERVER_NAME] = host || LOCALHOST
91
+ @env[SERVER_PORT] = port || DEFAULT_PORT
103
92
  end
104
93
 
105
94
  # Expected size of the body
@@ -107,46 +96,77 @@ module Thin
107
96
  @env[CONTENT_LENGTH].to_i
108
97
  end
109
98
 
110
- # Returns +true+ if the client expects the connection to be persistent.
111
- def persistent?
112
- # Clients and servers SHOULD NOT assume that a persistent connection
113
- # is maintained for HTTP versions less than 1.1 unless it is explicitly
114
- # signaled. (http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html)
115
- if @env[HTTP_VERSION] == HTTP_1_0
116
- @env[CONNECTION] =~ KEEP_ALIVE_REGEXP
99
+ def remote_address=(address)
100
+ @env[REMOTE_ADDR] = address
101
+ end
117
102
 
118
- # HTTP/1.1 client intends to maintain a persistent connection unless
119
- # a Connection header including the connection-token "close" was sent
120
- # in the request
121
- else
122
- @env[CONNECTION].nil? || @env[CONNECTION] !~ CLOSE_REGEXP
123
- end
103
+ def method=(method)
104
+ @env[REQUEST_METHOD] = method
124
105
  end
125
106
 
126
- def remote_address=(address)
127
- @env[REMOTE_ADDR] = address
107
+ def http_version=(string)
108
+ @env[HTTP_VERSION] = string
109
+ @env[SERVER_PROTOCOL] = string
110
+ end
111
+
112
+ def http_version
113
+ @env[HTTP_VERSION]
114
+ end
115
+
116
+ def path=(path)
117
+ @env[PATH_INFO] = path
128
118
  end
129
119
 
130
- def threaded=(value)
131
- @env[RACK_MULTITHREAD] = value
120
+ def query_string=(string)
121
+ @env[QUERY_STRING] = string
122
+ end
123
+
124
+ def fragment=(string)
125
+ @env[FRAGMENT] = string
126
+ end
127
+
128
+ def keep_alive=(bool)
129
+ @keep_alive = bool
130
+ end
131
+
132
+ def multithread=(bool)
133
+ @env[RACK_MULTITHREAD] = bool
134
+ end
135
+
136
+ def multiprocess=(bool)
137
+ @env[RACK_MULTIPROCESS] = bool
138
+ end
139
+
140
+ # Returns +true+ if the client expect the connection to be kept alive.
141
+ def keep_alive?
142
+ @keep_alive
143
+ end
144
+
145
+ def <<(data)
146
+ @body << data
147
+
148
+ # Transfert to a tempfile if body is very big.
149
+ move_body_to_tempfile if content_length > MAX_BODY
150
+
151
+ @body
132
152
  end
133
153
 
134
154
  def async_callback=(callback)
135
155
  @env[ASYNC_CALLBACK] = callback
136
- @env[ASYNC_CLOSE] = EventMachine::DefaultDeferrable.new
137
156
  end
138
-
139
- def async_close
140
- @async_close ||= @env[ASYNC_CLOSE]
157
+
158
+ def support_encoding_chunked?
159
+ @env[HTTP_VERSION] != HTTP_1_0
141
160
  end
142
161
 
143
- def head?
144
- @env[REQUEST_METHOD] == HEAD
162
+ # Called when we're done processing the request.
163
+ def finish
164
+ @body.rewind
145
165
  end
146
166
 
147
167
  # Close any resource used by the request
148
168
  def close
149
- @body.close! if @body.class == Tempfile
169
+ @body.delete if @body.class == Tempfile
150
170
  end
151
171
 
152
172
  private
@@ -1,18 +1,56 @@
1
+ require "rack"
2
+
1
3
  module Thin
2
4
  # A response sent to the client.
3
5
  class Response
6
+ # Template async response.
7
+ ASYNC = [-1, {}, []].freeze
8
+
9
+ # Store HTTP header name-value pairs direcly to a string
10
+ # and allow duplicated entries on some names.
11
+ class Headers
12
+ HEADER_FORMAT = "%s: %s\r\n".freeze
13
+ ALLOWED_DUPLICATES = %w(Set-Cookie Set-Cookie2 Warning WWW-Authenticate).freeze
14
+
15
+ def initialize
16
+ @sent = {}
17
+ @out = []
18
+ end
19
+
20
+ # Add <tt>key: value</tt> pair to the headers.
21
+ # Ignore if already sent and no duplicates are allowed
22
+ # for this +key+.
23
+ def []=(key, value)
24
+ if !@sent.has_key?(key) || ALLOWED_DUPLICATES.include?(key)
25
+ @sent[key] = true
26
+ value = case value
27
+ when Time
28
+ value.httpdate
29
+ when NilClass
30
+ return
31
+ else
32
+ value.to_s
33
+ end
34
+ @out << HEADER_FORMAT % [key, value]
35
+ end
36
+ end
37
+
38
+ def has_key?(key)
39
+ @sent[key]
40
+ end
41
+
42
+ def to_s
43
+ @out.join
44
+ end
45
+ end
46
+
4
47
  CONNECTION = 'Connection'.freeze
5
48
  CLOSE = 'close'.freeze
6
49
  KEEP_ALIVE = 'keep-alive'.freeze
7
50
  SERVER = 'Server'.freeze
8
51
  CONTENT_LENGTH = 'Content-Length'.freeze
9
-
10
- PERSISTENT_STATUSES = [100, 101].freeze
11
-
12
- #Error Responses
13
- ERROR = [500, {'Content-Type' => 'text/plain'}, ['Internal server error']].freeze
14
- PERSISTENT_ERROR = [500, {'Content-Type' => 'text/plain', 'Connection' => 'keep-alive', 'Content-Length' => "21"}, ['Internal server error']].freeze
15
- BAD_REQUEST = [400, {'Content-Type' => 'text/plain'}, ['Bad Request']].freeze
52
+
53
+ KEEP_ALIVE_STATUSES = [100, 101].freeze
16
54
 
17
55
  # Status code
18
56
  attr_accessor :status
@@ -21,32 +59,21 @@ module Thin
21
59
  attr_accessor :body
22
60
 
23
61
  # Headers key-value hash
24
- attr_reader :headers
25
-
26
- def initialize
27
- @headers = Headers.new
28
- @status = 200
29
- @persistent = false
30
- @skip_body = false
62
+ attr_reader :headers
63
+
64
+ attr_reader :http_version
65
+
66
+ def initialize(status=200, headers=nil, body=nil)
67
+ @headers = Headers.new
68
+ @status = status
69
+ @keep_alive = false
70
+ @body = body
71
+ @http_version = "HTTP/1.1"
72
+
73
+ self.headers = headers if headers
31
74
  end
32
75
 
33
- # String representation of the headers
34
- # to be sent in the response.
35
- def headers_output
36
- # Set default headers
37
- @headers[CONNECTION] = persistent? ? KEEP_ALIVE : CLOSE unless @headers.has_key?(CONNECTION)
38
- @headers[SERVER] = Thin::NAME unless @headers.has_key?(SERVER)
39
-
40
- @headers.to_s
41
- end
42
-
43
- # Top header of the response,
44
- # containing the status code and response headers.
45
- def head
46
- "HTTP/1.1 #{@status} #{HTTP_STATUS_CODES[@status.to_i]}\r\n#{headers_output}\r\n"
47
- end
48
-
49
- if Thin.ruby_18?
76
+ if System.ruby_18?
50
77
 
51
78
  # Ruby 1.8 implementation.
52
79
  # Respects Rack specs.
@@ -78,8 +105,22 @@ module Thin
78
105
 
79
106
  end
80
107
 
108
+ # Finish preparing the response.
109
+ def finish
110
+ @headers[CONNECTION] = keep_alive? ? KEEP_ALIVE : CLOSE
111
+ @headers[SERVER] = Thin::SERVER
112
+ end
113
+
114
+ # Top header of the response,
115
+ # containing the status code and response headers.
116
+ def head
117
+ status_message = Rack::Utils::HTTP_STATUS_CODES[@status.to_i]
118
+ "#{@http_version} #{@status} #{status_message}\r\n#{@headers.to_s}\r\n"
119
+ end
120
+
81
121
  # Close any resource used by the response
82
122
  def close
123
+ @body.fail if @body.respond_to?(:fail)
83
124
  @body.close if @body.respond_to?(:close)
84
125
  end
85
126
 
@@ -88,30 +129,56 @@ module Thin
88
129
  # define your own +each+ method on +body+.
89
130
  def each
90
131
  yield head
91
-
92
- unless @skip_body
93
- if @body.is_a?(String)
94
- yield @body
95
- else
96
- @body.each { |chunk| yield chunk }
97
- end
132
+ if @body.is_a?(String)
133
+ yield @body
134
+ else
135
+ @body.each { |chunk| yield chunk }
98
136
  end
99
137
  end
100
-
138
+
101
139
  # Tell the client the connection should stay open
102
- def persistent!
103
- @persistent = true
140
+ def keep_alive!
141
+ @keep_alive = true
104
142
  end
105
143
 
106
144
  # Persistent connection must be requested as keep-alive
107
145
  # from the server and have a Content-Length, or the response
108
146
  # status must require that the connection remain open.
109
- def persistent?
110
- (@persistent && @headers.has_key?(CONTENT_LENGTH)) || PERSISTENT_STATUSES.include?(@status)
147
+ def keep_alive?
148
+ (@keep_alive && @headers.has_key?(CONTENT_LENGTH)) || KEEP_ALIVE_STATUSES.include?(@status)
149
+ end
150
+
151
+ def async?
152
+ @status == ASYNC.first
153
+ end
154
+
155
+ def file?
156
+ @body.respond_to?(:to_path)
157
+ end
158
+
159
+ def filename
160
+ @body.to_path
161
+ end
162
+
163
+ def body_callback=(proc)
164
+ @body.callback(&proc) if @body.respond_to?(:callback)
165
+ @body.errback(&proc) if @body.respond_to?(:errback)
166
+ end
167
+
168
+ def chunked_encoding!
169
+ @headers['Transfer-Encoding'] = 'chunked'
170
+ end
171
+
172
+ def http_version=(string)
173
+ return unless string && string == "HTTP/1.1" || string == "HTTP/1.0"
174
+ @http_version = string
111
175
  end
112
176
 
113
- def skip_body!
114
- @skip_body = true
177
+ def self.error(status=500, message=Rack::Utils::HTTP_STATUS_CODES[status])
178
+ new status,
179
+ { "Content-Type" => "text/plain",
180
+ "Content-Length" => Rack::Utils.bytesize(message).to_s },
181
+ [message]
115
182
  end
116
183
  end
117
184
  end