diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1b5e6a8..87af470e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,20 +28,21 @@ jobs: - ubuntu ruby: - - 2.5 - 2.6 - 2.7 - 3.0 - 3.1 - 3.2 - 3.3 + - 3.4 - gemfile: ["Gemfile"] + gemfile: + - gems/rack-v2.rb include: - experimental: false os: macos - ruby: 3.3 + ruby: 3.4 gemfile: gems/rack-v2.rb - experimental: true os: ubuntu @@ -53,13 +54,16 @@ jobs: gemfile: gems/rack-v1.rb - experimental: true os: ubuntu - ruby: 3.2 + ruby: 3.4 gemfile: gems/rack-v2.rb - # enable when rack v3 is supported - # - experimental: true - # os: ubuntu - # ruby: 3.2 - # env: BUNDLE_GEMFILE=gems/rack-head.rb + - experimental: true + os: ubuntu + ruby: 3.4 + gemfile: gems/rack-v3.rb + - experimental: true + os: ubuntu + ruby: 3.4 + gemfile: gems/rack-head.rb steps: - uses: actions/checkout@v3 diff --git a/Gemfile b/Gemfile index 7d8e9e86..0711ed08 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,8 @@ gemspec group :development do gem "rake-compiler" + gem "rdoc" + gem "benchmark" end group :test do diff --git a/benchmark/benchmarker.rb b/benchmark/benchmarker.rb index 17378245..7929ba42 100644 --- a/benchmark/benchmarker.rb +++ b/benchmark/benchmarker.rb @@ -37,7 +37,7 @@ def start_server(handler_name) end app = proc do |env| - [200, {'Content-Type' => 'text/html', 'Content-Length' => '11'}, ['hello world']] + [200, {'content-type' => 'text/html', 'content-length' => '11'}, ['hello world']] end handler = Rack::Handler.const_get(handler_name) diff --git a/example/adapter.rb b/example/adapter.rb index 77375fdf..d583159e 100644 --- a/example/adapter.rb +++ b/example/adapter.rb @@ -8,7 +8,7 @@ def call(env) body = ["hello!"] [ 200, - { 'Content-Type' => 'text/plain' }, + { 'content-type' => 'text/plain' }, body ] end @@ -20,13 +20,13 @@ def call(env) run SimpleAdapter.new end map '/files' do - run Rack::File.new('.') + run Rack::Files.new('.') end end # You could also start the server like this: # # app = Rack::URLMap.new('/test' => SimpleAdapter.new, -# '/files' => Rack::File.new('.')) +# '/files' => Rack::Files.new('.')) # Thin::Server.start('0.0.0.0', 3000, app) # diff --git a/example/async_app.ru b/example/async_app.ru index 4e9be17b..8c5a2d8b 100755 --- a/example/async_app.ru +++ b/example/async_app.ru @@ -87,7 +87,7 @@ class AsyncApp body = DeferrableBody.new # Get the headers out there asap, let the client know we're alive... - EventMachine::next_tick { env['async.callback'].call [200, {'Content-Type' => 'text/plain'}, body] } + EventMachine::next_tick { env['async.callback'].call [200, {'content-type' => 'text/plain'}, body] } # Semi-emulate a long db request, instead of a timer, in reality we'd be # waiting for the response data. Whilst this happens, other connections diff --git a/example/async_chat.ru b/example/async_chat.ru index e9e3ddf8..55d12040 100755 --- a/example/async_chat.ru +++ b/example/async_chat.ru @@ -118,7 +118,7 @@ class Chat send_message = function(message_box) { xhr = XHR(); xhr.open("POST", "/", true); - xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("X_REQUESTED_WITH", "XMLHttpRequest"); xhr.send("message="+escape(message_box.value)); scroll(); @@ -161,7 +161,7 @@ class Chat body.callback { delete_user user_id } EventMachine::next_tick do - renderer.call [200, {'Content-Type' => 'text/html'}, body] + renderer.call [200, {'content-type' => 'text/html'}, body] end end diff --git a/example/async_tailer.ru b/example/async_tailer.ru index 467f0df6..eda6be9d 100755 --- a/example/async_tailer.ru +++ b/example/async_tailer.ru @@ -70,7 +70,7 @@ class AsyncTailer EventMachine::next_tick do - env['async.callback'].call [200, {'Content-Type' => 'text/html'}, body] + env['async.callback'].call [200, {'content-type' => 'text/html'}, body] body.call ["

