Skip to content

Commit 833a92f

Browse files
authored
Merge pull request #3089 from metacpan/haarg/static-rewrite
rewrite static middleware to correctly set headers
2 parents 61168a1 + 7dbbae4 commit 833a92f

File tree

3 files changed

+209
-63
lines changed

3 files changed

+209
-63
lines changed

lib/MetaCPAN/Middleware/Static.pm

Lines changed: 101 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package MetaCPAN::Middleware::Static;
22
use strict;
33
use warnings;
4-
use Plack::Builder qw( builder enable mount );
5-
use Plack::App::File ();
4+
use Cpanel::JSON::XS ();
65
use Cwd qw( cwd );
6+
use Plack::App::File ();
7+
use Plack::Builder qw( builder enable mount );
78
use Plack::MIME ();
8-
use Cpanel::JSON::XS ();
9+
use Plack::Util ();
910

1011
Plack::MIME->add_type(
1112
'.eot' => 'application/vnd.ms-fontobject',
13+
'.map' => 'application/json',
14+
'.mjs' => 'application/javascript',
1215
'.otf' => 'font/otf',
13-
'.ttf' => 'font/ttf',
14-
'.woff' => 'application/font-woff',
1516
'.woff2' => 'application/font-woff2',
1617
);
1718

@@ -21,26 +22,77 @@ my $hour_ttl = 60 * 60;
2122
my $day_ttl = $hour_ttl * 24;
2223
my $year_ttl = $day_ttl * 365;
2324

