Skip Menu |

This queue is for tickets about the Catalyst-Runtime CPAN distribution.

Report information
The Basics
Id: 78377
Status: resolved
Priority: 0/
Queue: Catalyst-Runtime

People
Owner: bobtfish [...] bobtfish.net
Requestors: KnowZeroX [...] yahoo.com
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in: 5.90015
Fixed in: (no value)



Subject: Content-length error fix
Ok, so I think I finished investigating the issue with the content length. And here are the results of my investigation plus a patch. Effectively this issue happens when the web server itself issued a Document Has Moved and Catalyst issues a Document Has Moved on a redirect while Keep-Alive is active.(From my understanding keep-alive was deprecated as of HTTP 1.1 but some browsers still use HTTP 1.0 to pre-load content on page and achieve more connections). So effectively what happens is catalyst calculates the content-length of its content not taking into account that the web server such as IIS appends its own content prior. This makes the content-length incorrect and cut off the buffer at the wrong point on the redirect page. Once redirected due to keep-alive the previous buffer that has been cut off still remains and again gives you the wrong content-length on the landing page. Anyways, the solution I propose is to add a config option of "disable_redirect_html" that will disable catalyst from writing its own HTML when the body is missing on redirects and lets the server handle the document has moved. Line 1856 - if ( !$response->has_body ) { to Line 1856 + if ( !$response->has_body and !$c->config-> {disable_redirect_html} ) { What do you think?
On Sat Jul 14 18:46:59 2012, KnowZeroX@yahoo.com wrote: Show quoted text
> Ok, so I think I finished investigating the issue with the content > length. And here are the results of my investigation plus a patch. > > Effectively this issue happens when the web server itself issued a > Document Has Moved and Catalyst issues a Document Has Moved on a > redirect while Keep-Alive is active.(From my understanding keep-alive > was deprecated as of HTTP 1.1 but some browsers still use HTTP 1.0 to > pre-load content on page and achieve more connections).
Keepalive is not deprecated? What implies this? Show quoted text
> So effectively what happens is catalyst calculates the content-length of > its content not taking into account that the web server such as IIS > appends its own content prior. This makes the content-length incorrect > and cut off the buffer at the wrong point on the redirect page. Once > redirected due to keep-alive the previous buffer that has been cut off > still remains and again gives you the wrong content-length on the > landing page.
So, what you're basically suggesting here is that IIS is broken? Show quoted text
> Anyways, the solution I propose is to add a config option of > "disable_redirect_html" that will disable catalyst from writing its own > HTML when the body is missing on redirects and lets the server handle > the document has moved. > > Line 1856 - > if ( !$response->has_body ) { > > to > > Line 1856 + > > if ( !$response->has_body and !$c->config-> > {disable_redirect_html} ) { > > What do you think?
This, however, does sound like an entirely reasonable way to fix the issue, if this is needed. I'm wondering, however, if this should instead be implmented as PSGI middleware - we already have server detection code, which applies middleware based upon the server you're running, and therefore this would allow you to now even have to configure this setting...
From: KnowZeroX [...] yahoo.com
Show quoted text
> This, however, does sound like an entirely reasonable way to fix the > issue, if this is needed. > > I'm wondering, however, if this should instead be implmented as PSGI > middleware - we already have server detection code, which applies > middleware based upon the server you're running, and therefore this > would allow you to now even have to configure this setting...
You mean make this setting default on IIS? Sure that would make sense. The user would still be able to overwrite it if need be via config right? Also you mentioned this issue effects other servers as well correct?
From: KnowZeroX [...] yahoo.com
Show quoted text
> Keepalive is not deprecated? What implies this?
From my understanding HTTP 1.1 sets up a persistent connection by default unless you send a Connection: close to specifically tell it not to. It is documented in rfc 2616 and rfc2068 That said from my understanding some modern browsers utilize HTTP 1.0 to achieve better preloading of content. Show quoted text
> So, what you're basically suggesting here is that IIS is broken?
What happens is IIS injects its own HTML on redirect once it sees the Location header. With keep-alive off it is not a problem but once keep- alive is enabled it cuts off buffer due to incorrect content-length. This causes IE9 to hang when dealing with large urls and causes firefox to show chunks of fragmented buffer.
On Sun Jul 15 07:36:16 2012, KnowZeroX@yahoo.com wrote: Show quoted text
> > Keepalive is not deprecated? What implies this?
> > From my understanding HTTP 1.1 sets up a persistent connection by > default unless you send a Connection: close to specifically tell it not > to. > > It is documented in rfc 2616 and rfc2068
Right, so keep alive is the default - which is entirely different to it being deprecated... Show quoted text
> That said from my understanding some modern browsers utilize HTTP 1.0 to > achieve better preloading of content.
eh, this doesn't make any sense? Show quoted text
> > So, what you're basically suggesting here is that IIS is broken?
> > What happens is IIS injects its own HTML on redirect once it sees the > Location header. With keep-alive off it is not a problem but once keep- > alive is enabled it cuts off buffer due to incorrect content-length.
Right, and this is as IIS is broken! Ignoring a large chunk of the response from the app is just totally broken behavior.
On Sun Jul 15 07:11:16 2012, KnowZeroX@yahoo.com wrote: Show quoted text
> You mean make this setting default on IIS? Sure that would make sense.
No, I mean not have a setting at all. And have the code in a middleware. And have that middleware auto-applied in the default set of middlewares if needed. Show quoted text
> The user would still be able to overwrite it if need be via config > right?
They could override it by not applying the default middlewares. Show quoted text
> Also you mentioned this issue effects other servers as well correct?
I have seen an issue I thought was possibly this with other servers, but the fact it's only on redirect implies (to me at least) that you're seeing an IIS specific issue here.
From: KnowZeroX [...] yahoo.com
On Sun Jul 15 07:49:52 2012, BOBTFISH wrote: Show quoted text
> On Sun Jul 15 07:11:16 2012, KnowZeroX@yahoo.com wrote:
> > You mean make this setting default on IIS? Sure that would make
sense. Show quoted text
> > No, I mean not have a setting at all. > > And have the code in a middleware. > > And have that middleware auto-applied in the default set of
middlewares Show quoted text
> if needed. >
> > The user would still be able to overwrite it if need be via config > > right?
> > They could override it by not applying the default middlewares. >
> > Also you mentioned this issue effects other servers as well correct?
> > I have seen an issue I thought was possibly this with other servers,
but Show quoted text
> the fact it's only on redirect implies (to me at least) that you're > seeing an IIS specific issue here. >
Ok, I took a look at IIS and apparently its being a little deceiving :/. By disabling Keep-Alive setting in IIS it effectively just sends a "Connection: close" back which effectively disables persistent connection from the client. In light of this, setting it up as a default middleware makes most sense. Well it effects both the redirect and the page following the redirect. So theoretically speaking it is possible for it to effect pages after the redirect of say an image, css or JavaScript under certain conditions.
From: KnowZeroX [...] yahoo.com
Show quoted text
> Right, so keep alive is the default - which is entirely different to
it Show quoted text
> being deprecated...
I meant to say the keep-alive setting aka Connection: Keep-Alive but yeah don't mind me I have been looking all night for the issue so lack of sleep is having me hickup here and there. Show quoted text
> eh, this doesn't make any sense?
I thought so too. But that is what I saw reading around. Though looking at it more closely it seems it was like that on winxp or something like that due to limitations of 2 http 1.1 connections on IE which was fixed in later browsers it seems. So you are right. Show quoted text
> Ignoring a large chunk of the response from the app is just totally > broken behavior.
I guess yeah, IIS persistent connections are broken. Anyways I am going to go to sleep because I am not thinking straight. Will you be able to patch the middleware? or should I write it and attach it?
Subject: Re: [rt.cpan.org #78377] Content-length error fix
Date: Sun, 15 Jul 2012 13:19:34 +0100
To: bug-Catalyst-Runtime [...] rt.cpan.org
From: Tomas Doran <bobtfish [...] bobtfish.net>
On 15 Jul 2012, at 13:15, via RT wrote: Show quoted text
> Anyways I am going to go to sleep because I am not thinking straight. > Will you be able to patch the middleware? or should I write it and > attach it?
I think we really want a separate / new middleware for this. And the chances of me getting time to get to it are low + I have no ability to test it I'm afraid! Cheers t0m
From: KnowZeroX [...] yahoo.com
On Sun Jul 15 08:19:38 2012, BOBTFISH wrote: Show quoted text
> > On 15 Jul 2012, at 13:15, via RT wrote: >
> > Anyways I am going to go to sleep because I am not thinking
> straight.
> > Will you be able to patch the middleware? or should I write it and > > attach it?
> > I think we really want a separate / new middleware for this. > > And the chances of me getting time to get to it are low + I have no > ability to test it I'm afraid! > > Cheers > t0m >
Alright then, I'll try to write one <strike>tomorrow</strike> today. After I wake up, at this point I don't trust myself to touch anything until I get some sleep lol
From: KnowZeroX [...] yahoo.com
Ok I am done. I decided to make two versions. IISKeepAliveFix.pm - The IIS keep alive fix which effectively on 30[123] response it removes Content-Length, Content-Type headers and sets the body content to undefined. and IISFix.pm - Includes both the fixed up version of IIS6ScriptNameFix and my IISKeepAliveFix in 1. Personally I think IISFix is the way to go but chose whichever you think is best.
Subject: IISKeepAliveFix.pm
package Plack::Middleware::IISKeepAliveFix; use strict; use parent 'Plack::Middleware'; use Plack::Util; sub call { my($self, $env) = @_; if ($env->{SERVER_SOFTWARE} && $env->{SERVER_SOFTWARE} =~ /IIS\/[6-9]\.[0-9]/) { # Fixes wrong SCRIPT_NAME and PATH_INFO that IIS sets my ($path) = ( $env->{REQUEST_URI} =~ /^([^?]*)(?:\?.*)?$/s ); $path =~ s/#.*$// if defined && length; # dumb clients sending URI fragments my @script_name = split(m!/!, URI::Escape::uri_unescape($path)); my @path_translated = split(m!/|\\\\?!, $env->{PATH_TRANSLATED}); my @path_info; while ($script_name[$#script_name] eq $path_translated[$#path_translated]) { pop(@path_translated); unshift(@path_info, pop(@script_name)); } unshift(@path_info, '', ''); $env->{PATH_INFO} = join('/', @path_info); $env->{SCRIPT_NAME} = join('/', @script_name); # Fixes buffer being cut off on redirect when keep-alive is active my $res = $self->app->($env); Plack::Util::response_cb($res, sub { my $res = shift; if ($res->[0] =~ m!^30[123]$! ) { Plack::Util::header_remove($res->[1], 'Content-Length'); Plack::Util::header_remove($res->[1], 'Content-Type'); $res->[2]=undef; } return; }); } } 1; __END__ =head1 NAME Plack::Middleware::IISKeepAliveFix - fixes buffer being cut off on redirect when keep-alive is active on IIS. =head1 SYNOPSIS # in your app.psgi use Plack::Builder; builder { enable "IISKeepAliveFix"; $app; }; # Or from the command line plackup -s FCGI -e 'enable "IISKeepAliveFix"' /path/to/app.psgi =head1 DESCRIPTION This middleware fixes buffer being cut off on redirect when keep-alive is active on IIS. =head1 AUTHORS Florian Ragwitz =cut
Subject: IISFix.pm
package Plack::Middleware::IISFix; use strict; use parent 'Plack::Middleware'; use Plack::Util; sub call { my($self, $env) = @_; if ($env->{SERVER_SOFTWARE} && $env->{SERVER_SOFTWARE} =~ /IIS\/[6-9]\.[0-9]/) { # Fixes wrong SCRIPT_NAME and PATH_INFO that IIS sets my ($path) = ( $env->{REQUEST_URI} =~ /^([^?]*)(?:\?.*)?$/s ); $path =~ s/#.*$// if defined && length; # dumb clients sending URI fragments my @script_name = split(m!/!, URI::Escape::uri_unescape($path)); my @path_translated = split(m!/|\\\\?!, $env->{PATH_TRANSLATED}); my @path_info; while ($script_name[$#script_name] eq $path_translated[$#path_translated]) { pop(@path_translated); unshift(@path_info, pop(@script_name)); } unshift(@path_info, '', ''); $env->{PATH_INFO} = join('/', @path_info); $env->{SCRIPT_NAME} = join('/', @script_name); # Fixes buffer being cut off on redirect when keep-alive is active my $res = $self->app->($env); Plack::Util::response_cb($res, sub { my $res = shift; if ($res->[0] =~ m!^30[123]$! ) { Plack::Util::header_remove($res->[1], 'Content-Length'); Plack::Util::header_remove($res->[1], 'Content-Type'); $res->[2]=undef; } return; }); } } 1; __END__ =head1 NAME Plack::Middleware::IISFix - fixes wrong SCRIPT_NAME and PATH_INFO that IIS sets and fixes buffer being cut off on redirect when keep-alive is active. =head1 SYNOPSIS # in your app.psgi use Plack::Builder; builder { enable "IISFix"; $app; }; # Or from the command line plackup -s FCGI -e 'enable "IISFix"' /path/to/app.psgi =head1 DESCRIPTION This middleware fixes wrong C<SCRIPT_NAME> and C<PATH_INFO> set by IIS and fixes buffer being cut off on redirect when keep-alive is active. =head1 AUTHORS Florian Ragwitz =cut
On Sun Jul 15 13:24:02 2012, KnowZeroX@yahoo.com wrote: Show quoted text
> Personally I think IISFix is the way to go but chose whichever you think > is best.
Fraid I disagree. We need to fix IISScriptNameFix in Plack core, as it's generic. However the other issue is due the the framework's default behavior, and therefore needs fixing in Catalyst. Anyway, thanks for the code!
On Sun Jul 15 13:24:02 2012, KnowZeroX@yahoo.com wrote: Show quoted text
> Ok I am done. I decided to make two versions. > > IISKeepAliveFix.pm - The IIS keep alive fix which effectively on 30[123] > response it removes Content-Length, Content-Type headers and sets the
body Show quoted text
> content to undefined.
looking inside IISKeepAliveFix, I see: # Fixes wrong SCRIPT_NAME and PATH_INFO that IIS sets my ($path) = ( $env->{REQUEST_URI} =~ /^([^?]*)(?:\?.*)?$/s ); $path =~ s/#.*$// if defined && length; # dumb clients sending URI fragments my @script_name = split(m!/!, URI::Escape::uri_unescape($path)); my @path_translated = split(m!/|\\\\?!, $env->{PATH_TRANSLATED}); my @path_info; which appears to be unexpectedly doing things to the request URI? Show quoted text
> and > > IISFix.pm - Includes both the fixed up version of IIS6ScriptNameFix
and my Show quoted text
> IISKeepAliveFix in 1.
and yes - this includes both. Can I get a middleware which does just the fix for this bug please? (And test cases would be really good, at least to let people who come later know what we're doing, and why....)
From: KnowZeroX [...] yahoo.com
Attaching the iiskeepalivefix.pm, sorry about that I by accident saved over iiskeepalivefix.pm the content of iisfix.pm. This is the right version. I will work on getting the test cases done now.
Subject: IISKeepAliveFix.pm
package Plack::Middleware::IISKeepAliveFix; use strict; use parent 'Plack::Middleware'; use Plack::Util; sub call { my($self, $env) = @_; if ($env->{SERVER_SOFTWARE} && $env->{SERVER_SOFTWARE} =~ /IIS\/[6-9]\.[0-9]/) { # Fixes buffer being cut off on redirect when keep-alive is active my $res = $self->app->($env); Plack::Util::response_cb($res, sub { my $res = shift; if ($res->[0] =~ m!^30[123]$! ) { Plack::Util::header_remove($res->[1], 'Content-Length'); Plack::Util::header_remove($res->[1], 'Content-Type'); $res->[2]=undef; } return; }); } } 1; __END__ =head1 NAME Plack::Middleware::IISKeepAliveFix - fixes buffer being cut off on redirect when keep-alive is active on IIS. =head1 SYNOPSIS # in your app.psgi use Plack::Builder; builder { enable "IISKeepAliveFix"; $app; }; # Or from the command line plackup -s FCGI -e 'enable "IISKeepAliveFix"' /path/to/app.psgi =head1 DESCRIPTION This middleware fixes buffer being cut off on redirect when keep-alive is active on IIS. =head1 AUTHORS Florian Ragwitz =cut
From: KnowZeroX [...] yahoo.com
Ok I finished with the test case, to make the test case work I had to make a few adjustments so I am including the final versions of everything attached here. iis_keep_alive_fix.t - test case IISKeepAliveFix.pm - middleware declare.txt - declaration
Subject: declare.txt
use Plack::Middleware::IISKeepAliveFix; $psgi_app = Plack::Middleware::Conditional->wrap( $psgi_app, builder => sub { Plack::Middleware::IISKeepAliveFix->wrap($_[0]) }, condition => sub { my ($env) = @_; return unless $env->{SERVER_SOFTWARE} && $env->{SERVER_SOFTWARE} =~ m!IIS\/[6-9]\.[0-9]!; 1; }, );
Subject: IISKeepAliveFix.pm
package Plack::Middleware::IISKeepAliveFix; use strict; use parent 'Plack::Middleware'; use Plack::Util; sub call { my($self, $env) = @_; # Fixes buffer being cut off on redirect when keep-alive is active my $res = $self->app->($env); Plack::Util::response_cb($res, sub { my $res = shift; if ($res->[0] =~ m!^30[123]$! ) { Plack::Util::header_remove($res->[1], 'Content-Length'); Plack::Util::header_remove($res->[1], 'Content-Type'); $res->[2]=['']; } return; }); } 1; __END__ =head1 NAME Plack::Middleware::IISKeepAliveFix - fixes buffer being cut off on redirect when keep-alive is active on IIS. =head1 SYNOPSIS # in your app.psgi use Plack::Builder; builder { enable "IISKeepAliveFix"; $app; }; # Or from the command line plackup -s FCGI -e 'enable "IISKeepAliveFix"' /path/to/app.psgi =head1 DESCRIPTION This middleware fixes buffer being cut off on redirect when keep-alive is active on IIS. =head1 AUTHORS Florian Ragwitz =cut
Subject: iis_keep_alive_fix.t
use strict; use Test::More; use Plack::Test; use HTTP::Request::Common; use Plack::Middleware::IISKeepAliveFix; my $app=Plack::Middleware::IISKeepAliveFix->wrap( sub { my $env = shift; my $location='/go/?'.join('|', (0..1000)); return [ 302, [ 'Content-Type' => 'text/html', 'Content-Length' => 285, 'Location' => $location, ],[qq~<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Moved</title> </head> <body> <p>This item has moved <a href="$location">here</a>.</p> </body> </html>~] ]; }); test_psgi $app => sub { my $cb = shift; my $res = $cb->(GET "/"); ok(!$res->content); ok(!$res->content_length); ok(!$res->content_type); }; done_testing;
Awesome! However, I'm sorry if I'm being dumb - but I can't see which versions you've tested / found the problem with in this ticket. Can you enlighten me?
From: KnowZeroX [...] yahoo.com
On Mon Jul 16 05:47:35 2012, BOBTFISH wrote: Show quoted text
> Awesome! However, I'm sorry if I'm being dumb - but I can't see which > versions you've tested / found the problem with in this ticket. > > Can you enlighten me?
On the first one by accident was the same as IISFix (copy and paste error lol). The second one was good but I couldn't get it to pass the test case because HTTP::Response didn't like undef body. Also I removed the smart code and just moved it to the deceleration. The final version is this: https://rt.cpan.org/Ticket/Attachment/1099581/578128/IISKeepAliveFix.pm this is the test case: https://rt.cpan.org/Ticket/Attachment/1099581/578129/iis_keep_alive_fix. t an this is the suggested method of declaration: https://rt.cpan.org/Ticket/Attachment/1099581/578127/declare.txt
Subject: Re: [rt.cpan.org #78377] Content-length error fix
Date: Mon, 16 Jul 2012 10:55:45 +0100
To: bug-Catalyst-Runtime [...] rt.cpan.org
From: Tomas Doran <bobtfish [...] bobtfish.net>
On 16 Jul 2012, at 10:54, via RT wrote: Show quoted text
> Queue: Catalyst-Runtime > Ticket <URL: https://rt.cpan.org/Ticket/Display.html?id=78377 > > > On Mon Jul 16 05:47:35 2012, BOBTFISH wrote:
>> Awesome! However, I'm sorry if I'm being dumb - but I can't see which >> versions you've tested / found the problem with in this ticket. >> >> Can you enlighten me?
>
<sigh> Sorry - I'm not being clear! I get it with which versions of the middleware are good. I meant which version(s) of IIS is this applicable to.. Sorry!
From: KnowZeroX [...] yahoo.com
On Mon Jul 16 05:56:00 2012, BOBTFISH wrote: Show quoted text
> > On 16 Jul 2012, at 10:54, via RT wrote: >
> > Queue: Catalyst-Runtime > > Ticket <URL: https://rt.cpan.org/Ticket/Display.html?id=78377 > > > > > On Mon Jul 16 05:47:35 2012, BOBTFISH wrote:
> >> Awesome! However, I'm sorry if I'm being dumb - but I can't see
which Show quoted text
> >> versions you've tested / found the problem with in this ticket. > >> > >> Can you enlighten me?
> >
> > <sigh> Sorry - I'm not being clear! > > I get it with which versions of the middleware are good. > > I meant which version(s) of IIS is this applicable to.. Sorry!
At this point I have been able to replicate the issue in IIS7/7.5 let me check IIS6 now.
From: KnowZeroX [...] yahoo.com
Ok I have did a test on IIS6 and IIS6 seems to be unaffected. So at this point I will say change it to: m!IIS\/7\.[0-9]! Some time later when I have the chance to install Windows 8 Server on my test pc I will get back to you on whether IIS8 is still effected or not.
From: KnowZeroX [...] yahoo.com
Ok I did one more test an ran into an issue, apparently me changing: $res->[2]=undef; to $res->[2]=['']; in Line 19 of my middleware is causing unintended issues. But then with it being undef there is no way to get the test case working due to limitations of HTTP::Response :/
From: KnowZeroX [...] yahoo.com
I am going to have to review the psgi spec again to see what I can do around this, I'll get you a fixed version of both, it and that test case by end of today.
From: KnowZeroX [...] yahoo.com
I found a solution faster then I though, attaching all the stuff.
Subject: declare.txt
use Plack::Middleware::IISKeepAliveFix; $psgi_app = Plack::Middleware::Conditional->wrap( $psgi_app, builder => sub { Plack::Middleware::IISKeepAliveFix->wrap($_[0]) }, condition => sub { my ($env) = @_; return unless $env->{SERVER_SOFTWARE} && $env->{SERVER_SOFTWARE} =~ m!IIS\/7\.[0-9]!; 1; }, );
Subject: IISKeepAliveFix.pm
package Plack::Middleware::IISKeepAliveFix; use strict; use parent 'Plack::Middleware'; use Plack::Util; sub call { my($self, $env) = @_; # Fixes buffer being cut off on redirect when keep-alive is active my $res = $self->app->($env); Plack::Util::response_cb($res, sub { my $res = shift; if ($res->[0] =~ m!^30[123]$! ) { Plack::Util::header_remove($res->[1], 'Content-Length'); Plack::Util::header_remove($res->[1], 'Content-Type'); return sub{ my $chunk; return unless defined $chunk; return ''; }; } return; }); } 1; __END__ =head1 NAME Plack::Middleware::IISKeepAliveFix - fixes buffer being cut off on redirect when keep-alive is active on IIS. =head1 SYNOPSIS # in your app.psgi use Plack::Builder; builder { enable "IISKeepAliveFix"; $app; }; # Or from the command line plackup -s FCGI -e 'enable "IISKeepAliveFix"' /path/to/app.psgi =head1 DESCRIPTION This middleware fixes buffer being cut off on redirect when keep-alive is active on IIS. =head1 AUTHORS Florian Ragwitz =cut
Subject: iis_keep_alive_fix.t
use strict; use Test::More; use Plack::Test; use HTTP::Request::Common; use Plack::Middleware::IISKeepAliveFix; my $app=Plack::Middleware::IISKeepAliveFix->wrap( sub { my $env = shift; my $location='/go/?'.join('|', (0..1000)); return [ 302, [ 'Content-Type' => 'text/html', 'Content-Length' => 285, 'Location' => $location, ],[qq~<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Moved</title> </head> <body> <p>This item has moved <a href="$location">here</a>.</p> </body> </html>~] ]; }); test_psgi(app=>$app,client=> sub { my $cb = shift; my $res = $cb->(GET "/"); ok(!$res->content); ok(!$res->content_length); ok(!$res->content_type); }); done_testing;