Skip to content

Commit d925c73

Browse files
committed
Updated cache headers.
1 parent 97c3983 commit d925c73

File tree

11 files changed

+81
-23
lines changed

11 files changed

+81
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ AppEngine version, listed here to ease deployment and troubleshooting.
44
## Next Release (replace with git tag when deployed)
55
* Bump runtimeVersion to `2025.10.21`.
66
* Upgraded stable Flutter analysis SDK to `3.35.6`.
7+
* Added more explicitly public `cache-control` to content pages.
78

89
## `20251017t101000-all`
910
* Bump runtimeVersion to `2025.10.17`.

app/lib/frontend/handlers/cache_control.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ final class CacheControl {
6262
public: true,
6363
);
6464

65+
/// `Cache-Control` headers for package content pages, returning content that
66+
/// is not updated frequently.
67+
static const packageContentPage = CacheControl(
68+
maxAge: Duration(minutes: 30),
69+
public: true,
70+
);
71+
72+
/// `Cache-Control` headers for package listing pages, returning content that
73+
/// is may be updated frequently.
74+
static const packageListingPage = CacheControl(
75+
maxAge: Duration(minutes: 5),
76+
public: true,
77+
);
78+
6579
/// `Cache-Control` headers for API end-points returning completion data for
6680
/// use in IDE integrations.
6781
static const completionData = CacheControl(

app/lib/frontend/handlers/documentation.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ import '../../shared/urls.dart';
2323
///
2424
/// - `/documentation/<package>/<version>`
2525
Future<shelf.Response> documentationHandler(shelf.Request request) async {
26+
final requestMethod = request.method.toUpperCase();
27+
if (requestMethod != 'HEAD' && requestMethod != 'GET') {
28+
// TODO: Should probably be "method not supported"!
29+
return notFoundHandler(request);
30+
}
31+
2632
final docFilePath = parseRequestUri(request.requestedUri);
2733
if (docFilePath == null) {
2834
return notFoundHandler(request);
@@ -58,12 +64,6 @@ Future<shelf.Response> documentationHandler(shelf.Request request) async {
5864
),
5965
);
6066
}
61-
final String requestMethod = request.method.toUpperCase();
62-
63-
if (requestMethod != 'HEAD' && requestMethod != 'GET') {
64-
// TODO: Should probably be "method not supported"!
65-
return notFoundHandler(request);
66-
}
6767

6868
final package = docFilePath.package;
6969
final version = docFilePath.version!;

app/lib/frontend/handlers/landing.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:async';
66

77
import 'package:_pub_shared/search/tags.dart';
8+
import 'package:pub_dev/frontend/handlers/cache_control.dart';
89
import 'package:pub_dev/search/top_packages.dart';
910
import 'package:shelf/shelf.dart' as shelf;
1011

@@ -49,7 +50,10 @@ Future<shelf.Response> indexLandingHandler(shelf.Request request) async {
4950
}
5051

5152
if (requestContext.uiCacheEnabled) {
52-
return htmlResponse((await cache.uiIndexPage().get(_render))!);
53+
return htmlResponse(
54+
(await cache.uiIndexPage().get(_render))!,
55+
headers: CacheControl.packageListingPage.headers,
56+
);
5357
}
5458
return htmlResponse(await _render());
5559
}

app/lib/frontend/handlers/listing.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import 'dart:async';
77
import 'package:_pub_shared/search/search_form.dart';
88
import 'package:_pub_shared/search/tags.dart';
99
import 'package:logging/logging.dart';
10+
import 'package:pub_dev/frontend/handlers/cache_control.dart';
11+
import 'package:pub_dev/frontend/request_context.dart';
1012
import 'package:shelf/shelf.dart' as shelf;
1113

1214
import '../../package/name_tracker.dart';
@@ -94,6 +96,9 @@ Future<shelf.Response> _packagesHandlerHtmlCore(shelf.Request request) async {
9496
openSections: openSections,
9597
),
9698
status: statusCode,
99+
headers: statusCode == 200 && requestContext.uiCacheEnabled
100+
? CacheControl.packageListingPage.headers
101+
: null,
97102
);
98103
_searchOverallLatencyTracker.add(sw.elapsed);
99104
return result;

