Unverified Commit bb74a328 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] adds etag/cache control header to debug asset server (#51143)

parent c5dd3ec4
......@@ -137,6 +137,10 @@ class WebAssetServer implements AssetReader {
return shelf.Response.notFound('');
}
// Track etag headers for better caching of resources.
final String ifNoneMatch = request.headers[HttpHeaders.ifNoneMatchHeader];
headers[HttpHeaders.cacheControlHeader] = 'max-age=0, must-revalidate';
// NOTE: shelf removes leading `/` for some reason.
final String requestPath = request.url.path.startsWith('/')
? request.url.path
......@@ -146,16 +150,29 @@ class WebAssetServer implements AssetReader {
// Attempt to look up the file by URI.
if (_files.containsKey(requestPath)) {
final List<int> bytes = getFile(requestPath);
// Use the underlying buffer hashCode as a revision string. This buffer is
// replaced whenever the frontend_server produces new output files, which
// will also change the hashCode.
final String etag = bytes.hashCode.toString();
if (ifNoneMatch == etag) {
return shelf.Response.notModified();
}
headers[HttpHeaders.contentLengthHeader] = bytes.length.toString();
headers[HttpHeaders.contentTypeHeader] = 'application/javascript';
headers[HttpHeaders.etagHeader] = etag;
return shelf.Response.ok(bytes, headers: headers);
}
// If this is a sourcemap file, then it might be in the in-memory cache.
// Attempt to lookup the file by URI.
if (_sourcemaps.containsKey(requestPath)) {
final List<int> bytes = getSourceMap(requestPath);
final String etag = bytes.hashCode.toString();
if (ifNoneMatch == etag) {
return shelf.Response.notModified();
}
headers[HttpHeaders.contentLengthHeader] = bytes.length.toString();
headers[HttpHeaders.contentTypeHeader] = 'application/json';
headers[HttpHeaders.etagHeader] = etag;
return shelf.Response.ok(bytes, headers: headers);
}
......@@ -172,6 +189,13 @@ class WebAssetServer implements AssetReader {
if (!file.existsSync()) {
return shelf.Response.notFound('');
}
// For real files, use a serialized file stat as a revision
final String etag = file.lastModifiedSync().toIso8601String();
if (ifNoneMatch == etag) {
return shelf.Response.notModified();
}
final int length = file.lengthSync();
// Attempt to determine the file's mime type. if this is not provided some
// browsers will refuse to render images/show video et cetera. If the tool
......@@ -186,6 +210,7 @@ class WebAssetServer implements AssetReader {
mimeType ??= _kDefaultMimeType;
headers[HttpHeaders.contentLengthHeader] = length.toString();
headers[HttpHeaders.contentTypeHeader] = mimeType;
headers[HttpHeaders.etagHeader] = etag;
return shelf.Response.ok(file.openRead(), headers: headers);
}
......
......@@ -87,8 +87,9 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/foo.js')));
expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()),
containsPair('content-type', 'application/javascript'),
containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair(HttpHeaders.contentTypeHeader, 'application/javascript'),
containsPair(HttpHeaders.etagHeader, isNotNull)
]));
expect((await response.read().toList()).first, source.readAsBytesSync());
}, overrides: <Type, Generator>{
......@@ -102,12 +103,30 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/foo.js')));
expect(response.headers, allOf(<Matcher>[
containsPair('content-length', '9'),
containsPair('content-type', 'application/javascript'),
containsPair(HttpHeaders.contentLengthHeader, '9'),
containsPair(HttpHeaders.contentTypeHeader, 'application/javascript'),
containsPair(HttpHeaders.etagHeader, isNotNull),
containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
]));
expect((await response.read().toList()).first, utf8.encode('main() {}'));
}));
test('Returns notModified when the ifNoneMatch header matches the etag', () => testbed.run(() async {
webAssetServer.writeFile('/foo.js', 'main() {}');
final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/foo.js')));
final String etag = response.headers[HttpHeaders.etagHeader];
final Response cachedResponse = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/foo.js'), headers: <String, String>{
HttpHeaders.ifNoneMatchHeader: etag
}));
expect(cachedResponse.statusCode, HttpStatus.notModified);
expect(await cachedResponse.read().toList(), isEmpty);
}));
test('handles missing JavaScript files from in memory cache', () => testbed.run(() async {
final File source = globals.fs.file('source')
..writeAsStringSync('main() {}');
......@@ -141,8 +160,10 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://localhost/foo.js')));
expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()),
containsPair('content-type', 'application/javascript'),
containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair(HttpHeaders.contentTypeHeader, 'application/javascript'),
containsPair(HttpHeaders.etagHeader, isNotNull),
containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
]));
expect((await response.read().toList()).first, source.readAsBytesSync());
}, overrides: <Type, Generator>{
......@@ -157,8 +178,10 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/assets/abcd%25E8%25B1%25A1%25E5%25BD%25A2%25E5%25AD%2597.png')));
expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()),
containsPair('content-type', 'image/png'),
containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair(HttpHeaders.contentTypeHeader, 'image/png'),
containsPair(HttpHeaders.etagHeader, isNotNull),
containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
]));
expect((await response.read().toList()).first, source.readAsBytesSync());
}));
......@@ -171,8 +194,10 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo.png')));
expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()),
containsPair('content-type', 'image/png'),
containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair(HttpHeaders.contentTypeHeader, 'image/png'),
containsPair(HttpHeaders.etagHeader, isNotNull),
containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
]));
expect((await response.read().toList()).first, source.readAsBytesSync());
}, overrides: <Type, Generator>{
......@@ -187,7 +212,7 @@ void main() {
final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/foo.dart')));
expect(response.headers, containsPair('content-length', source.lengthSync().toString()));
expect(response.headers, containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()));
expect((await response.read().toList()).first, source.readAsBytesSync());
}, overrides: <Type, Generator>{
Platform: () => linux,
......@@ -209,8 +234,8 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo.png')));
expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()),
containsPair('content-type', 'image/png'),
containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair(HttpHeaders.contentTypeHeader, 'image/png'),
]));
expect((await response.read().toList()).first, source.readAsBytesSync());
}));
......@@ -224,8 +249,8 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo')));
expect(response.headers, allOf(<Matcher>[
containsPair('content-length', '100'),
containsPair('content-type', 'application/octet-stream'),
containsPair(HttpHeaders.contentLengthHeader, '100'),
containsPair(HttpHeaders.contentTypeHeader, 'application/octet-stream'),
]));
expect((await response.read().toList()).first, source.readAsBytesSync());
}));
......@@ -239,8 +264,8 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo')));
expect(response.headers, allOf(<Matcher>[
containsPair('content-length', '3'),
containsPair('content-type', 'application/octet-stream'),
containsPair(HttpHeaders.contentLengthHeader, '3'),
containsPair(HttpHeaders.contentTypeHeader, 'application/octet-stream'),
]));
expect((await response.read().toList()).first, source.readAsBytesSync());
}));
......@@ -264,8 +289,8 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http:///packages/flutter_tools/foo.dart')));
expect(response.headers, allOf(<Matcher>[
containsPair('content-length', '3'),
containsPair('content-type', 'application/octet-stream'),
containsPair(HttpHeaders.contentLengthHeader, '3'),
containsPair(HttpHeaders.contentTypeHeader, 'application/octet-stream'),
]));
expect((await response.read().toList()).first, source.readAsBytesSync());
}));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment