public
Rubygem
Description: A very fast & simple Ruby web server
Homepage: http://code.macournoyer.com/thin/
Clone URL: git://github.com/macournoyer/thin.git
Click here to lend your support to: thin and make a donation at www.pledgie.com !
Add Content-Length header to response automatically when possible [#74 
state:resolved]
Dan Kubb (author)
Fri Jul 18 12:40:18 -0700 2008
macournoyer (committer)
Sat Jul 19 06:34:08 -0700 2008
commit  affccc93679a419c8fca2f92fc11e006687bdf15
tree    f2bdb3301514db8c1e743a5b7bdd3d721765e567
parent  f99bd54ab90485a5be1bdb3f86600f5fc68728a9
...
8
9
10
11
12
13
14
 
15
16
17
...
8
9
10
 
 
 
 
11
12
13
14
0
@@ -8,10 +8,7 @@ class SimpleAdapter
0
     body = ["hello!"]
0
     [
0
       200,
0
-      {
0
-        'Content-Type'   => 'text/plain',
0
-        'Content-Length' => body.join.size.to_s,
0
-      },
0
+      { 'Content-Type' => 'text/plain' },
0
       body
0
     ]
0
   end
...
15
16
17
18
19
20
21
 
22
23
24
...
15
16
17
 
 
 
 
18
19
20
21
0
@@ -15,10 +15,7 @@ app = proc do |env|
0
   
0
   [
0
     200,                                        # Status code
0
-    {
0
-      'Content-Type' => 'text/html',            # Reponse headers
0
-      'Content-Length' => body.join.size.to_s
0
-    },
0
+    { 'Content-Type' => 'text/html' },          # Reponse headers
0
     body                                        # Body of the response
0
   ]
0
 end
...
5
6
7
 
 
 
 
8
9
10
...
66
67
68
 
 
 
69
70
71
...
142
143
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
146
147
...
5
6
7
8
9
10
11
12
13
14
...
70
71
72
73
74
75
76
77
78
...
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
0
@@ -5,6 +5,10 @@ module Thin
0
   # This class is instanciated by EventMachine on each new connection
0
   # that is opened.
0
   class Connection < EventMachine::Connection
0
+    CONTENT_LENGTH    = 'Content-Length'.freeze
0
+    TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
0
+    CHUNKED_REGEXP    = /\bchunked\b/i.freeze
0
+
0
     include Logging
0
     
0
     # Rack application (adapter) served by this connection.
0
@@ -66,6 +70,9 @@ module Thin
0
     def post_process(result)
0
       return unless result
0
       
0
+      # Set the Content-Length header if possible
0
+      set_content_length(result) if need_content_length?(result)
0
+      
0
       @response.status, @response.headers, @response.body = result
0
       
0
       # Make the response persistent if requested by the client
0
@@ -142,5 +149,29 @@ module Thin
0
       def socket_address
0
         Socket.unpack_sockaddr_in(get_peername)[1]
0
       end
0
+      
0
+    private
0
+      def need_content_length?(result)
0
+        status, headers, body = result
0
+        return false if headers.has_key?(CONTENT_LENGTH)
0
+        return false if (100..199).include?(status) || status == 204 || status == 304
0
+        return false if headers.has_key?(TRANSFER_ENCODING) && headers[TRANSFER_ENCODING] =~ CHUNKED_REGEXP
0
+        return false unless body.kind_of?(String) || body.kind_of?(Array)
0
+        true
0
+      end
0
+      
0
+      def set_content_length(result)
0
+        headers, body = result[1..2]
0
+        case body
0
+        when String
0
+          headers[CONTENT_LENGTH] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
0
+        when Array
0
+           bytes = 0
0
+           body.each do |p|
0
+             bytes += p.respond_to?(:bytesize) ? p.bytesize : p.size
0
+           end
0
+           headers[CONTENT_LENGTH] = bytes.to_s
0
+        end
0
+      end
0
   end
