From baec8d2c95bff086e47045c693b0cc1121dad61d Mon Sep 17 00:00:00 2001 From: Geoff Buesing Date: Sun, 17 Jan 2010 13:13:43 -0600 Subject: [PATCH] Rack::TrailingSlashRedirect - Remove trailing slash on path via 301 redirect --- README.rdoc | 1 + lib/rack/contrib.rb | 1 + lib/rack/contrib/trailing_slash_redirect.rb | 25 ++++++++++++ test/spec_rack_trailing_slash.rb | 56 +++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 0 deletions(-) create mode 100644 lib/rack/contrib/trailing_slash_redirect.rb create mode 100644 test/spec_rack_trailing_slash.rb diff --git a/README.rdoc b/README.rdoc index ca015ae..fa59cd3 100644 --- a/README.rdoc +++ b/README.rdoc @@ -46,6 +46,7 @@ interface: * Rack::HostMeta - Configures /host-meta using a block * Rack::Cookies - Adds simple cookie jar hash to env * Rack::Access - Limit access based on IP address +* Rack::TrailingSlashRedirect - Remove trailing slash on path via 301 redirect === Use diff --git a/lib/rack/contrib.rb b/lib/rack/contrib.rb index 8905437..0263a37 100644 --- a/lib/rack/contrib.rb +++ b/lib/rack/contrib.rb @@ -27,6 +27,7 @@ module Rack autoload :Sendfile, "rack/contrib/sendfile" autoload :Signals, "rack/contrib/signals" autoload :TimeZone, "rack/contrib/time_zone" + autoload :TrailingSlash, "rack/contrib/trailing_slash" autoload :Evil, "rack/contrib/evil" autoload :Callbacks, "rack/contrib/callbacks" autoload :NestedParams, "rack/contrib/nested_params" diff --git a/lib/rack/contrib/trailing_slash_redirect.rb b/lib/rack/contrib/trailing_slash_redirect.rb new file mode 100644 index 0000000..6c44e31 --- /dev/null +++ b/lib/rack/contrib/trailing_slash_redirect.rb @@ -0,0 +1,25 @@ +module Rack + # Request paths with a trailing slash are 301 redirected to the version without, e.g.: + # + # GET /foo/ # => 301 redirects to /foo + class TrailingSlashRedirect + HAS_TRAILING_SLASH = %r{^/(.*)/$} + + def initialize(app) + @app = app + end + + def call(env) + if env['PATH_INFO'] =~ HAS_TRAILING_SLASH + port = env["SERVER_PORT"] + port_str = ":#{port}" if port && port != '80' + location = "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{port_str}/#{$1}" + location = "#{location}?#{env['QUERY_STRING']}" if env['QUERY_STRING'].to_s =~ /\S/ + [301, {"Location" => location}, []] + else + @app.call(env) + end + end + + end +end diff --git a/test/spec_rack_trailing_slash.rb b/test/spec_rack_trailing_slash.rb new file mode 100644 index 0000000..50fad48 --- /dev/null +++ b/test/spec_rack_trailing_slash.rb @@ -0,0 +1,56 @@ +require 'test/spec' +require 'rack/mock' +require 'rack/contrib/trailing_slash_redirect' + +context "Rack::TrailingSlashRedirect" do + + specify "passes to downstream app when no trailing slash on path" do + app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]} + middleware = Rack::TrailingSlashRedirect.new(app) + status, headers, body = middleware.call({'PATH_INFO' => '/foo'}) + status.should == 200 + assert_equal ['Downstream app'], body + headers['Location'].should == nil + end + + specify "301 redirects when trailing slash on path" do + app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]} + middleware = Rack::TrailingSlashRedirect.new(app) + status, headers, body = middleware.call({'rack.url_scheme' => 'http', 'HTTP_HOST' => 'bar.com', 'PATH_INFO' => '/foo/'}) + status.should == 301 + headers['Location'].should == 'http://bar.com/foo' + end + + specify "query string is respected on redirect" do + app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]} + middleware = Rack::TrailingSlashRedirect.new(app) + status, headers, body = middleware.call({'rack.url_scheme' => 'http', 'HTTP_HOST' => 'bar.com', 'PATH_INFO' => '/foo/', 'QUERY_STRING' => 'baz=hi'}) + status.should == 301 + headers['Location'].should == 'http://bar.com/foo?baz=hi' + end + + specify "http is respected on redirect" do + app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]} + middleware = Rack::TrailingSlashRedirect.new(app) + status, headers, body = middleware.call({'rack.url_scheme' => 'https', 'HTTP_HOST' => 'bar.com', 'PATH_INFO' => '/foo/', 'QUERY_STRING' => 'baz=hi'}) + status.should == 301 + headers['Location'].should == 'https://bar.com/foo?baz=hi' + end + + specify "server port is respected on redirect when not 80" do + app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]} + middleware = Rack::TrailingSlashRedirect.new(app) + status, headers, body = middleware.call({'rack.url_scheme' => 'http', 'SERVER_PORT' => '8080', 'HTTP_HOST' => 'bar.com', 'PATH_INFO' => '/foo/'}) + status.should == 301 + headers['Location'].should == 'http://bar.com:8080/foo' + end + + specify "server port 80 is not added to Location on redirect" do + app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]} + middleware = Rack::TrailingSlashRedirect.new(app) + status, headers, body = middleware.call({'rack.url_scheme' => 'http', 'SERVER_PORT' => '80', 'HTTP_HOST' => 'bar.com', 'PATH_INFO' => '/foo/'}) + status.should == 301 + headers['Location'].should == 'http://bar.com/foo' + end + +end \ No newline at end of file -- 1.6.1