Async Tailer

"]
       
diff --git a/example/config.ru b/example/config.ru
index bc655f85..8a530fd2 100644
--- a/example/config.ru
+++ b/example/config.ru
@@ -14,7 +14,7 @@ app = proc do |env|
   
   [
     200,                                        # Status code
-    { 'Content-Type' => 'text/html' },          # Reponse headers
+    { 'content-type' => 'text/html' },          # Reponse headers
     body                                        # Body of the response
   ]
 end
diff --git a/ext/thin_parser/common.rl b/ext/thin_parser/common.rl
index 08877036..ad9ba529 100644
--- a/ext/thin_parser/common.rl
+++ b/ext/thin_parser/common.rl
@@ -43,7 +43,7 @@
   Method = ( upper | digit | safe ){1,20} >mark %request_method;
 
   http_number = ( digit+ "." digit+ ) ;
-  HTTP_Version = ( "HTTP/" http_number ) >mark %http_version ;
+  HTTP_Version = ( "HTTP/" http_number ) >mark %request_http_version ;
   Request_Line = ( Method " " Request_URI ("#" Fragment){0,1} " " HTTP_Version CRLF ) ;
 
   field_name = ( token -- ":" )+ >start_field %write_field;
diff --git a/ext/thin_parser/parser.c b/ext/thin_parser/parser.c
index 42832de4..7005d083 100644
--- a/ext/thin_parser/parser.c
+++ b/ext/thin_parser/parser.c
@@ -295,8 +295,8 @@ case 13:
 tr17:
 #line 59 "parser.rl"
 	{	
-    if (parser->http_version != NULL) {
-      parser->http_version(parser->data, PTR_TO(mark), LEN(mark, p));
+    if (parser->request_http_version != NULL) {
+      parser->request_http_version(parser->data, PTR_TO(mark), LEN(mark, p));
     }
   }
 	goto st14;
diff --git a/ext/thin_parser/parser.h b/ext/thin_parser/parser.h
index f80d40f6..6a587e3a 100644
--- a/ext/thin_parser/parser.h
+++ b/ext/thin_parser/parser.h
@@ -33,7 +33,7 @@ typedef struct http_parser {
   element_cb fragment;
   element_cb request_path;
   element_cb query_string;
-  element_cb http_version;
+  element_cb request_http_version;
   element_cb header_done;
   
 } http_parser;
diff --git a/ext/thin_parser/parser.rl b/ext/thin_parser/parser.rl
index 05c8eb62..eb0db63c 100644
--- a/ext/thin_parser/parser.rl
+++ b/ext/thin_parser/parser.rl
@@ -56,9 +56,9 @@
     }
   }
 
