Merge branch 'master' into psgi
Florian Ragwitz [Sun, 23 Jan 2011 15:42:37 +0000 (15:42 +0000)]
master:
Version 5.80030
Fix a warning with undef bodies
Changelog the Package::Stash bugfix
Changelog the resp->body improvements
Work with PP Package::Stash again now the incorrect assumption that broke with ::XS is fixed
Stop relying on Package::Stash PP bugs
Add conflict
Bad test failed to show bug.
Fix the case for body '0'
Make response body able to be undef to allow RenderView to see 'defined but empty' body, allowing X-Sendfile to work nicer. Also removes horrible modifier code. win/win if it doesn't break anything else.
Extra links in docs
Add test case for uri_for() with #fragment and query params, broken by 5.7008 when uri_for() was reimplemented without URI.pm.
Found a another fault in chained action dispatcher.

Conflicts:
lib/Catalyst/Engine/CGI.pm

1  2 
Makefile.PL
lib/Catalyst.pm
lib/Catalyst/Engine.pm

diff --combined Makefile.PL
@@@ -19,11 -19,9 +19,11 @@@ requires 'namespace::clean' => '0.13'
  requires 'B::Hooks::EndOfScope' => '0.08';
  requires 'MooseX::Emulate::Class::Accessor::Fast' => '0.00903';
  requires 'Class::MOP' => '0.95';
 +requires 'Data::OptList';
  requires 'Moose' => '1.03';
  requires 'MooseX::MethodAttributes::Inheritable' => '0.24';
  requires 'MooseX::Role::WithOverloading' => '0.05';
 +requires 'MooseX::Types::LoadableClass' => '0.003';
  requires 'Carp';
  requires 'Class::C3::Adopt::NEXT' => '0.07';
  requires 'CGI::Simple::Cookie' => '1.109';
@@@ -45,7 -43,6 +45,7 @@@ requires 'Text::SimpleTable' => '0.03'
  requires 'Time::HiRes';
  requires 'Tree::Simple' => '1.15';
  requires 'Tree::Simple::Visitor::FindByPath';
 +requires 'Try::Tiny';
  requires 'URI' => '1.35';
  requires 'Task::Weaken';
  requires 'Text::Balanced'; # core in 5.8.x but mentioned for completeness