0
 end
0
\ No newline at end of file
...
22
23
24
25
26
 
 
27
28
29
...
22
23
24
 
 
25
26
27
28
29
0
@@ -22,8 +22,8 @@ module Thin
0
     FORWARDED_FOR     = 'HTTP_X_FORWARDED_FOR'.freeze
0
     CONTENT_LENGTH    = 'CONTENT_LENGTH'.freeze
0
     CONNECTION        = 'HTTP_CONNECTION'.freeze
0
-    KEEP_ALIVE_REGEXP = /keep-alive/i
0
-    CLOSE_REGEXP      = /close/i
0
+    KEEP_ALIVE_REGEXP = /\bkeep-alive\b/i.freeze
0
+    CLOSE_REGEXP      = /\bclose\b/i.freeze
0
     
0
     # Freeze some Rack header names
0
     RACK_INPUT        = 'rack.input'.freeze
...
43
44
45
46
47
48
49
 
50
51
52
...
43
44
45
 
 
 
 
46
47
48
49
0
@@ -43,10 +43,7 @@ module Thin
0
         
0
         [
0
           200,
0
-          {
0
-            'Content-Type' => 'text/html',
0
-            'Content-Length' => body.size.to_s
0
-          },
0
+          { 'Content-Type' => 'text/html' },
0
           [body]
0
         ]
0
       end
...
6
7
8
9
 
10
11
12
...
6
7
8
 
9
10
11
12
0
@@ -6,7 +6,7 @@ describe Server, "HTTP pipelining" do
0
     start_server do |env|
0
       calls += 1
0
       body = env['PATH_INFO'] + '-' + calls.to_s
0
-      [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.size.to_s }, body]
0
+      [200, { 'Content-Type' => 'text/html' }, body]
0
     end
0
     @server.maximum_persistent_connections = 1024
0
   end
...
4
5
6
7
 
8
9
10
...
4
5
6
 
7
8
9
10
0
@@ -4,7 +4,7 @@ describe Server, 'robustness' do
0
   before do
0
     start_server do |env|
0
       body = 'hello!'
0
-      [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.size.to_s }, body]
0
+      [200, { 'Content-Type' => 'text/html' }, body]
0
     end
0
   end
0
   
...
3
4
5
6
 
7
8
9
...
3
4
5
 
6
7
8
9
0
@@ -3,7 +3,7 @@ require File.dirname(__FILE__) + '/../spec_helper'
0
 describe Server, "stopping" do
0
   before do
0
     start_server do |env|
0
-      [200, { 'Content-Type' => 'text/html', 'Content-Length' => '2' }, ['ok']]
0
+      [200, { 'Content-Type' => 'text/html' }, ['ok']]
0
     end
0
   end
0
   
...
12
13
14
15
 
16
17
18
...
12
13
14
 
15
16
17
18
0
@@ -12,7 +12,7 @@ else
0
       sleep 2 # HACK ooh boy, I wish I knew how to make those specs more stable...
0
       start_server('0.0.0.0', 5555, :backend => Backends::SwiftiplyClient, :wait_for_socket => false) do |env|
0
         body = env.inspect + env['rack.input'].read
0
-        [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.size.to_s }, body]
0
+        [200, { 'Content-Type' => 'text/html' }, body]
0
       end
0
     end
0
     
...
4
5
6
7
 
8
9
10
 
11
12
13
14
15
16
17
18
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
21
22
...
35
36
37
38
 
39
40
41
...
4
5
6
 
7
8
9
 
10
11
12
13
14
15
 
 
 
 
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
...
46
47
48
 
49
50
51
52
0
@@ -4,19 +4,30 @@ describe Server, 'on TCP socket' do
0
   before do
0
     start_server do |env|
0
       body = env.inspect + env['rack.input'].read
0
-      [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.size.to_s }, body]
0
+      [200, { 'Content-Type' => 'text/html' }, body]
0
     end