-  action http_version {	
-    if (parser->http_version != NULL) {
-      parser->http_version(parser->data, PTR_TO(mark), LEN(mark, fpc));
+  action request_http_version {
+    if (parser->request_http_version != NULL) {
+      parser->request_http_version(parser->data, PTR_TO(mark), LEN(mark, fpc));
     }
   }
 
diff --git a/ext/thin_parser/thin.c b/ext/thin_parser/thin.c
index b3e777cc..082d7bd5 100644
--- a/ext/thin_parser/thin.c
+++ b/ext/thin_parser/thin.c
@@ -21,7 +21,7 @@ static VALUE global_request_method;
 static VALUE global_request_uri;
 static VALUE global_fragment;
 static VALUE global_query_string;
-static VALUE global_http_version;
+static VALUE global_request_http_version;
 static VALUE global_content_length;
 static VALUE global_http_content_length;
 static VALUE global_request_path;
@@ -150,11 +150,12 @@ static void query_string(void *data, const char *at, size_t length)
   rb_hash_aset(req, global_query_string, val);
 }
 
-static void http_version(void *data, const char *at, size_t length)
+static void request_http_version(void *data, const char *at, size_t length)
 {
   VALUE req = (VALUE)data;
   VALUE val = rb_str_new(at, length);
-  rb_hash_aset(req, global_http_version, val);
+  rb_hash_aset(req, global_request_http_version, val);
+  rb_hash_aset(req, global_server_protocol, val);
 }
 
 /** Finalizes the request header to have a bunch of stuff that's
@@ -211,7 +212,6 @@ static void header_done(void *data, const char *at, size_t length)
   }
   
   /* set some constants */
-  rb_hash_aset(req, global_server_protocol, global_server_protocol_value);
   rb_hash_aset(req, global_url_scheme, global_url_scheme_value);
   rb_hash_aset(req, global_script_name, global_empty);
 }
@@ -237,7 +237,7 @@ VALUE Thin_HttpParser_alloc(VALUE klass)
   hp->fragment = fragment;
   hp->request_path = request_path;
   hp->query_string = query_string;
-  hp->http_version = http_version;
+  hp->request_http_version = request_http_version;
   hp->header_done = header_done;
   thin_http_parser_init(hp);
 
@@ -401,7 +401,7 @@ void Init_thin_parser()
   DEF_GLOBAL(request_uri, "REQUEST_URI");
   DEF_GLOBAL(fragment, "FRAGMENT");
   DEF_GLOBAL(query_string, "QUERY_STRING");
-  DEF_GLOBAL(http_version, "HTTP_VERSION");
+  DEF_GLOBAL(request_http_version, "thin.request_http_version");
   DEF_GLOBAL(request_path, "REQUEST_PATH");
   DEF_GLOBAL(content_length, "CONTENT_LENGTH");
   DEF_GLOBAL(http_content_length, "HTTP_CONTENT_LENGTH");
diff --git a/gems/rack-head.rb b/gems/rack-head.rb
index e79a8585..d3001cbe 100644
--- a/gems/rack-head.rb
+++ b/gems/rack-head.rb
@@ -1,16 +1,5 @@
 # frozen_string_literal: true
 
-source 'https://rubygems.org'
-
-gemspec path: "../"
+eval_gemfile "../Gemfile"
 
 gem 'rack', github: 'rack/rack'
-
-group :development do
-  gem "rake-compiler"
-end
-
-group :test do
-  gem "rake", ">= 12.3.3"
-  gem "rspec", "~> 3.5"
-end
diff --git a/gems/rack-v1.rb b/gems/rack-v1.rb
index a77be550..e5be601e 100644
--- a/gems/rack-v1.rb
+++ b/gems/rack-v1.rb
@@ -1,17 +1,5 @@
 # frozen_string_literal: true
 
-source 'https://rubygems.org'
-
-gemspec path: "../"
+eval_gemfile "../Gemfile"
 
 gem 'rack', '~> 1.0'
-
-group :development do
-  gem "rake-compiler"
-end
-
-group :test do
-  gem "rake", ">= 12.3.3"
-  gem "rspec", "~> 3.5"
-end
-
diff --git a/gems/rack-v2.rb b/gems/rack-v2.rb
index 498d17c4..8dfecd29 100644
--- a/gems/rack-v2.rb
+++ b/gems/rack-v2.rb
@@ -1,16 +1,5 @@
 # frozen_string_literal: true
 
-source 'https://rubygems.org'
+eval_gemfile "../Gemfile"
 
