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

This commit is contained in:
Jonah Williams 2020-02-21 14:15:54 -08:00 committed by GitHub
parent c5dd3ec47a
commit bb74a328b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 69 additions and 19 deletions

View File

@ -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 {
// 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 {
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 {
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);
} }

View File

@ -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() {
.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() {
.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() {
.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() {
.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() {
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() {
.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() {
.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() {
.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() {
.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());
})); }));