From: John Napiorkowski Date: Tue, 2 Dec 2014 15:16:04 +0000 (-0600) Subject: proxy object for the PSGI writer X-Git-Tag: 5.90079_001~5 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=catagits%2FCatalyst-Runtime.git;a=commitdiff_plain;h=e8361cf8fc1d23adf4a14a81726477c48a80449e proxy object for the PSGI writer --- diff --git a/Changes b/Changes index 619653c..066fc25 100644 --- a/Changes +++ b/Changes @@ -20,10 +20,14 @@ - lots of UTF8 changes. Again we think this is now more correct but please test. - Allow $c->res->redirect($url) to accept $url as an object that does ->as_string which I think will ease a common case (and common bug) and added documentation. - - UTF-8 is now the default encoding (there used to be none...). You can disable + - !!! UTF-8 is now the default encoding (there used to be none...). You can disable this if you need to with MyApp->config(encoding => undef) if it causes you trouble. - Calling $c->res->write($data) now encodes $data based on the configured encoding (UTF-8 is default). + - $c->res->writer_fh now returns Catalyst::Response::Writer which is a decorator + over the PSGI writer and provides and additional methd 'write_encoded' that just + does the right thing for encoding your responses. This is probably the method + you want to use. 5.90077 - 2014-11-18 - We store the PSGI $env in Catalyst::Engine for backcompat reasons. Changed diff --git a/lib/Catalyst.pm b/lib/Catalyst.pm index 141099c..9153f82 100644 --- a/lib/Catalyst.pm +++ b/lib/Catalyst.pm @@ -86,8 +86,10 @@ has response => ( lazy => 1, ); sub _build_response_constructor_args { - my $self = shift; - { _log => $self->log }; + return +{ + _log => $_[0]->log, + encoding => $_[0]->encoding, + }; } has namespace => (is => 'rw'); diff --git a/lib/Catalyst/Response.pm b/lib/Catalyst/Response.pm index bf4ef44..82677a7 100644 --- a/lib/Catalyst/Response.pm +++ b/lib/Catalyst/Response.pm @@ -5,6 +5,7 @@ use HTTP::Headers; use Moose::Util::TypeConstraints; use namespace::autoclean; use Scalar::Util 'blessed'; +use Catalyst::Response::Writer; with 'MooseX::Emulate::Class::Accessor::Fast'; @@ -52,7 +53,17 @@ has write_fh => ( builder=>'_build_write_fh', ); -sub _build_write_fh { shift ->_writer } +sub _build_write_fh { + my $writer = $_[0]->_writer; # We need to get the finalize headers side effect... + my $requires_encoding = $_[0]->content_type =~ m/^text|xml$|javascript$/; + my %fields = ( + _writer => $writer, + _encoding => $_[0]->encoding, + _requires_encoding => $requires_encoding, + ); + + return bless \%fields, 'Catalyst::Response::Writer'; +} sub DEMOLISH { my $self = shift; @@ -83,6 +94,8 @@ has _context => ( clearer => '_clear_context', ); +has encoding => (is=>'ro'); + before [qw(status headers content_encoding content_length content_type header)] => sub { my $self = shift; @@ -411,9 +424,22 @@ encoding is UTF-8). =head2 $res->write_fh -Returns a PSGI $writer object that has two methods, write and close. You can -close over this object for asynchronous and nonblocking applications. For -example (assuming you are using a supporting server, like L +Returns an instance of L, which is a lightweight +decorator over the PSGI C<$writer> object (see L). + +In addition to proxying the C and C method from the underlying PSGI +writer, this proxy object knows any application wide encoding, and provides a method +C that will properly encode your written lines based upon your +encoding settings. By default in L responses are UTF-8 encoded and this +is the encoding used if you respond via C. If you want to handle +encoding yourself, you can use the C method directly. + +Encoding only applies to content types for which it matters. Currently the following +content types are assumed to need encoding: text (including HTML), xml and javascript. + +We provide access to this object so that you can properly close over it for use in +asynchronous and nonblocking applications. For example (assuming you are using a supporting +server, like L: package AsyncExample::Controller::Root; diff --git a/lib/Catalyst/Response/Writer.pm b/lib/Catalyst/Response/Writer.pm new file mode 100644 index 0000000..55cbdd1 --- /dev/null +++ b/lib/Catalyst/Response/Writer.pm @@ -0,0 +1,65 @@ +package Catalyst::Response::Writer; + +sub write { shift->{_writer}->write(@_) } +sub close { shift->{_writer}->close } + +sub write_encoded { + my ($self, $line) = @_; + if((my $enc = $self->{_encoding}) && $self->{_requires_encoding}) { + # Not going to worry about CHECK arg since Unicode always croaks I think - jnap + $line = $enc->encode($line); + } + + $self->write($line); +} + +=head1 TITLE + +Catalyst::Response::Writer - Proxy over the PSGI Writer + +=head1 SYNOPSIS + + sub myaction : Path { + my ($self, $c) = @_; + my $w = $c->response->writer_fh; + + $w->write("hello world"); + $w->close; + } + +=head1 DESCRIPTION + +This wraps the PSGI writer (see L) +for more. We wrap this object so we can provide some additional methods that +make sense from inside L + +=head1 METHODS + +This class does the following methods + +=head2 write + +=head2 close + +These delegate to the underlying L writer object + +=head2 write_encoded + +If the application defines a response encoding (default is UTF8) and the +content type is a type that needs to be encoded (text types like HTML or XML and +Javascript) we first encode the line you want to write. This is probably the +thing you want to always do. If you use the L<\write> method directly you will +need to handle your own encoding. + +=head1 AUTHORS + +Catalyst Contributors, see Catalyst.pm + +=head1 COPYRIGHT + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +1; diff --git a/t/utf_incoming.t b/t/utf_incoming.t index 8966d56..fef3553 100644 --- a/t/utf_incoming.t +++ b/t/utf_incoming.t @@ -81,20 +81,32 @@ use File::Spec; $c->response->content_type('text/html'); my $writer = $c->res->write_fh; - - $writer->write(Encode::encode_utf8('

This is stream_write_fh action ♥

')); + $writer->write_encoded('

This is stream_write_fh action ♥

'); $writer->close; } + # Stream a file with utf8 chars directly, you don't need to decode sub stream_body_fh :Local { my ($self, $c) = @_; - my $path = File::Spec->catfile('t', 'utf8.txt'); open(my $fh, '<', $path) || die "trouble: $!"; $c->response->content_type('text/html'); $c->response->body($fh); } + # If you pull the file contents into a var, NOW you need to specify the + # IO encoding on the FH. Ultimately Plack at the end wants bytes... + sub stream_body_fh2 :Local { + my ($self, $c) = @_; + my $path = File::Spec->catfile('t', 'utf8.txt'); + open(my $fh, '<:encoding(UTF-8)', $path) || die "trouble: $!"; + my $contents = do { local $/; <$fh> }; + + $c->response->content_type('text/html'); + $c->response->body($contents); + } + + package MyApp; use Catalyst; @@ -242,4 +254,13 @@ use Catalyst::Test 'MyApp'; is $res->content_charset, 'UTF-8'; } +{ + my $res = request "/root/stream_body_fh2"; + + is $res->code, 200, 'OK'; + is decode_utf8($res->content), "

This is stream_body_fh action ♥

\n", 'correct body'; + is $res->content_length, 41, 'correct length'; + is $res->content_charset, 'UTF-8'; +} + done_testing;