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 { ...@@ -137,6 +137,10 @@ class WebAssetServer implements AssetReader {
return shelf.Response.notFound(''); 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. // NOTE: shelf removes leading `/` for some reason.
final String requestPath = request.url.path.startsWith('/') final String requestPath = request.url.path.startsWith('/')
? request.url.path ? request.url.path
...@@ -146,16 +150,29 @@ class WebAssetServer implements AssetReader { ...@@ -146,16 +150,29 @@ class WebAssetServer implements AssetReader {
// Attempt to look up the file by URI. // Attempt to look up the file by URI.
if (_files.containsKey(requestPath)) { if (_files.containsKey(requestPath)) {
final List<int> bytes = getFile(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.contentLengthHeader] = bytes.length.toString();
headers[HttpHeaders.contentTypeHeader] = 'application/javascript'; headers[HttpHeaders.contentTypeHeader] = 'application/javascript';
headers[HttpHeaders.etagHeader] = etag;
return shelf.Response.ok(bytes, headers: headers); return shelf.Response.ok(bytes, headers: headers);
} }
// If this is a sourcemap file, then it might be in the in-memory cache. // If this is a sourcemap file, then it might be in the in-memory cache.
// Attempt to lookup the file by URI. // Attempt to lookup the file by URI.
if (_sourcemaps.containsKey(requestPath)) { if (_sourcemaps.containsKey(requestPath)) {
final List<int> bytes = getSourceMap(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.contentLengthHeader] = bytes.length.toString();
headers[HttpHeaders.contentTypeHeader] = 'application/json'; headers[HttpHeaders.contentTypeHeader] = 'application/json';
headers[HttpHeaders.etagHeader] = etag;
return shelf.Response.ok(bytes, headers: headers); return shelf.Response.ok(bytes, headers: headers);
} }
...@@ -172,6 +189,13 @@ class WebAssetServer implements AssetReader { ...@@ -172,6 +189,13 @@ class WebAssetServer implements AssetReader {
if (!file.existsSync()) { if (!file.existsSync()) {
return shelf.Response.notFound(''); 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(); final int length = file.lengthSync();
// Attempt to determine the file's mime type. if this is not provided some // 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 // browsers will refuse to render images/show video et cetera. If the tool
...@@ -186,6 +210,7 @@ class WebAssetServer implements AssetReader { ...@@ -186,6 +210,7 @@ class WebAssetServer implements AssetReader {
mimeType ??= _kDefaultMimeType; mimeType ??= _kDefaultMimeType;
headers[HttpHeaders.contentLengthHeader] = length.toString(); headers[HttpHeaders.contentLengthHeader] = length.toString();
headers[HttpHeaders.contentTypeHeader] = mimeType; headers[HttpHeaders.contentTypeHeader] = mimeType;
headers[HttpHeaders.etagHeader] = etag;
return shelf.Response.ok(file.openRead(), headers: headers); return shelf.Response.ok(file.openRead(), headers: headers);
} }
......
...@@ -87,8 +87,9 @@ void main() { ...@@ -87,8 +87,9 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/foo.js'))); .handleRequest(Request('GET', Uri.parse('http://foobar/foo.js')));
expect(response.headers, allOf(<Matcher>[ expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()), containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair('content-type', 'application/javascript'), containsPair(HttpHeaders.contentTypeHeader, 'application/javascript'),
containsPair(HttpHeaders.etagHeader, isNotNull)
])); ]));
expect((await response.read().toList()).first, source.readAsBytesSync()); expect((await response.read().toList()).first, source.readAsBytesSync());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -102,12 +103,30 @@ void main() { ...@@ -102,12 +103,30 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/foo.js'))); .handleRequest(Request('GET', Uri.parse('http://foobar/foo.js')));
expect(response.headers, allOf(<Matcher>[ expect(response.headers, allOf(<Matcher>[
containsPair('content-length', '9'), containsPair(HttpHeaders.contentLengthHeader, '9'),
containsPair('content-type', 'application/javascript'), 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() {}')); 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 { test('handles missing JavaScript files from in memory cache', () => testbed.run(() async {
final File source = globals.fs.file('source') final File source = globals.fs.file('source')
..writeAsStringSync('main() {}'); ..writeAsStringSync('main() {}');
...@@ -141,8 +160,10 @@ void main() { ...@@ -141,8 +160,10 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://localhost/foo.js'))); .handleRequest(Request('GET', Uri.parse('http://localhost/foo.js')));
expect(response.headers, allOf(<Matcher>[ expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()), containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair('content-type', 'application/javascript'), 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()); expect((await response.read().toList()).first, source.readAsBytesSync());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -157,8 +178,10 @@ void main() { ...@@ -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'))); .handleRequest(Request('GET', Uri.parse('http://foobar/assets/abcd%25E8%25B1%25A1%25E5%25BD%25A2%25E5%25AD%2597.png')));
expect(response.headers, allOf(<Matcher>[ expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()), containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair('content-type', 'image/png'), 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()); expect((await response.read().toList()).first, source.readAsBytesSync());
})); }));
...@@ -171,8 +194,10 @@ void main() { ...@@ -171,8 +194,10 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo.png'))); .handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo.png')));
expect(response.headers, allOf(<Matcher>[ expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()), containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair('content-type', 'image/png'), 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()); expect((await response.read().toList()).first, source.readAsBytesSync());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -187,7 +212,7 @@ void main() { ...@@ -187,7 +212,7 @@ void main() {
final Response response = await webAssetServer final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/foo.dart'))); .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()); expect((await response.read().toList()).first, source.readAsBytesSync());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Platform: () => linux, Platform: () => linux,
...@@ -209,8 +234,8 @@ void main() { ...@@ -209,8 +234,8 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo.png'))); .handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo.png')));
expect(response.headers, allOf(<Matcher>[ expect(response.headers, allOf(<Matcher>[
containsPair('content-length', source.lengthSync().toString()), containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
containsPair('content-type', 'image/png'), containsPair(HttpHeaders.contentTypeHeader, 'image/png'),
])); ]));
expect((await response.read().toList()).first, source.readAsBytesSync()); expect((await response.read().toList()).first, source.readAsBytesSync());
})); }));
...@@ -224,8 +249,8 @@ void main() { ...@@ -224,8 +249,8 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo'))); .handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo')));
expect(response.headers, allOf(<Matcher>[ expect(response.headers, allOf(<Matcher>[
containsPair('content-length', '100'), containsPair(HttpHeaders.contentLengthHeader, '100'),
containsPair('content-type', 'application/octet-stream'), containsPair(HttpHeaders.contentTypeHeader, 'application/octet-stream'),
])); ]));
expect((await response.read().toList()).first, source.readAsBytesSync()); expect((await response.read().toList()).first, source.readAsBytesSync());
})); }));
...@@ -239,8 +264,8 @@ void main() { ...@@ -239,8 +264,8 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo'))); .handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo')));
expect(response.headers, allOf(<Matcher>[ expect(response.headers, allOf(<Matcher>[
containsPair('content-length', '3'), containsPair(HttpHeaders.contentLengthHeader, '3'),
containsPair('content-type', 'application/octet-stream'), containsPair(HttpHeaders.contentTypeHeader, 'application/octet-stream'),
])); ]));
expect((await response.read().toList()).first, source.readAsBytesSync()); expect((await response.read().toList()).first, source.readAsBytesSync());
})); }));
...@@ -264,8 +289,8 @@ void main() { ...@@ -264,8 +289,8 @@ void main() {
.handleRequest(Request('GET', Uri.parse('http:///packages/flutter_tools/foo.dart'))); .handleRequest(Request('GET', Uri.parse('http:///packages/flutter_tools/foo.dart')));
expect(response.headers, allOf(<Matcher>[ expect(response.headers, allOf(<Matcher>[
containsPair('content-length', '3'), containsPair(HttpHeaders.contentLengthHeader, '3'),
containsPair('content-type', 'application/octet-stream'), containsPair(HttpHeaders.contentTypeHeader, 'application/octet-stream'),
])); ]));
expect((await response.read().toList()).first, source.readAsBytesSync()); 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