diff --git a/.idea/runConfigurations/sends_304_on_if_modified_since_in_cache_test_dart.xml b/.idea/runConfigurations/sends_304_on_if_modified_since_in_cache_test_dart.xml
new file mode 100644
index 00000000..57b298cf
--- /dev/null
+++ b/.idea/runConfigurations/sends_304_on_if_modified_since_in_cache_test_dart.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/tests_in_cache_test_dart.xml b/.idea/runConfigurations/tests_in_cache_test_dart.xml
new file mode 100644
index 00000000..33520a9a
--- /dev/null
+++ b/.idea/runConfigurations/tests_in_cache_test_dart.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index ac775e77..a9b69342 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ Future configureServer(Angel app) async {
cache.patterns.addAll([
'robots.txt',
new RegExp(r'\.(png|jpg|gif|txt)$'),
- new Glob('/public/**/*'),
+ new Glob('public/**/*'),
]);
// REQUIRED: The middleware that serves cached responses
diff --git a/lib/src/cache.dart b/lib/src/cache.dart
index 794fde2b..181a0af0 100644
--- a/lib/src/cache.dart
+++ b/lib/src/cache.dart
@@ -1,12 +1,7 @@
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
-import 'package:intl/intl.dart';
import 'package:pool/pool.dart';
-
-final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
-
-/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`.
-String _formatDateForHttp(DateTime dt) => _fmt.format(dt.toUtc()) + ' GMT';
+import 'util.dart';
/// A flexible response cache for Angel.
///
@@ -39,7 +34,7 @@ class ResponseCache {
/// This prevents the server from even having to access the cache, and plays very well with static assets.
Future ifModifiedSince(RequestContext req, ResponseContext res) async {
if (req.headers.value('if-modified-since') != null) {
- var modifiedSince = _fmt
+ var modifiedSince = fmt
.parse(req.headers.value('if-modified-since').replaceAll('GMT', ''));
// Check if there is a cache entry.
@@ -47,8 +42,9 @@ class ResponseCache {
if (pattern.allMatches(req.uri.path).isNotEmpty &&
_cache.containsKey(req.uri.path)) {
var response = _cache[req.uri.path];
+ //print('${response.timestamp} vs ${modifiedSince}');
- if (response.timestamp.compareTo(modifiedSince) <= 0) {
+ if (response.timestamp.compareTo(modifiedSince) < 0) {
res.statusCode = 304;
return false;
}
@@ -61,8 +57,7 @@ class ResponseCache {
/// Serves content from the cache, if applicable.
Future handleRequest(RequestContext req, ResponseContext res) async {
- if (!await ifModifiedSince(req, res))
- return false;
+ if (!await ifModifiedSince(req, res)) return false;
// Check if there is a cache entry.
for (var pattern in patterns) {
@@ -70,9 +65,13 @@ class ResponseCache {
var now = new DateTime.now().toUtc();
if (_cache.containsKey(req.uri.path)) {
- // If the cache timeout has been met, don't send the cached response.
var response = _cache[req.uri.path];
- if (now.difference(response.timestamp) >= timeout) return true;
+
+ if (timeout != null) {
+ // If the cache timeout has been met, don't send the cached response.
+ if (now.difference(response.timestamp) >= timeout) return true;
+ }
+
_setCachedHeaders(response.timestamp, req, res);
res
..headers.addAll(response.headers)
@@ -110,7 +109,8 @@ class ResponseCache {
}
// Save the response.
- var writeLock = _writeLocks.putIfAbsent(req.uri.path, () => new Pool(1));
+ var writeLock =
+ _writeLocks.putIfAbsent(req.uri.path, () => new Pool(1));
await writeLock.withResource(() {
_cache[req.uri.path] = new _CachedResponse(
new Map.from(res.headers), res.buffer.toBytes(), now);
@@ -129,11 +129,11 @@ class ResponseCache {
res.headers
..['cache-control'] = '$privacy, max-age=${timeout?.inSeconds ?? 0}'
- ..['last-modified'] = _formatDateForHttp(modified);
+ ..['last-modified'] = formatDateForHttp(modified);
if (timeout != null) {
var expiry = new DateTime.now().add(timeout);
- res.headers['expires'] = _formatDateForHttp(expiry);
+ res.headers['expires'] = formatDateForHttp(expiry);
}
}
}
diff --git a/lib/src/util.dart b/lib/src/util.dart
new file mode 100644
index 00000000..90a91172
--- /dev/null
+++ b/lib/src/util.dart
@@ -0,0 +1,6 @@
+import 'package:intl/intl.dart';
+
+final DateFormat fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
+
+/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`.
+String formatDateForHttp(DateTime dt) => fmt.format(dt.toUtc()) + ' GMT';
\ No newline at end of file
diff --git a/test/cache_test.dart b/test/cache_test.dart
new file mode 100644
index 00000000..4ec6c603
--- /dev/null
+++ b/test/cache_test.dart
@@ -0,0 +1,101 @@
+import 'dart:async';
+import 'package:angel_cache/src/util.dart';
+import 'package:angel_cache/angel_cache.dart';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_test/angel_test.dart';
+import 'package:http/http.dart' as http;
+import 'package:glob/glob.dart';
+import 'package:test/test.dart';
+
+main() async {
+ group('no timeout', () {
+ TestClient client;
+ DateTime lastModified;
+ http.Response response1, response2;
+
+ setUp(() async {
+ var app = new Angel();
+ var cache = new ResponseCache()
+ ..patterns.addAll([
+ new Glob('/*.txt'),
+ ]);
+
+ app.use(cache.handleRequest);
+
+ app.get(
+ '/date.txt',
+ (ResponseContext res) =>
+ res.write(new DateTime.now().toIso8601String()));
+
+ app.addRoute('PURGE', '*', (RequestContext req) {
+ cache.purge(req.uri.path);
+ print('Purged ${req.uri.path}');
+ });
+
+ app.responseFinalizers.add(cache.responseFinalizer);
+
+ var oldHandler = app.errorHandler;
+ app.errorHandler = (e, req, res) {
+ if (e.error == null) return oldHandler(e, req, res);
+ return Zone.current.handleUncaughtError(e.error, e.stackTrace);
+ };
+
+ client = await connectTo(app);
+ response1 = await client.get('/date.txt');
+ response2 = await client.get('/date.txt');
+ lastModified = new DateTime.now().toUtc();
+ print('Response 1 status: ${response1.statusCode}');
+ print('Response 2 status: ${response2.statusCode}');
+ print('Response 1 body: ${response1.body}');
+ print('Response 2 body: ${response2.body}');
+ print('Response 1 headers: ${response1.headers}');
+ print('Response 2 headers: ${response2.headers}');
+ });
+
+ tearDown(() => client.close());
+
+ test('saves content', () async {
+ expect(response1.body, response2.body);
+ });
+
+ test('saves headers', () async {
+ response1.headers.forEach((k, v) {
+ expect(response2.headers, containsPair(k, v));
+ });
+ });
+
+ test('first response is normal', () {
+ expect(response1.statusCode, 200);
+ });
+
+ test('sends last-modified', () {
+ expect(response2.headers.keys, contains('last-modified'));
+ });
+
+ test('invalidate', () async {
+ await client.sendUnstreamed(
+ 'PURGE', '/date.txt', {'x-http-method-override': 'PURGE'});
+ var response = await client.get('/date.txt');
+ print('Response after invalidation: ${response.body}');
+ expect(response.body, isNot(response1.body));
+ });
+
+ test('sends 304 on if-modified-since', () async {
+ var response = await client.get('/date.txt',
+ headers: {'if-modified-since': formatDateForHttp(lastModified)});
+ expect(response.statusCode, 304);
+ });
+
+ test('last-modified in the past', () async {
+ var response = await client.get('/date.txt', headers: {
+ 'if-modified-since':
+ formatDateForHttp(lastModified.subtract(const Duration(days: 10)))
+ });
+ print('Response: ${response.body}');
+ expect(response.statusCode, 200);
+ expect(response.body, isNot(response1.body));
+ });
+ });
+
+ group('with timeout', () {});
+}