25+
sub _response_mw {
26+
my ( $app, $cb ) = @_;
27+
sub { Plack::Util::response_cb( $app->(@_), $cb ) };
28+
}
29+
30+
sub _add_headers {
31+
my ( $app, $add_headers ) = @_;
32+
_response_mw(
33+
$app,
34+
sub {
35+
my $res = shift;
36+
my ( $status, $headers ) = @$res;
37+
if ( $status >= 200 && $status < 300 ) {
38+
push @$headers, @$add_headers;
39+
}
40+
return $res;
41+
}
42+
);
43+
}
44+
45+
sub _add_surrogate_keys {
46+
my ($app) = @_;
47+
_response_mw(
48+
$app,
49+
sub {
50+
my $res = shift;
51+
my $headers = $res->[1];
52+
if ( my $content_type
53+
= Plack::Util::header_get( $headers, 'Content-Type' ) )
54+
{
55+
$content_type =~ s/;.*//;
56+
my $media_type = $content_type =~ s{/.*}{}r;
57+
push @$headers,
58+
'Surrogate-Key' => join( ', ',
59+
map "content_type=$_",
60+
$content_type, $media_type );
61+
}
62+
return $res;
63+
}
64+
);
65+
}
66+
67+
sub _file_app {
68+
my ( $type, $path, $headers ) = @_;
69+
_add_surrogate_keys( _add_headers(
70+
Plack::App::File->new( $type => $path )->to_app, $headers,
71+
) );
72+
}
73+
74+
sub _get_assets {
75+
my ($root) = @_;
76+
open my $fh, '<', "$root/assets/assets.json"
77+
or die "can't find asset map";
78+
my $json = do { local $/; <$fh> };
79+
close $fh;
80+
my $files = Cpanel::JSON::XS->new->decode($json);
81+
return [ map "/assets/$_", @$files ];
82+
}
83+
2484
sub wrap {
2585
my ( $self, $app, %args ) = @_;
2686
my $root_dir = $args{root} || cwd;
87+
my $root = "$root_dir/root";
2788
my $dev_mode
2889
= exists $args{dev_mode}
2990
? $args{dev_mode}
3091
: ( $ENV{PLACK_ENV} && $ENV{PLACK_ENV} eq 'development' );
3192

32-
my $get_assets = sub {
33-
open my $fh, '<', "$root_dir/root/assets/assets.json"
34-
or die "can't find asset map";
35-
my $json = do { local $/; <$fh> };
36-
close $fh;
37-
my $files = Cpanel::JSON::XS->new->decode($json);
38-
return [ map "/assets/$_", @$files ];
39-
};
40-
4193
my $assets;
4294
if ( !$dev_mode ) {
43-
$assets = $get_assets->();
95+
$assets = _get_assets($root);
4496
}
4597

4698
builder {
@@ -49,60 +101,49 @@ sub wrap {
49101
sub {
50102
my ($env) = @_;
51103
if ($dev_mode) {
52-
$assets = $get_assets->();
104+
$assets = _get_assets($root);
53105
}
54106
push @{ $env->{'metacpan.assets'} ||= [] }, @$assets;
55107
$app->($env);
56108
};
57109
};
58110

59-
my $favicon_app
60-
= Plack::App::File->new( file => 'root/static/icons/favicon.ico' )
61-
->to_app;
62-
mount '/favicon.ico' => sub {
63-
my $res = $favicon_app->(@_);
64-
push @{ $res->[1] },
65-
(
66-
'Cache-Control' => "max-age=${day_ttl}",
111+
mount '/favicon.ico' => _file_app(
112+
file => "$root/static/icons/favicon.ico",
113+
[
114+
'Cache-Control' => "public, max-age=${day_ttl}",
67115
'Surrogate-Control' => "max-age=${year_ttl}",
68116
'Surrogate-Key' => 'assets',
69-
);
70-
$res;
71-
};
72-
my $static_app
73-
= Plack::App::File->new( root => 'root/static' )->to_app;
74-
mount '/static' => sub {
75-
my $env = shift;
76-
my $res = $static_app->($env);
77-
if ( $env->{PATH_INFO} =~ m{^/(?:images|icons|fonts|modules)/} ) {
78-
push @{ $res->[1] },
79-
( 'Cache-Control' =>
80-
"public, max-age=${year_ttl}, immutable", );
81-
}
82-
else {
83-
push @{ $res->[1] },
84-
( 'Cache-Control' => "public, max-age=${day_ttl}", );
85-
}
86-
push @{ $res->[1] },
87-
(
88-
'Surrogate-Key' => 'assets',
89-
'Surrogate-Control' => "max-age=${year_ttl}",
90-
);
91-
$res;
92-
};
93-
my $assets_app
94-
= Plack::App::File->new( root => 'root/assets' )->to_app;
95-
mount '/assets' => sub {
96-
my $env = shift;
97-
my $res = $assets_app->($env);
98-
push @{ $res->[1] },
99-
(
100-
'Cache-Control' => "public, max-age=${year_ttl}, immutable",
101-
'Surrogate-Key' => 'assets',
117+
],
118+
);
119+
120+
for my $static_dir ( qw(
121+
assets
122+
static/icons
123+
static/images
124+
) )
125+
{
126+
mount "/$static_dir" => _file_app(
127+
root => "$root/$static_dir",
128+
[
129+
'Cache-Control' =>
130+
"public, max-age=${year_ttl}, immutable",
131+
'Surrogate-Control' => "max-age=${year_ttl}",
132+
'Surrogate-Key' => 'assets',
133+
],
134+
);
135+
}
136+
137+
mount "/static" => _file_app(
138+
root => "$root/static",
139+
[
140+
$dev_mode
141+
? ( 'Cache-Control' => "public, max-age=${day_ttl}", )
142+
: ( 'Cache-Control' => "public", ),
102143
'Surrogate-Control' => "max-age=${year_ttl}",
103-
);
104-
return $res;
105-
};
144+
'Surrogate-Key' => 'assets',
145+
],
146+
);
106147

107148
mount '/' => $app;
108149
};

lib/MetaCPAN/Web.pm

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ use Catalyst::Runtime 5.90042;
66

77
use Catalyst qw/
88
ConfigLoader
9-
Static::Simple
109
Authentication
11-
+MetaCPAN::Role::Fastly::Catalyst
1210
/, '-Log=warn,error,fatal';
1311
use Log::Log4perl::Catalyst ();
1412

15-
extends 'Catalyst';
13+
with 'MetaCPAN::Role::Fastly';
14+
with 'MetaCPAN::Role::Fastly::Catalyst';
1615

1716
__PACKAGE__->request_class_traits( [ qw(
1817
MetaCPAN::Web::Role::Request

t/static-mounts.t

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use strict;
2+
use warnings;
3+
use lib 't/lib';
4+
5+
use Test::More;
6+
use MetaCPAN::Web::Test qw( app GET test_psgi );
7+
8+
test_psgi app, sub {
9+
my $cb = shift;
10+
{
11+
ok( my $res = $cb->( GET '/favicon.ico' ), 'GET /favicon.ico' );
12+
is( $res->code, 200, 'code 200' );
13+
unlike $res->header('Cache-Control'), qr/immutable/, "not immutable";
14+
is_deeply [ sort split /, /, $res->header('Surrogate-Key') ], [ qw(
15+
assets
16+
content_type=image
17+
content_type=image/vnd.microsoft.icon
18+
) ],
19+
'correct Surrogate-Key';
20+
}
21+
{
22+
ok( my $res = $cb->( GET '/static/opensearch.xml' ),
23+
'GET /static/opensearch.xml' );
24+
is( $res->code, 200, 'code 200' );
25+
unlike $res->header('Cache-Control'), qr/immutable/, "not immutable";
26+
is_deeply [ sort split /, /, $res->header('Surrogate-Key') ], [ qw(
27+
assets
28+
content_type=application
29+
content_type=application/xml
30+
) ],
31+
'correct Surrogate-Key';
32+
}
33+
{
34+
ok( my $res = $cb->( GET '/static/fastly_do_not_delete.gif' ),
35+
'GET /static/fastly_do_not_delete.gif' );
36+
is( $res->code, 200, 'code 200' );
37+
unlike $res->header('Cache-Control'), qr/immutable/, "not immutable";
38+
is_deeply [ sort split /, /, $res->header('Surrogate-Key') ], [ qw(
39+
assets
40+
content_type=image
41+
content_type=image/gif
42+
) ],
43+
'correct Surrogate-Key';
44+
}
45+
{
46+
ok( my $res = $cb->( GET '/static/icons/grid.svg' ),
47+
'GET /static/icons/grid.svg' );
48+
is( $res->code, 200, 'code 200' );
49+
like $res->header('Cache-Control'), qr/immutable/, "immutable";
50+
is_deeply [ sort split /, /, $res->header('Surrogate-Key') ], [ qw(
51+
assets
52+
content_type=image
53+
content_type=image/svg+xml
54+
) ],
55+
'correct Surrogate-Key';
56+
}
57+
{
58+
ok( my $res = $cb->( GET '/static/images/dots.svg' ),
59+
'GET /static/images/dots.svg' );
60+
is( $res->code, 200, 'code 200' );
61+
like $res->header('Cache-Control'), qr/immutable/, "immutable";
62+
is_deeply [ sort split /, /, $res->header('Surrogate-Key') ], [ qw(
63+
assets
64+
content_type=image
65+
content_type=image/svg+xml
66+
) ],
67+
'correct Surrogate-Key';
68+
}
69+
{
70+
ok( my $res = $cb->( GET '/static/js/main.mjs' ),
71+
'GET /static/js/main.mjs' );
72+
is( $res->code, 200, 'code 200' );
73+
unlike $res->header('Cache-Control'), qr/immutable/, "not immutable";
74+
is_deeply [ sort split /,? /, $res->header('Surrogate-Key') ], [ qw(
75+
assets
76+
content_type=application
77+
content_type=application/javascript
78+
) ],
79+
'correct Surrogate-Key';
80+
}
81+
{
82+
ok( my $res = $cb->( GET '/assets/assets.json' ),
83+
'GET /assets/assets.json' );
84+
is( $res->code, 200, 'code 200' );
85+
like $res->header('Cache-Control'), qr/immutable/, "immutable";
86+
is_deeply [ sort split /, /, $res->header('Surrogate-Key') ], [ qw(
87+
assets
88+
content_type=application
89+
content_type=application/json
90+
) ],
91+
'correct Surrogate-Key';
92+
}
93+
{
94+
ok( my $res = $cb->( GET '/assets/this-file-does-not-exist.js' ),
95+
'GET /assets/this-file-does-not-exist.js' );
96+
is( $res->code, 404, 'code 404' );
97+
unlike $res->header('Cache-Control'), qr/immutable/, "not immutable";
98+
is_deeply [ sort split /, /, $res->header('Surrogate-Key') ], [ qw(
99+
content_type=text
100+
content_type=text/plain
101+
) ],
102+
'correct Surrogate-Key';
103+
}
104+
};
105+
106+
done_testing;

0 commit comments

Comments
 (0)