Skip to content

Commit 95ffeb9

Browse files
committed
Implement support for conditional requests with If-Modified-Since
1 parent 428f5c7 commit 95ffeb9

File tree

4 files changed

+106
-0
lines changed

4 files changed

+106
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* Added HTTP cache-control support
44
* Added HTTP range request support for partial content
5+
* Added support for conditional requests with If-Modified-Since
56

67
## 0.0.4
78

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ A simple handler for the Shelf ecosystem to serve files from Flutter assets.
1414
- Content type detection
1515
- Optional caching support with customizable max-age
1616
- Support for HTTP range requests
17+
- Support for conditional requests with If-Modified-Since
1718

1819
## Usage
1920

lib/shelf_flutter_asset.dart

+25
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import 'package:mime/mime.dart';
66
import 'package:path/path.dart' as p;
77
import 'package:shelf/shelf.dart';
88

9+
/// Application start time used for Last-Modified headers
10+
final _appStartTime = DateTime.now();
11+
912
/// The default resolver for MIME types.
1013
final _defaultMimeTypeResolver = MimeTypeResolver();
1114

@@ -57,11 +60,33 @@ Handler createAssetHandler(
5760

5861
final contentType = mimeResolver.lookup(key);
5962

63+
// Use a fixed last-modified timestamp for static assets
64+
// This is based on the application start time
65+
final lastModified = _appStartTime;
66+
final lastModifiedHeader = HttpDate.format(lastModified);
67+
6068
final headers = {
6169
HttpHeaders.contentLengthHeader: '${body.length}',
6270
if (contentType != null) HttpHeaders.contentTypeHeader: contentType,
71+
HttpHeaders.lastModifiedHeader: lastModifiedHeader,
6372
};
6473

74+
// Check if the resource has been modified since the client's version
75+
final ifModifiedSince = request.headers[HttpHeaders.ifModifiedSinceHeader];
76+
if (ifModifiedSince != null) {
77+
try {
78+
final clientDate = HttpDate.parse(ifModifiedSince);
79+
// Compare dates, treating them as equal if they are within 1 second
80+
// This helps with test reliability and HTTP date precision issues
81+
final difference = lastModified.difference(clientDate).inSeconds.abs();
82+
if (difference < 1) {
83+
return Response.notModified(headers: headers);
84+
}
85+
} catch (_) {
86+
// Invalid date format, ignore header
87+
}
88+
}
89+
6590
// Add cache control headers if enabled
6691
if (enableCaching) {
6792
headers[HttpHeaders.cacheControlHeader] = 'max-age=$maxAge, public';

test/conditional_get_test.dart

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import 'dart:io';
2+
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:shelf_flutter_asset/shelf_flutter_asset.dart';
5+
6+
import 'test_util.dart';
7+
8+
void main() {
9+
setUpAll(() async {
10+
setUpAssets();
11+
});
12+
13+
group('Conditional GET Requests', () {
14+
test('includes Last-Modified header in response', () async {
15+
final handler = createAssetHandler();
16+
final request = makeRequest(
17+
path: '/index.html',
18+
);
19+
final response = await handler(request);
20+
21+
expect(response.statusCode, equals(200));
22+
expect(response.headers[HttpHeaders.lastModifiedHeader], isNotNull);
23+
expect(response.headers[HttpHeaders.lastModifiedHeader], isNotEmpty);
24+
});
25+
26+
test('returns 304 Not Modified for valid If-Modified-Since header',
27+
() async {
28+
// Create a single handler instance for all requests in this test
29+
// to ensure we get the same Last-Modified timestamp
30+
final handler = createAssetHandler();
31+
32+
// First get the Last-Modified value
33+
final initialRequest = makeRequest(path: '/index.html');
34+
final initialResponse = await handler(initialRequest);
35+
final lastModified =
36+
initialResponse.headers[HttpHeaders.lastModifiedHeader]!;
37+
38+
// Then make a conditional request with that same timestamp
39+
final conditionalRequest = makeRequest(
40+
path: '/index.html',
41+
headers: {HttpHeaders.ifModifiedSinceHeader: lastModified},
42+
);
43+
44+
final conditionalResponse = await handler(conditionalRequest);
45+
46+
expect(conditionalResponse.statusCode, equals(304));
47+
expect(await conditionalResponse.read().toList(), isEmpty);
48+
});
49+
50+
test('returns 200 OK for If-Modified-Since header with old date', () async {
51+
final handler = createAssetHandler();
52+
53+
// Use a date from the past
54+
final oldDate = HttpDate.format(DateTime(2000, 1, 1));
55+
56+
final request = makeRequest(
57+
path: '/index.html',
58+
headers: {HttpHeaders.ifModifiedSinceHeader: oldDate},
59+
);
60+
final response = await handler(request);
61+
62+
expect(response.statusCode, equals(200));
63+
expect(await response.read().toList(), isNotEmpty);
64+
});
65+
66+
test('ignores invalid If-Modified-Since header', () async {
67+
final handler = createAssetHandler();
68+
69+
final request = makeRequest(
70+
path: '/index.html',
71+
headers: {HttpHeaders.ifModifiedSinceHeader: 'invalid-date-format'},
72+
);
73+
final response = await handler(request);
74+
75+
expect(response.statusCode, equals(200));
76+
expect(await response.read().toList(), isNotEmpty);
77+
});
78+
});
79+
}

0 commit comments

Comments
 (0)