-gemspec path: "../"
-
-gem 'rack', '~> 2.0'
-
-group :development do
-  gem "rake-compiler"
-end
-
-group :test do
-  gem "rake", ">= 12.3.3"
-  gem "rspec", "~> 3.5"
-end
+gem 'rack', "~> 2.0"
diff --git a/gems/rack-v3.rb b/gems/rack-v3.rb
new file mode 100644
index 00000000..01923819
--- /dev/null
+++ b/gems/rack-v3.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+eval_gemfile "../Gemfile"
+
+gem 'rack', "~> 3.0"
diff --git a/lib/rack/adapter/loader.rb b/lib/rack/adapter/loader.rb
index 4574f284..c05968ed 100644
--- a/lib/rack/adapter/loader.rb
+++ b/lib/rack/adapter/loader.rb
@@ -15,6 +15,13 @@ class AdapterNotFound < RuntimeError; end
     [:file,    nil]
   ]
   
+  # Rack v1 compatibility...
+  unless const_defined?(:Files)
+    require 'rack/file'
+    
+    Files = File
+  end
+  
   module Adapter
     # Guess which adapter to use based on the directory structure
     # or file content.
@@ -64,7 +71,7 @@ def self.for(name, options={})
         return Merb::Rack::Application.new
         
       when :file
-        return Rack::File.new(options[:chdir])
+        return Rack::Files.new(options[:chdir])
         
       else
         raise AdapterNotFound, "Adapter not found: #{name}"
diff --git a/lib/rack/adapter/rails.rb b/lib/rack/adapter/rails.rb
index 0da1b98b..31113e20 100644
--- a/lib/rack/adapter/rails.rb
+++ b/lib/rack/adapter/rails.rb
@@ -23,7 +23,7 @@ def initialize(options = {})
         load_application
 
         @rails_app = self.class.rack_based? ? ActionController::Dispatcher.new : CgiApp.new
-        @file_app  = Rack::File.new(::File.join(RAILS_ROOT, "public"))
+        @file_app  = Rack::Files.new(::File.join(RAILS_ROOT, "public"))
       end
 
       def load_application
diff --git a/lib/thin/request.rb b/lib/thin/request.rb
index b5efe24b..4b3fe4df 100644
--- a/lib/thin/request.rb
+++ b/lib/thin/request.rb
@@ -22,7 +22,7 @@ class Request
     SERVER_NAME       = 'SERVER_NAME'.freeze
     REQUEST_METHOD    = 'REQUEST_METHOD'.freeze
     LOCALHOST         = 'localhost'.freeze
-    HTTP_VERSION      = 'HTTP_VERSION'.freeze
+    REQUEST_HTTP_VERSION      = 'thin.request_http_version'.freeze
     HTTP_1_0          = 'HTTP/1.0'.freeze
     REMOTE_ADDR       = 'REMOTE_ADDR'.freeze
     CONTENT_LENGTH    = 'CONTENT_LENGTH'.freeze
@@ -114,7 +114,7 @@ def persistent?
       # Clients and servers SHOULD NOT assume that a persistent connection
       # is maintained for HTTP versions less than 1.1 unless it is explicitly
       # signaled. (http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html)
-      if @env[HTTP_VERSION] == HTTP_1_0
+      if @env[REQUEST_HTTP_VERSION] == HTTP_1_0
         @env[CONNECTION] =~ KEEP_ALIVE_REGEXP
 
       # HTTP/1.1 client intends to maintain a persistent connection unless
diff --git a/lib/thin/response.rb b/lib/thin/response.rb
index 7a5ce93c..f5ff806e 100644
--- a/lib/thin/response.rb
+++ b/lib/thin/response.rb
@@ -1,18 +1,64 @@
 module Thin
   # A response sent to the client.
   class Response
