use Moose::Util::TypeConstraints;
use namespace::autoclean;
use Scalar::Util 'blessed';
+use Catalyst::Response::Writer;
+use Catalyst::Utils ();
with 'MooseX::Emulate::Class::Accessor::Fast';
+our $DEFAULT_ENCODE_CONTENT_TYPE_MATCH = qr{text|xml$|javascript$};
+
+has encodable_content_type => (
+ is => 'rw',
+ required => 1,
+ default => sub { $DEFAULT_ENCODE_CONTENT_TYPE_MATCH }
+);
+
has _response_cb => (
is => 'ro',
isa => 'CodeRef',
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]->encodable_response;
+ my %fields = (
+ _writer => $writer,
+ _context => $_[0]->_context,
+ _requires_encoding => $requires_encoding,
+ );
+
+ return bless \%fields, 'Catalyst::Response::Writer';
+}
sub DEMOLISH {
my $self = shift;
has headers => (
is => 'rw',
isa => 'HTTP::Headers',
- handles => [qw(content_encoding content_length content_type header)],
+ handles => [qw(content_encoding content_length content_type content_type_charset header)],
default => sub { HTTP::Headers->new() },
required => 1,
lazy => 1,
my $self = shift;
$self->_context->log->warn(
- "Useless setting a header value after finalize_headers called." .
+ "Useless setting a header value after finalize_headers and the response callback has been called." .
" Not what you want." )
- if ( $self->finalized_headers && @_ );
+ if ( $self->_context && $self->finalized_headers && !$self->_has_response_cb && @_ );
};
sub output { shift->body(@_) }
$buffer = q[] unless defined $buffer;
- $buffer = $self->_context->encoding->encode( $buffer, $self->_context->_encode_check )
- if $self->_context->encoding && $self->content_type =~ /^text|xml$|javascript$/;
+ if($self->encodable_response) {
+ $buffer = $self->_context->encoding->encode( $buffer, $self->_context->_encode_check )
+ }
my $len = length($buffer);
$self->_writer->write($buffer);
my ($status, $headers, $body) = @$psgi_res;
$self->status($status);
$self->headers(HTTP::Headers->new(@$headers));
- $self->body($body);
+ # Can be arrayref or filehandle...
+ if(defined $body) { # probably paranoia
+ ref $body eq 'ARRAY' ? $self->body(join('', @$body)) : $self->body($body);
+ }
} elsif(ref $psgi_res eq 'CODE') {
$psgi_res->(sub {
my $response = shift;
$self->status($status);
$self->headers(HTTP::Headers->new(@$headers));
if(defined $maybe_body) {
- $self->body($maybe_body);
+ # Can be arrayref or filehandle...
+ ref $maybe_body eq 'ARRAY' ? $self->body(join('', @$maybe_body)) : $self->body($maybe_body);
} else {
return $self->write_fh;
}
} else {
die "You can't set a Catalyst response from that, expect a valid PSGI response";
}
+
+ # Encoding compatibilty. If the response set a charset, well... we need
+ # to assume its properly encoded and NOT encode for this response. Otherwise
+ # We risk double encoding.
+ if($self->content_type_charset) {
+ # We have to do this since for backcompat reasons having a charset doesn't always
+ # mean that the body is already encoded :(
+ $self->_context->clear_encoding;
+ }
}
=head1 NAME
$c->response->body('Catalyst rocks!');
Sets or returns the output (text or binary data). If you are returning a large body,
-you might want to use a L<IO::Handle> type of object (Something that implements the read method
-in the same fashion), or a filehandle GLOB. Catalyst
-will write it piece by piece into the response.
+you might want to use a L<IO::Handle> type of object (Something that implements the getline method
+in the same fashion), or a filehandle GLOB. These will be passed down to the PSGI
+handler you are using and might be optimized using server specific abilities (for
+example L<Twiggy> will attempt to server a real local file in a non blocking manner).
+
+If you are using a filehandle as the body response you are responsible for
+making sure it conforms to the L<PSGI> specification with regards to content
+encoding. Unlike with scalar body values or when using the streaming interfaces
+we currently do not attempt to normalize and encode your filehandle. In general
+this means you should be sure to be sending bytes not UTF8 decoded multibyte
+characters.
+
+Most of the time when you do:
+
+ open(my $fh, '<:raw', $path);
+
+You should be fine. If you open a filehandle with a L<PerlIO> layer you probably
+are not fine. You can usually fix this by explicitly using binmode to set
+the IOLayer to :raw. Its possible future versions of L<Catalyst> will try to
+'do the right thing'.
When using a L<IO::Handle> type of object and no content length has been
already set in the response headers Catalyst will make a reasonable attempt
yourself, which will be respected and sent by Catalyst in the response.
Please note that the object needs to implement C<getline>, not just
-C<read>.
+C<read>. Older versions of L<Catalyst> expected your filehandle like objects
+to do read. If you have code written for this expectation and you cannot
+change the code to meet the L<PSGI> specification, you can try the following
+middleware L<Plack::Middleware::AdaptFilehandleRead> which will attempt to
+wrap your object in an interface that so conforms.
Starting from version 5.90060, when using an L<IO::Handle> object, you
may want to use L<Plack::Middleware::XSendfile>, to delegate the
L<Catalyst::Plugin::Static::Simple> will guess the mime type based on the file
it found, while L<Catalyst::View::TT> defaults to C<text/html>.
+=head2 $res->content_type_charset
+
+Shortcut for $res->headers->content_type_charset;
+
=head2 $res->cookies
Returns a reference to a hash containing cookies to be set. The keys of the
=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<Twiggy>
+Returns an instance of L<Catalyst::Response::Writer>, which is a lightweight
+decorator over the PSGI C<$writer> object (see L<PSGI.pod\Delayed-Response-and-Streaming-Body>).
+
+In addition to proxying the C<write> and C<close> method from the underlying PSGI
+writer, this proxy object knows any application wide encoding, and provides a method
+C<write_encoded> that will properly encode your written lines based upon your
+encoding settings. By default in L<Catalyst> responses are UTF-8 encoded and this
+is the encoding used if you respond via C<write_encoded>. If you want to handle
+encoding yourself, you can use the C<write> 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<Twiggy>:
package AsyncExample::Controller::Root;
}
Please note this does not attempt to map or nest your PSGI application under
-the Controller and Action namespace or path.
+the Controller and Action namespace or path. You may wish to review 'PSGI Helpers'
+under L<Catalyst::Utils> for help in properly nesting applications.
+
+B<NOTE> If your external PSGI application returns a response that has a character
+set associated with the content type (such as "text/html; charset=UTF-8") we set
+$c->clear_encoding to remove any additional content type encoding processing later
+in the application (this is done to avoid double encoding issues).
+
+=head2 encodable_content_type
+
+This is a regular expression used to determine of the current content type
+should be considered encodable. Currently we apply default encoding (usually
+UTF8) to text type contents. Here's the default regular expression:
+
+This would match content types like:
+
+ text/plain
+ text/html
+ text/xml
+ application/javascript
+ application/xml
+ application/vnd.user+xml
+
+B<NOTE>: We don't encode JSON content type responses by default since most
+of the JSON serializers that are commonly used for this task will do so
+automatically and we don't want to double encode. If you are not using a
+tool like L<JSON> to produce JSON type content, (for example you are using
+a template system, or creating the strings manually) you will need to either
+encoding the body yourself:
+
+ $c->response->body( $c->encoding->encode( $body, $c->_encode_check ) );
+
+Or you can alter the regular expression using this attribute.
+
+=head2 encodable_response
+
+Given a L<Catalyst::Response> return true if its one that can be encoded.
+
+ make sure there is an encoding set on the response
+ make sure the content type is encodable
+ make sure no content type charset has been already set to something different from the global encoding
+ make sure no content encoding is present.
+
+Note this does not inspect a body since we do allow automatic encoding on streaming
+type responses.
+
+=cut
+
+sub encodable_response {
+ my ($self) = @_;
+ return 0 unless $self->_context; # Cases like returning a HTTP Exception response you don't have a context here...
+ return 0 unless $self->_context->encoding;
+
+ # The response is considered to have a 'manual charset' when a charset is already set on
+ # the content type of the response AND it is not the same as the one we set in encoding.
+ # If there is no charset OR we are asking for the one which is the same as the current
+ # required encoding, that is a flag that we want Catalyst to encode the response automatically.
+ my $has_manual_charset = 0;
+ if(my $charset = $self->content_type_charset) {
+ $has_manual_charset = (uc($charset) ne uc($self->_context->encoding->mime_name)) ? 1:0;
+ }
+
+ # Content type is encodable if it matches the regular expression stored in this attribute
+ my $encodable_content_type = $self->content_type =~ m/${\$self->encodable_content_type}/ ? 1:0;
+
+ # The content encoding is allowed (for charset encoding) only if its empty or is set to identity
+ my $allowed_content_encoding = (!$self->content_encoding || $self->content_encoding eq 'identity') ? 1:0;
+
+ # The content type must be an encodable type, and there must be NO manual charset and also
+ # the content encoding must be the allowed values;
+ if(
+ $encodable_content_type and
+ !$has_manual_charset and
+ $allowed_content_encoding
+ ) {
+ return 1;
+ } else {
+ return 0;
+ }
+}
=head2 DEMOLISH