@@@ -54,13 -51,10 +54,13 @@@ requires 'MooseX::Getopt' => '0.30'
  requires 'MooseX::Types';
  requires 'MooseX::Types::Common::Numeric';
  requires 'String::RewritePrefix' => '0.004'; # Catalyst::Utils::resolve_namespace
 +requires 'Plack' => '0.9935'; # Setup empty PATH_INFO if needed
 +requires 'Plack::Middleware::ReverseProxy' => '0.04';
  
  test_requires 'Class::Data::Inheritable';
  test_requires 'Test::Exception';
  test_requires 'Test::More' => '0.88';
 +test_requires 'Data::Dump';
  
  # aggregate tests if AGGREGATE_TESTS is set and a recent Test::Aggregate and a Test::Simple it works with is available
  if ($ENV{AGGREGATE_TESTS} && can_use('Test::Simple', '0.88') && can_use('Test::Aggregate', '0.364')) {
@@@ -129,6 -123,7 +129,7 @@@ my %conflicts = 
      'Catalyst::Plugin::ENV' => '9999', # This plugin is just stupid, full stop
                                         # should have been a core fix.
      'Catalyst::Plugin::Unicode::Encoding' => '0.2',
+     'Catalyst::Plugin::Authentication' => '0.10010', # _config accessor in ::Credential::Password
      'Catalyst::Authentication::Credential::HTTP' => '1.009',
      'Catalyst::Plugin::Session::Store::File' => '0.16',
      'Catalyst::Plugin::Session' => '0.21',
diff --combined lib/Catalyst.pm
@@@ -29,10 -29,8 +29,10 @@@ use Tree::Simple::Visitor::FindByUID
  use Class::C3::Adopt::NEXT;
  use List::MoreUtils qw/uniq/;
  use attributes;
 +use String::RewritePrefix;
  use utf8;
  use Carp qw/croak carp shortmess/;
 +use Try::Tiny;
  
  BEGIN { require 5.008004; }
  
@@@ -71,17 -69,17 +71,17 @@@ our $GO        = Catalyst::Exception::G
  __PACKAGE__->mk_classdata($_)
    for qw/components arguments dispatcher engine log dispatcher_class
    engine_class context_class request_class response_class stats_class
 -  setup_finished/;
 +  setup_finished _psgi_app/;
  
  __PACKAGE__->dispatcher_class('Catalyst::Dispatcher');
 -__PACKAGE__->engine_class('Catalyst::Engine::CGI');
 +__PACKAGE__->engine_class('Catalyst::Engine');
  __PACKAGE__->request_class('Catalyst::Request');
  __PACKAGE__->response_class('Catalyst::Response');
  __PACKAGE__->stats_class('Catalyst::Stats');
  
  # Remember to update this in Catalyst::Runtime as well!
  
- our $VERSION = '5.80029';
+ our $VERSION = '5.80030';
  
  sub import {
      my ( $class, @arguments ) = @_;
@@@ -1116,10 -1114,7 +1116,10 @@@ sub setup 
      $class->setup_log( delete $flags->{log} );
      $class->setup_plugins( delete $flags->{plugins} );
      $class->setup_dispatcher( delete $flags->{dispatcher} );
 -    $class->setup_engine( delete $flags->{engine} );
 +    if (my $engine = delete $flags->{engine}) {
 +        $class->log->warn("Specifying the engine in ->setup is no longer supported, XXX FIXME");
 +    }
 +    $class->setup_engine();
      $class->setup_stats( delete $flags->{stats} );
  
      for my $flag ( sort keys %{$flags} ) {
@@@ -1862,7 -1857,7 +1862,7 @@@ sub finalize_headers 
      }
  
      # Content-Length
-     if ( $response->body && !$response->content_length ) {
+     if ( defined $response->body && length $response->body && !$response->content_length ) {
  
          # get the length from a filehandle
          if ( blessed( $response->body ) && $response->body->can('read') || ref( $response->body ) eq 'GLOB' )
@@@ -1943,7 -1938,7 +1943,7 @@@ sub handle_request 
  
      # Always expect worst case!
      my $status = -1;
 -    eval {
 +    try {
          if ($class->debug) {
              my $secs = time - $START || 1;
              my $av = sprintf '%.3f', $COUNT / $secs;
          my $c = $class->prepare(@arguments);
          $c->dispatch;
          $status = $c->finalize;
 -    };
 -
 -    if ( my $error = $@ ) {
 -        chomp $error;
 -        $class->log->error(qq/Caught exception in engine "$error"/);
      }
 +    catch {
 +        chomp(my $error = $_);
 +        $class->log->error(qq/Caught exception in engine "$error"/);
 +    };
  
      $COUNT++;
  
@@@ -1995,38 -1991,28 +1995,38 @@@ sub prepare 
          $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION );
      }
  
 -    #XXX reuse coderef from can
 -    # Allow engine to direct the prepare flow (for POE)
 -    if ( $c->engine->can('prepare') ) {
 -        $c->engine->prepare( $c, @arguments );
 -    }
 -    else {
 -        $c->prepare_request(@arguments);
 -        $c->prepare_connection;
 -        $c->prepare_query_parameters;
 -        $c->prepare_headers;
 -        $c->prepare_cookies;
 -        $c->prepare_path;
 -
 -        # Prepare the body for reading, either by prepare_body
 -        # or the user, if they are using $c->read
 -        $c->prepare_read;
 -
 -        # Parse the body unless the user wants it on-demand
 -        unless ( ref($c)->config->{parse_on_demand} ) {
 -            $c->prepare_body;
 +    try {
 +        # Allow engine to direct the prepare flow (for POE)
 +        if ( my $prepare = $c->engine->can('prepare') ) {
 +            $c->engine->$prepare( $c, @arguments );
 +        }
 +        else {
 +            $c->prepare_request(@arguments);
 +            $c->prepare_connection;
 +            $c->prepare_query_parameters;
 +            $c->prepare_headers;
 +            $c->prepare_cookies;
 +            $c->prepare_path;
 +
 +            # Prepare the body for reading, either by prepare_body
 +            # or the user, if they are using $c->read
 +            $c->prepare_read;
 +
 +            # Parse the body unless the user wants it on-demand
 +            unless ( ref($c)->config->{parse_on_demand} ) {
 +                $c->prepare_body;
 +            }
          }
      }
 +    # VERY ugly and probably shouldn't rely on ->finalize actually working
 +    catch {
 +        # failed prepare is always due to an invalid request, right?
 +        $c->response->status(400);
 +        $c->response->content_type('text/plain');
 +        $c->response->body('Bad Request');
 +        $c->finalize;
 +        die $_;
 +    };
  
      my $method  = $c->req->method  || '';
      my $path    = $c->req->path;
@@@ -2376,11 -2362,11 +2376,11 @@@ sub prepare_write { my $c = shift; $c->
  
  =head2 $c->request_class
  
- Returns or sets the request class.
+ Returns or sets the request class. Defaults to L<Catalyst::Request>.
  
  =head2 $c->response_class
  
- Returns or sets the response class.
+ Returns or sets the response class. Defaults to L<Catalyst::Response>.
  
  =head2 $c->read( [$maxlength] )
  
@@@ -2405,7 -2391,7 +2405,7 @@@ Starts the engine
  
  =cut
  
 -sub run { my $c = shift; return $c->engine->run( $c, @_ ) }
 +sub run { my $c = shift; return $c->engine->run( $c, $c->psgi_app, @_ ) }
  
  =head2 $c->set_action( $action, $code, $namespace, $attrs )
  
@@@ -2590,100 -2576,113 +2590,100 @@@ Sets up engine
  =cut
  
  sub setup_engine {
 -    my ( $class, $engine ) = @_;
 +    my ($class) = @_;
  
 -    if ($engine) {
 -        $engine = 'Catalyst::Engine::' . $engine;
 -    }
 +    my $engine = $class->engine_class;
 +    Class::MOP::load_class($engine);
  
 -    if ( my $env = Catalyst::Utils::env_value( $class, 'ENGINE' ) ) {
 -        $engine = 'Catalyst::Engine::' . $env;
 +    if ($ENV{MOD_PERL}) {
 +        require 'Catalyst/Engine/Loader.pm';
 +        my $apache = Catalyst::Engine::Loader->auto;
 +        # FIXME - Immutable
 +        $class->meta->add_method(handler => sub {
 +            my $r = shift;
 +            my $app = $class->psgi_app;
 +            $apache->call_app($r, $app);
 +        });
      }
  
 -    if ( $ENV{MOD_PERL} ) {
 -        my $meta = Class::MOP::get_metaclass_by_name($class);
 +    $class->engine( $engine->new );
  
 -        # create the apache method
 -        $meta->add_method('apache' => sub { shift->engine->apache });
 +    return;
 +}
  
 -        my ( $software, $version ) =
 -          $ENV{MOD_PERL} =~ /^(\S+)\/(\d+(?:[\.\_]\d+)+)/;
 +=head2 $c->psgi_app
  
 -        $version =~ s/_//g;
 -        $version =~ s/(\.[^.]+)\./$1/g;
 +Builds a PSGI application coderef for the catalyst application C<$c> using
 +L</"$c->setup_psgi_app">, stores it internally, and returns it. On the next call
 +to this method, C<setup_psgi_app> won't be invoked again, but its persisted
 +return value of it will be returned.
  
 -        if ( $software eq 'mod_perl' ) {
 +This is the top-level entrypoint for things that need a full blown Catalyst PSGI
 +app. If you only need the raw PSGI application, without any middlewares, use
 +L</"$c->raw_psgi_app"> instead.
  
 -            if ( !$engine ) {
 +=cut
  
 -                if ( $version >= 1.99922 ) {
 -                    $engine = 'Catalyst::Engine::Apache2::MP20';
 -                }
 +sub psgi_app {
 +    my ($app) = @_;
  
 -                elsif ( $version >= 1.9901 ) {
 -                    $engine = 'Catalyst::Engine::Apache2::MP19';
 -                }
 +    unless ($app->_psgi_app) {
 +        my $psgi_app = $app->setup_psgi_app;
 +        $app->_psgi_app($psgi_app);
 +    }
  
 -                elsif ( $version >= 1.24 ) {
 -                    $engine = 'Catalyst::Engine::Apache::MP13';
 -                }
 +    return $app->_psgi_app;
 +}
  
 -                else {
 -                    Catalyst::Exception->throw( message =>
 -                          qq/Unsupported mod_perl version: $ENV{MOD_PERL}/ );
 -                }
 +=head2 $c->setup_psgi_app
  
 -            }
 +Builds a PSGI application coderef for the catalyst application C<$c>.
  
 -            # install the correct mod_perl handler
 -            if ( $version >= 1.9901 ) {
 -                *handler = sub  : method {
 -                    shift->handle_request(@_);
 -                };
 -            }
 -            else {
 -                *handler = sub ($$) { shift->handle_request(@_) };
 -            }
 -
 -        }
 +If we're able to locate a C<${myapp}.psgi> file in the applications home
 +directory, we'll use that to obtain our code reference.
  
 -        elsif ( $software eq 'Zeus-Perl' ) {
 -            $engine = 'Catalyst::Engine::Zeus';
 -        }
 +Otherwise the raw psgi app, without any middlewares is created using
 +C<raw_psgi_app> and wrapped into L<Plack::Middleware::ReverseProxy>
 +conditionally. See L</"PROXY SUPPORT">.
  
 -        else {
 -            Catalyst::Exception->throw(
 -                message => qq/Unsupported mod_perl: $ENV{MOD_PERL}/ );
 -        }
 -    }
 +=cut
  
 -    unless ($engine) {
 -        $engine = $class->engine_class;
 -    }
 +sub setup_psgi_app {
 +    my ($app) = @_;
  
 -    Class::MOP::load_class($engine);
 +    if (my $home = Path::Class::Dir->new($app->config->{home})) {
 +        my $psgi_file = $home->file(
 +            Catalyst::Utils::appprefix($app) . '.psgi',
 +        );
  
 -    # check for old engines that are no longer compatible
 -    my $old_engine;
 -    if ( $engine->isa('Catalyst::Engine::Apache')
 -        && !Catalyst::Engine::Apache->VERSION )
 -    {
 -        $old_engine = 1;
 +        return Plack::Util::load_psgi($psgi_file)
 +            if -e $psgi_file;
      }
  
 -    elsif ( $engine->isa('Catalyst::Engine::Server::Base')
 -        && Catalyst::Engine::Server->VERSION le '0.02' )
 -    {
 -        $old_engine = 1;
 -    }
 +    return Plack::Middleware::Conditional->wrap(
 +        $app->raw_psgi_app,
 +        builder   => sub { Plack::Middleware::ReverseProxy->wrap($_[0]) },
 +        condition => sub {
 +            my ($env) = @_;
 +            return if $app->config->{ignore_frontend_proxy};
 +            return $env->{REMOTE_ADDR} eq '127.0.0.1'
 +                || $app->config->{using_frontend_proxy};
 +        },
 +    );
 +}
  
 -    elsif ($engine->isa('Catalyst::Engine::HTTP::POE')
 -        && $engine->VERSION eq '0.01' )
 -    {
 -        $old_engine = 1;
 -    }
 +=head2 $c->raw_psgi_app
  
 -    elsif ($engine->isa('Catalyst::Engine::Zeus')
 -        && $engine->VERSION eq '0.01' )
 -    {
 -        $old_engine = 1;
 -    }
 +Returns a PSGI application code reference for the catalyst application
 +C<$c>. This is the bare application without any middlewares
 +applied. C<${myapp}.psgi> is not taken into account. See
 +L</"$c->setup_psgi_app">.
  
 -    if ($old_engine) {
 -        Catalyst::Exception->throw( message =>
 -              qq/Engine "$engine" is not supported by this version of Catalyst/
 -        );
 -    }
 +=cut
  
 -    # engine instance
 -    $class->engine( $engine->new );
 +sub raw_psgi_app {
 +    my ($app) = @_;
 +    return $app->engine->build_psgi_app($app);
  }
  
  =head2 $c->setup_home
diff --combined lib/Catalyst/Engine.pm
@@@ -10,17 -10,12 +10,17 @@@ use HTML::Entities
  use HTTP::Body;
  use HTTP::Headers;
  use URI::QueryParam;
 +use Moose::Util::TypeConstraints;
 +use Plack::Loader;
 +use Plack::Middleware::Conditional;
 +use Plack::Middleware::ReverseProxy;
 +use Catalyst::Engine::Loader;
  use Encode ();
  use utf8;
  
  use namespace::clean -except => 'meta';
  
 -has env => (is => 'rw');
 +has env => (is => 'ro', writer => '_set_env', clearer => '_clear_env');
  
  # input position and length
  has read_length => (is => 'rw');
@@@ -28,20 -23,6 +28,20 @@@ has read_position => (is => 'rw')
  
  has _prepared_write => (is => 'rw');
  
 +has _response_cb => (
 +    is      => 'ro',
 +    isa     => 'CodeRef',
 +    writer  => '_set_response_cb',
 +    clearer => '_clear_response_cb',
 +);
 +
 +has _writer => (
 +    is      => 'ro',
 +    isa     => duck_type([qw(write close)]),
 +    writer  => '_set_writer',
 +    clearer => '_clear_writer',
 +);
 +
  # Amount of data to read from input on each pass
  our $CHUNKSIZE = 64 * 1024;
  
@@@ -80,12 -61,6 +80,12 @@@ sub finalize_body 
      else {
          $self->write( $c, $body );
      }
 +
 +    $self->_writer->close;
 +    $self->_clear_writer;
 +    $self->_clear_env;
 +
 +    return;
  }
  
  =head2 $self->finalize_cookies($c)
@@@ -330,17 -305,7 +330,17 @@@ Abstract method, allows engines to writ
  
  =cut
  
 -sub finalize_headers { }
 +sub finalize_headers {
 +    my ($self, $ctx) = @_;
 +
 +    my @headers;
 +    $ctx->response->headers->scan(sub { push @headers, @_ });
 +
 +    $self->_set_writer($self->_response_cb->([ $ctx->response->status, \@headers ]));
 +    $self->_clear_response_cb;
 +
 +    return;
 +}
  
  =head2 $self->finalize_read($c)
  
@@@ -439,22 -404,7 +439,22 @@@ Abstract method implemented in engines
  
  =cut
  
 -sub prepare_connection { }
 +sub prepare_connection {
 +    my ($self, $ctx) = @_;
 +
 +    my $env = $self->env;
 +    my $request = $ctx->request;
 +
 +    $request->address( $env->{REMOTE_ADDR} );
 +    $request->hostname( $env->{REMOTE_HOST} )
 +        if exists $env->{REMOTE_HOST};
 +    $request->protocol( $env->{SERVER_PROTOCOL} );
 +    $request->remote_user( $env->{REMOTE_USER} );
 +    $request->method( $env->{REQUEST_METHOD} );
 +    $request->secure( $env->{'psgi.url_scheme'} eq 'https' ? 1 : 0 );
 +
 +    return;
 +}
  
  =head2 $self->prepare_cookies($c)
  
@@@ -474,19 -424,7 +474,19 @@@ sub prepare_cookies 
  
  =cut
  
 -sub prepare_headers { }
 +sub prepare_headers {
 +    my ($self, $ctx) = @_;
 +
 +    my $env = $self->env;
 +    my $headers = $ctx->request->headers;
 +
 +    for my $header (keys %{ $env }) {
 +        next unless $header =~ /^(HTTP|CONTENT|COOKIE)/i;
 +        (my $field = $header) =~ s/^HTTPS?_//;
 +        $field =~ tr/_/-/;
 +        $headers->header($field => $env->{$header});
 +    }
 +}
  
  =head2 $self->prepare_parameters($c)
  
@@@ -524,61 -462,7 +524,61 @@@ abstract method, implemented by engines
  
  =cut
  
 -sub prepare_path { }
 +sub prepare_path {
 +    my ($self, $ctx) = @_;
 +
 +    my $env = $self->env;
 +
 +    my $scheme    = $ctx->request->secure ? 'https' : 'http';
 +    my $host      = $env->{HTTP_HOST} || $env->{SERVER_NAME};
 +    my $port      = $env->{SERVER_PORT} || 80;
 +    my $base_path = $env->{SCRIPT_NAME} || "/";
 +
 +    # set the request URI
 +    my $path;
 +    if (!$ctx->config->{use_request_uri_for_path}) {
 +        my $path_info = $env->{PATH_INFO};
 +        if ( exists $env->{REDIRECT_URL} ) {
 +            $base_path = $env->{REDIRECT_URL};
 +            $base_path =~ s/\Q$path_info\E$//;
 +        }
 +        $path = $base_path . $path_info;
 +        $path =~ s{^/+}{};
 +        $path =~ s/([^$URI::uric])/$URI::Escape::escapes{$1}/go;
 +        $path =~ s/\?/%3F/g; # STUPID STUPID SPECIAL CASE
 +    }
 +    else {
 +        my $req_uri = $env->{REQUEST_URI};
 +        $req_uri =~ s/\?.*$//;
 +        $path = $req_uri;
 +        $path =~ s{^/+}{};
 +    }
 +
 +    # Using URI directly is way too slow, so we construct the URLs manually
 +    my $uri_class = "URI::$scheme";
 +
 +    # HTTP_HOST will include the port even if it's 80/443
 +    $host =~ s/:(?:80|443)$//;
 +
 +    if ($port !~ /^(?:80|443)$/ && $host !~ /:/) {
 +        $host .= ":$port";
 +    }
 +
 +    my $query = $env->{QUERY_STRING} ? '?' . $env->{QUERY_STRING} : '';
 +    my $uri   = $scheme . '://' . $host . '/' . $path . $query;
 +
 +    $ctx->request->uri( (bless \$uri, $uri_class)->canonical );
 +
 +    # set the base URI
 +    # base must end in a slash
 +    $base_path .= '/' unless $base_path =~ m{/$};
 +
 +    my $base_uri = $scheme . '://' . $host . $base_path;
 +
 +    $ctx->request->base( bless \$base_uri, $uri_class );
 +
 +    return;
 +}
  
  =head2 $self->prepare_request($c)
  
@@@ -589,11 -473,7 +589,11 @@@ process the query string and extract qu
  =cut
  
  sub prepare_query_parameters {
 -    my ( $self, $c, $query_string ) = @_;
 +    my ($self, $c) = @_;
 +
 +    my $query_string = exists $self->env->{QUERY_STRING}
 +        ? $self->env->{QUERY_STRING}
 +        : '';
  
      # Check for keywords (no = signs)
      # (yes, index() is faster than a regex :))
@@@ -655,10 -535,7 +655,10 @@@ Populate the context object from the re
  
  =cut
  
 -sub prepare_request { }
 +sub prepare_request {
 +    my ($self, $ctx, %args) = @_;
 +    $self->_set_env($args{env});
 +}
  
  =head2 $self->prepare_uploads($c)
  
@@@ -738,7 -615,7 +738,7 @@@ sub read 
      my $rc = $self->read_chunk( $c, my $buffer, $readlen );
      if ( defined $rc ) {
          if (0 == $rc) { # Nothing more to read even though Content-Length
 -                        # said there should be. FIXME - Warn in the log here?
 +                        # said there should be.
              $self->finalize_read;
              return;
          }
@@@ -759,10 -636,7 +759,10 @@@ there is no more data to be read
  
  =cut
  
 -sub read_chunk { }
 +sub read_chunk {
 +    my ($self, $ctx) = (shift, shift);
 +    return $self->env->{'psgi.input'}->read(@_);
 +}
  
  =head2 $self->read_length
  
@@@ -773,57 -647,13 +773,57 @@@ header
  
  The amount of input data that has already been read.
  
 -=head2 $self->run($c)
 +=head2 $self->run($app, $server)
 +
 +Start the engine. Builds a PSGI application and calls the
 +run method on the server passed in, which then causes the
 +engine to loop, handling requests..
 +
 +=cut
 +
 +sub run {
 +    my ($self, $app, $psgi, @args) = @_;
 +    # @args left here rather than just a $options, $server for back compat with the
 +    # old style scripts which send a few args, then a hashref
 +
 +    # They should never actually be used in the normal case as the Plack engine is
 +    # passed in got all the 'standard' args via the loader in the script already.
 +
 +    # FIXME - we should stash the options in an attribute so that custom args
 +    # like Gitalist's --git_dir are possible to get from the app without stupid tricks.
 +    my $server = pop @args if blessed $args[-1];
 +    my $options = pop @args if ref($args[-1]) eq 'HASH';
 +    if (! $server ) {
 +        $server = Catalyst::Engine::Loader->auto(); # We're not being called from a script,
 +                                                    # so auto detect what backend to run on.
 +                                                    # This should never happen, as mod_perl
 +                                                    # never calls ->run, instead the $app->handle
 +                                                    # method is called per request.
 +        $app->log->warn("Not supplied a Plack engine, falling back to engine auto-loader (are your scripts ancient?)")
 +    }
 +    $server->run($psgi, $options);
 +}
 +
 +=head2 build_psgi_app ($app, @args)
  
 -Start the engine. Implemented by the various engine classes.
 +Builds and returns a PSGI application closure, wrapping it in the reverse proxy
 +middleware if the using_frontend_proxy config setting is set.
  
  =cut
  
 -sub run { }
 +sub build_psgi_app {
 +    my ($self, $app, @args) = @_;
 +
 +    return sub {
 +        my ($env) = @_;
 +
 +        return sub {
 +            my ($respond) = @_;
 +            $self->_set_response_cb($respond);
 +            $app->handle_request(env => $env);
 +        };
 +    };
 +}
  
  =head2 $self->write($c, $buffer)
  
@@@ -839,12 -669,33 +839,12 @@@ sub write 
          $self->_prepared_write(1);
      }
  
--    return 0 if !defined $buffer;
 -
 -    my $len   = length($buffer);
 -    my $wrote = syswrite STDOUT, $buffer;
 -
 -    if ( !defined $wrote && $! == EWOULDBLOCK ) {
 -        # Unable to write on the first try, will retry in the loop below
 -        $wrote = 0;
 -    }
 -
 -    if ( defined $wrote && $wrote < $len ) {
 -        # We didn't write the whole buffer
 -        while (1) {
 -            my $ret = syswrite STDOUT, $buffer, $CHUNKSIZE, $wrote;
 -            if ( defined $ret ) {
 -                $wrote += $ret;
 -            }
 -            else {
 -                next if $! == EWOULDBLOCK;
 -                return;
 -            }
++    $buffer = q[] unless defined $buffer;
  
 -            last if $wrote >= $len;
 -        }
 -    }
 +    my $len = length($buffer);
 +    $self->_writer->write($buffer);
  
 -    return $wrote;
 +    return $len;
  }
  
  =head2 $self->unescape_uri($uri)