-    CONNECTION     = 'Connection'.freeze
+    class Stream
+      def initialize(writer)
+        @read_closed = true
+        @write_closed = false
+        @writer = writer
+      end
+
+      def read(length = nil, outbuf = nil)
+        raise ::IOError, 'not opened for reading' if @read_closed
+      end
+
+      def write(chunk)
+        raise ::IOError, 'not opened for writing' if @write_closed
+
+        @writer.call(chunk)
+      end
+
+      alias :<< :write
+
+      def close
+        @read_closed = @write_closed = true
+
+        nil
+      end
+
+      def closed?
+        @read_closed && @write_closed
+      end
+
+      def close_read
+        @read_closed = true
+
+        nil
+      end
+
+      def close_write
+        @write_closed = true
+
+        nil
+      end
+
+      def flush
+        self
+      end
+    end
+
+    CONNECTION     = 'connection'.freeze
     CLOSE          = 'close'.freeze
     KEEP_ALIVE     = 'keep-alive'.freeze
-    SERVER         = 'Server'.freeze
-    CONTENT_LENGTH = 'Content-Length'.freeze
+    SERVER         = 'server'.freeze
+    CONTENT_LENGTH = 'content-length'.freeze
 
     PERSISTENT_STATUSES  = [100, 101].freeze
 
     #Error Responses
-    ERROR            = [500, {'Content-Type' => 'text/plain'}, ['Internal server error']].freeze
-    PERSISTENT_ERROR = [500, {'Content-Type' => 'text/plain', 'Connection' => 'keep-alive', 'Content-Length' => "21"}, ['Internal server error']].freeze
-    BAD_REQUEST      = [400, {'Content-Type' => 'text/plain'}, ['Bad Request']].freeze
+    ERROR            = [500, {'content-type' => 'text/plain'}, ['Internal server error']].freeze
+    PERSISTENT_ERROR = [500, {'content-type' => 'text/plain', 'connection' => 'keep-alive', 'content-length' => "21"}, ['Internal server error']].freeze
+    BAD_REQUEST      = [400, {'content-type' => 'text/plain'}, ['Bad Request']].freeze
 
     # Status code
     attr_accessor :status
@@ -86,14 +132,16 @@ def close
     # Yields each chunk of the response.
     # To control the size of each chunk
     # define your own +each+ method on +body+.
-    def each
+    def each(&block)
       yield head
 
       unless @skip_body
         if @body.is_a?(String)
           yield @body
-        else
+        elsif @body.respond_to?(:each)
           @body.each { |chunk| yield chunk }
+        else
+          @body.call(Stream.new(block))
         end
       end
     end
@@ -104,7 +152,7 @@ def persistent!
     end
 
     # Persistent connection must be requested as keep-alive
-    # from the server and have a Content-Length, or the response
+    # from the server and have a content-length, or the response
     # status must require that the connection remain open.
     def persistent?
       (@persistent && @headers.has_key?(CONTENT_LENGTH)) || PERSISTENT_STATUSES.include?(@status)
diff --git a/lib/thin/stats.rb b/lib/thin/stats.rb
index 57252363..385b0887 100644
--- a/lib/thin/stats.rb
+++ b/lib/thin/stats.rb
@@ -43,7 +43,7 @@ def serve(env)
         
         [
           200,
-          { 'Content-Type' => 'text/html' },
+          { 'content-type' => 'text/html' },
           [body]
         ]
       end
diff --git a/script/profile b/script/profile
index 87045277..6bc81fdb 100755
--- a/script/profile
+++ b/script/profile
@@ -9,7 +9,7 @@ require 'thin'
 
 class Adapter
   def call(env)
-    [200, {'Content-Type' => 'text/html', 'Content-Length' => '11'}, ['hello world']]
+    [200, {'content-type' => 'text/html', 'content-length' => '11'}, ['hello world']]
   end
 end
 
diff --git a/site/rdoc.rb b/site/rdoc.rb
index aca0e339..9652805f 100644
--- a/site/rdoc.rb
+++ b/site/rdoc.rb
@@ -169,7 +169,7 @@ module Page
   %realtitle%
 ENDIF:title
   
-  
+