app/lib/frontend/handlers/package.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ Future<shelf.Response> _handlePackagePage({
301301
Entry<String>? cacheEntry,
302302
}) async {
303303
checkPackageVersionParams(packageName, versionName);
304+
final cacheEnabled = requestContext.uiCacheEnabled && cacheEntry != null;
304305

305306
final canonicalUrl = canonicalUrlFn(
306307
await _canonicalPackageName(packageName),
@@ -314,7 +315,7 @@ Future<shelf.Response> _handlePackagePage({
314315
}
315316
final Stopwatch sw = Stopwatch()..start();
316317
String? cachedPage;
317-
if (requestContext.uiCacheEnabled && cacheEntry != null) {
318+
if (cacheEnabled) {
318319
cachedPage = await cacheEntry.get();
319320
}
320321

@@ -350,12 +351,15 @@ Future<shelf.Response> _handlePackagePage({
350351
} else {
351352
throw StateError('Unknown result type: ${renderedResult.runtimeType}');
352353
}
353-
if (requestContext.uiCacheEnabled && cacheEntry != null) {
354+
if (cacheEnabled) {
354355
await cacheEntry.set(cachedPage);
355356
}
356357
_packageDoneLatencyTracker.add(sw.elapsed);
357358
}
358-
return htmlResponse(cachedPage);
359+
return htmlResponse(
360+
cachedPage,
361+
headers: cacheEnabled ? CacheControl.packageContentPage.headers : null,
362+
);
359363
}
360364

361365
/// Returns the optionally lowercased version of [name], but only if there

app/lib/frontend/request_context.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Future<RequestContext> buildRequestContext({
110110
// don't cache if client session is active
111111
!clientSessionCookieStatus.isPresent &&
112112
// sanity check, this should be covered by client session cookie
113-
(csrfToken?.isNotEmpty ?? false);
113+
!(csrfToken?.isNotEmpty ?? false);
114114
return RequestContext(
115115
indentJson: indentJson,
116116
blockRobots: !enableRobots,

app/lib/task/handlers.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:path/path.dart' as p;
66

77
import 'package:pub_dev/dartdoc/dartdoc_page.dart';
88
import 'package:pub_dev/dartdoc/models.dart';
9+
import 'package:pub_dev/frontend/handlers/cache_control.dart';
910
import 'package:pub_dev/shared/exceptions.dart';
1011
import 'package:pub_dev/shared/handlers.dart';
1112
import 'package:pub_dev/shared/redis_cache.dart';
@@ -90,7 +91,10 @@ Future<shelf.Response> handleDartDoc(
9091
);
9192
final htmlBytes = await htmlBytesCacheEntry.get();
9293
if (htmlBytes != null) {
93-
return htmlBytesResponse(htmlBytes);
94+
return htmlBytesResponse(
95+
htmlBytes,
96+
headers: CacheControl.packageContentPage.headers,
97+
);
9498
}
9599

96100
// check cached status for redirect or missing pages
@@ -211,7 +215,10 @@ Future<shelf.Response> handleDartDoc(
211215
switch (status.code) {
212216
case DocPageStatusCode.ok:
213217
await htmlBytesCacheEntry.set(bytes!);
214-
return htmlBytesResponse(bytes);
218+
return htmlBytesResponse(
219+
bytes,
220+
headers: CacheControl.packageContentPage.headers,
221+
);
215222
case DocPageStatusCode.redirect:
216223
return redirectPathResponse(status.redirectPath!);
217224
case DocPageStatusCode.missing:
@@ -236,13 +243,14 @@ Future<shelf.Response> handleDartDoc(
236243
}
237244

238245
if (request.method.toUpperCase() == 'HEAD') {
239-
return htmlResponse('');
246+
return htmlResponse('', headers: CacheControl.packageContentPage.headers);
240247
}
241248

242249
final acceptsGzip = request.acceptsGzipEncoding();
243250
return shelf.Response.ok(
244251
acceptsGzip ? dataGz : gzip.decode(dataGz),
245252
headers: {
253+
...CacheControl.packageContentPage.headers,
246254
'Content-Type': mime,
247255
'Vary': 'Accept-Encoding', // body depends on accept-encoding!
248256
if (acceptsGzip) 'Content-Encoding': 'gzip',

pkg/pub_integration/lib/src/fake_test_context_provider.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,20 @@ class TestContextProvider {
5858
await _fakePubServerProcess.kill();
5959
}
6060

61-
Future<TestUser> createAnonymousTestUser() async {
61+
Future<TestUser> createAnonymousTestUser({
62+
bool expectAllResponsesToBeCacheControlPublic = true,
63+
}) async {
6264
final session = await _testBrowser.createSession();
6365
return TestUser(
6466
email: '',
6567
browserApi: PubApiClient(pubHostedUrl),
6668
serverApi: PubApiClient(pubHostedUrl),
6769
withBrowserPage: <T>(Future<T> Function(Page) fn) async {
68-
return await session.withPage<T>(fn: fn);
70+
return await session.withPage<T>(
71+
fn: fn,
72+
expectAllResponsesToBeCacheControlPublic:
73+
expectAllResponsesToBeCacheControlPublic,
74+
);
6975
},
7076
readLatestEmail: () async => throw UnimplementedError(),
7177
createCredentials: () async => throw UnimplementedError(),

pkg/pub_integration/lib/src/test_browser.dart

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,10 @@ class TestBrowserSession {
185185
TestBrowserSession(this._browser, this._context);
186186

187187
/// Creates a new page and setup overrides and tracking.
188-
Future<R> withPage<R>({required Future<R> Function(Page page) fn}) async {
188+
Future<R> withPage<R>({
189+
required Future<R> Function(Page page) fn,
190+
bool expectAllResponsesToBeCacheControlPublic = false,
191+
}) async {
189192
final clientErrors = <ClientError>[];
190193
final serverErrors = <String>[];
191194
final page = await _context.newPage();
@@ -261,20 +264,31 @@ class TestBrowserSession {
261264
}
262265
}
263266

264-
if (!rs.url.startsWith('data:') &&
265-
// exempt the image URL from markdown_samples.md
266-
rs.url != 'https://pub.dev/static/img/pub-dev-logo.svg') {
267+
if (rs.status == 200 &&
268+
rs.request.method.toUpperCase() == 'GET' &&
269+
// filters out data: and other-domain URLs
270+
rs.url.startsWith(_browser._origin)) {
267271
final uri = Uri.parse(rs.url);
268-
if (uri.pathSegments.length > 1 && uri.pathSegments.first == 'static') {
272+
final firstPathSegment = uri.pathSegments.firstOrNull;
273+
274+
if (firstPathSegment == 'static') {
269275
if (!uri.pathSegments[1].startsWith('hash-')) {
270276
serverErrors.add('Static URL ${rs.url} is without hash segment.');
271277
}
278+
}
272279

280+
final shouldBePublic =
281+
firstPathSegment == 'static' ||
282+
firstPathSegment == 'documentation' ||
283+
expectAllResponsesToBeCacheControlPublic;
284+
final knownExemption =
285+
firstPathSegment == 'experimental' || firstPathSegment == 'report';
286+
if (shouldBePublic && !knownExemption) {
273287
final cacheHeader = rs.headers[HttpHeaders.cacheControlHeader];
274288
if (cacheHeader == null ||
275289
!cacheHeader.contains('public') ||
276290
!cacheHeader.contains('max-age')) {
277-
serverErrors.add('Static ${rs.url} is without public caching.');
291+
serverErrors.add('${rs.url} is without public caching.');
278292
}
279293
}
280294
}

0 commit comments

Comments
 (0)