0
   end
0
-    
0
+  
0
   it 'should GET from Net::HTTP' do
0
     get('/?cthis').should include('cthis')
0
   end
0
   
0
   it 'should GET from TCPSocket' do
0
-    send_data("GET /?this HTTP/1.1\r\nConnection: close\r\n\r\n").
0
-      should include("HTTP/1.1 200 OK",
0
-                     "Content-Type: text/html", "Content-Length: ",
0
-                     "Connection: close", "this")
0
+    status, headers, body = parse_response(send_data("GET /?this HTTP/1.0\r\nConnection: close\r\n\r\n"))
0
+    status.should == 200
0
+    headers['Content-Type'].should == 'text/html'
0
+    headers['Connection'].should == 'close'
0
+    body.should include('this')
0
+  end
0
+  
0
+  it "should add the Content-Length to the response when not present" do
0
+    status, headers, body = parse_response(send_data("GET / HTTP/1.0\r\nConnection: close\r\n\r\n"))
0
+    headers.should have_key('Content-Length')
0
+  end
0
+  
0
+  it 'should set the Content-Length to equal the body size in bytes' do
0
+    status, headers, body = parse_response(send_data("GET / HTTP/1.0\r\nConnection: close\r\n\r\n"))
0
+    headers['Content-Length'].should == (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
0
   end
0
   
0
   it 'should return empty string on incomplete headers' do
0
@@ -35,7 +46,7 @@ describe Server, 'on TCP socket' do
0
     big = 'X' * (20 * 1024)
0
     post('/', :big => big).should include(big)
0
   end
0
-    
0
+  
0
   it "should retreive remote address" do
0
     get('/').should include('"REMOTE_ADDR"=>"127.0.0.1"')
0
   end
...
6
7
8
9
 
10
11
12
...
6
7
8
 
9
10
11
12
0
@@ -6,7 +6,7 @@ describe Server, 'with threads' do
0
     start_server DEFAULT_TEST_ADDRESS, DEFAULT_TEST_PORT, :threaded => true do |env|
0
       sleep env['PATH_INFO'].delete('/').to_i
0
       @requests += 1
0
-      [200, { 'Content-Type' => 'text/html', 'Content-Length' => '2' }, 'hi']
0
+      [200, { 'Content-Type' => 'text/html' }, 'hi']
0
     end
0
   end
0
   
...
3
4
5
6
 
7
8
9
...
3
4
5
 
6
7
8
9
0
@@ -3,7 +3,7 @@ require File.dirname(__FILE__) + '/../spec_helper'
0
 describe Server, "on UNIX domain socket" do
0
   before do
0
     start_server('/tmp/thin_test.sock') do |env|
0
-      [200, { 'Content-Type' => 'text/html', 'Content-Length' => env.inspect.size.to_s }, [env.inspect]]
0
+      [200, { 'Content-Type' => 'text/html' }, [env.inspect]]
0
     end
0
   end
0
   
...
190
191
192
 
 
 
 
 
 
 
 
 
 
193
194
195
...
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
0
@@ -190,6 +190,16 @@ module Helpers
0
     out
0
   end
0
   
0
+  def parse_response(response)
0
+    raw_headers, body = response.split("\r\n\r\n", 2)
0
+    raw_status, raw_headers = raw_headers.split("\r\n", 2)
0
+
0
+    status  = raw_status.match(%r{\AHTTP/1.1\s+(\d+)\b}).captures.first.to_i
0
+    headers = Hash[ *raw_headers.split("\r\n").map { |h| h.split(/:\s+/, 2) }.flatten ]
0
+
0
+    [ status, headers, body ]
0
+  end
0
+  
0
   def get(url)
0
     if @server.backend.class == Backends::UnixServer
0
       send_data("GET #{url} HTTP/1.1\r\nConnection: close\r\n\r\n")

Comments