Skip to content

Commit 428f5c7

Browse files
committed
Implement cache control support and range requests
1 parent a866cfd commit 428f5c7

11 files changed

+300
-37
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.1.0
2+
3+
* Added HTTP cache-control support
4+
* Added HTTP range request support for partial content
5+
16
## 0.0.4
27

38
* Update library metadata

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77

88
A simple handler for the Shelf ecosystem to serve files from Flutter assets.
99

10+
## Features
11+
12+
- Serves files from Flutter assets
13+
- Support for default documents (like index.html)
14+
- Content type detection
15+
- Optional caching support with customizable max-age
16+
- Support for HTTP range requests
17+
1018
## Usage
1119

1220
Bind as root handler:
@@ -67,3 +75,24 @@ void main() {
6775
io.serve(app, 'localhost', 8080);
6876
}
6977
```
78+
79+
## Advanced Usage
80+
81+
### Caching
82+
83+
Enable browser caching for better performance:
84+
85+
```dart
86+
import 'package:shelf/shelf_io.dart' as io;
87+
import 'package:shelf_flutter_asset/shelf_flutter_asset.dart';
88+
89+
void main() {
90+
var assetHandler = createAssetHandler(
91+
defaultDocument: 'index.html',
92+
enableCaching: true, // Enable caching
93+
maxAge: 3600, // Cache for 1 hour (in seconds)
94+
);
95+
96+
io.serve(assetHandler, 'localhost', 8080);
97+
}
98+
```

example/pubspec.lock

+49-25
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ packages:
3737
dependency: transitive
3838
description:
3939
name: collection
40-
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
40+
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
4141
url: "https://pub.dev"
4242
source: hosted
43-
version: "1.17.1"
43+
version: "1.18.0"
4444
cupertino_icons:
4545
dependency: "direct main"
4646
description:
@@ -83,14 +83,30 @@ packages:
8383
url: "https://pub.dev"
8484
source: hosted
8585
version: "4.0.2"
86-
js:
86+
leak_tracker:
8787
dependency: transitive
8888
description:
89-
name: js
90-
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
89+
name: leak_tracker
90+
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
9191
url: "https://pub.dev"
9292
source: hosted
93-
version: "0.6.7"
93+
version: "10.0.5"
94+
leak_tracker_flutter_testing:
95+
dependency: transitive
96+
description:
97+
name: leak_tracker_flutter_testing
98+
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
99+
url: "https://pub.dev"
100+
source: hosted
101+
version: "3.0.5"
102+
leak_tracker_testing:
103+
dependency: transitive
104+
description:
105+
name: leak_tracker_testing
106+
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
107+
url: "https://pub.dev"
108+
source: hosted
109+
version: "3.0.1"
94110
lints:
95111
dependency: transitive
96112
description:
@@ -103,26 +119,26 @@ packages:
103119
dependency: transitive
104120
description:
105121
name: matcher
106-
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
122+
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
107123
url: "https://pub.dev"
108124
source: hosted
109-
version: "0.12.15"
125+
version: "0.12.16+1"
110126
material_color_utilities:
111127
dependency: transitive
112128
description:
113129
name: material_color_utilities
114-
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
130+
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
115131
url: "https://pub.dev"
116132
source: hosted
117-
version: "0.2.0"
133+
version: "0.11.1"
118134
meta:
119135
dependency: transitive
120136
description:
121137
name: meta
122-
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
138+
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
123139
url: "https://pub.dev"
124140
source: hosted
125-
version: "1.9.1"
141+
version: "1.15.0"
126142
mime:
127143
dependency: transitive
128144
description:
@@ -135,10 +151,10 @@ packages:
135151
dependency: transitive
136152
description:
137153
name: path
138-
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
154+
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
139155
url: "https://pub.dev"
140156
source: hosted
141-
version: "1.8.3"
157+
version: "1.9.0"
142158
plugin_platform_interface:
143159
dependency: transitive
144160
description:
@@ -161,7 +177,7 @@ packages:
161177
path: ".."
162178
relative: true
163179
source: path
164-
version: "0.0.3"
180+
version: "0.1.0"
165181
sky_engine:
166182
dependency: transitive
167183
description: flutter
@@ -171,26 +187,26 @@ packages:
171187
dependency: transitive
172188
description:
173189
name: source_span
174-
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
190+
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
175191
url: "https://pub.dev"
176192
source: hosted
177-
version: "1.9.1"
193+
version: "1.10.0"
178194
stack_trace:
179195
dependency: transitive
180196
description:
181197
name: stack_trace
182-
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
198+
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
183199
url: "https://pub.dev"
184200
source: hosted
185-
version: "1.11.0"
201+
version: "1.11.1"
186202
stream_channel:
187203
dependency: transitive
188204
description:
189205
name: stream_channel
190-
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
206+
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
191207
url: "https://pub.dev"
192208
source: hosted
193-
version: "2.1.1"
209+
version: "2.1.2"
194210
string_scanner:
195211
dependency: transitive
196212
description:
@@ -211,10 +227,10 @@ packages:
211227
dependency: transitive
212228
description:
213229
name: test_api
214-
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
230+
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
215231
url: "https://pub.dev"
216232
source: hosted
217-
version: "0.5.1"
233+
version: "0.7.2"
218234
typed_data:
219235
dependency: transitive
220236
description:
@@ -231,6 +247,14 @@ packages:
231247
url: "https://pub.dev"
232248
source: hosted
233249
version: "2.1.4"
250+
vm_service:
251+
dependency: transitive
252+
description:
253+
name: vm_service
254+
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
255+
url: "https://pub.dev"
256+
source: hosted
257+
version: "14.2.5"
234258
webview_flutter:
235259
dependency: "direct main"
236260
description:
@@ -264,5 +288,5 @@ packages:
264288
source: hosted
265289
version: "3.0.2"
266290
sdks:
267-
dart: ">=3.0.0-0 <4.0.0"
268-
flutter: ">=3.0.0"
291+
dart: ">=3.3.0 <4.0.0"
292+
flutter: ">=3.18.0-18.0.pre.54"

lib/shelf_flutter_asset.dart

+64-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:io';
2+
import 'dart:math';
23

34
import 'package:flutter/services.dart';
45
import 'package:mime/mime.dart';
@@ -8,6 +9,9 @@ import 'package:shelf/shelf.dart';
89
/// The default resolver for MIME types.
910
final _defaultMimeTypeResolver = MimeTypeResolver();
1011

12+
/// Default cache duration (1 hour)
13+
const _defaultMaxAge = 3600;
14+
1115
/// Creates a Shelf [Handler] that serves files from Flutter assets.
1216
///
1317
/// If requested resource does not exist and [defaultDocument] is specified,
@@ -20,10 +24,16 @@ final _defaultMimeTypeResolver = MimeTypeResolver();
2024
/// If your assets are not in the standard `assets` directory,
2125
/// or you want to share only subtree of the assets path structure,
2226
/// you may use [rootPath] argument to set the root directory for your handler.
27+
///
28+
/// Set [enableCaching] to true to add cache-control headers with [maxAge] seconds
29+
/// (defaults to 1 hour).
30+
///
2331
Handler createAssetHandler(
2432
{String? defaultDocument,
2533
String rootPath = 'assets',
26-
MimeTypeResolver? contentTypeResolver}) {
34+
MimeTypeResolver? contentTypeResolver,
35+
bool enableCaching = false,
36+
int maxAge = _defaultMaxAge}) {
2737
final mimeResolver = contentTypeResolver ?? _defaultMimeTypeResolver;
2838

2939
return (Request request) async {
@@ -52,6 +62,16 @@ Handler createAssetHandler(
5262
if (contentType != null) HttpHeaders.contentTypeHeader: contentType,
5363
};
5464

65+
// Add cache control headers if enabled
66+
if (enableCaching) {
67+
headers[HttpHeaders.cacheControlHeader] = 'max-age=$maxAge, public';
68+
}
69+
70+
// Handle range requests
71+
if (request.headers.containsKey('range')) {
72+
return _handleRangeRequest(request, body, headers);
73+
}
74+
5575
return Response.ok((request.method == 'HEAD') ? null : body,
5676
headers: headers);
5777
};
@@ -66,3 +86,46 @@ Future<Uint8List?> _loadResource(String key) async {
6686

6787
return null;
6888
}
89+
90+
/// Handles HTTP range requests by parsing the Range header and returning
91+
/// the appropriate partial content response.
92+
Response _handleRangeRequest(
93+
Request request, Uint8List body, Map<String, String> headers) {
94+
final rangeHeader = request.headers['range']!;
95+
final match = RegExp(r'bytes=(\d*)-(\d*)').firstMatch(rangeHeader);
96+
97+
if (match == null) {
98+
return Response(416, headers: headers); // Range Not Satisfiable
99+
}
100+
101+
final startStr = match.group(1);
102+
final endStr = match.group(2);
103+
104+
int start = startStr!.isEmpty ? 0 : int.parse(startStr);
105+
int end = endStr!.isEmpty ? body.length - 1 : int.parse(endStr);
106+
107+
// Validate the range
108+
if (start >= body.length || end >= body.length || start > end) {
109+
return Response(416, headers: {
110+
...headers,
111+
'content-range': 'bytes */${body.length}',
112+
});
113+
}
114+
115+
// Limit the length to the actual body size
116+
end = min(end, body.length - 1);
117+
118+
final length = end - start + 1;
119+
final rangeBody = body.sublist(start, end + 1);
120+
121+
final rangeHeaders = {
122+
...headers,
123+
HttpHeaders.contentLengthHeader: '$length',
124+
'content-range': 'bytes $start-$end/${body.length}',
125+
'accept-ranges': 'bytes',
126+
};
127+
128+
return Response(206, // Partial Content
129+
body: (request.method == 'HEAD') ? null : rangeBody,
130+
headers: rangeHeaders);
131+
}

pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: shelf_flutter_asset
22
description: A simple handler for the Shelf ecosystem to serve files from Flutter assets.
3-
version: 0.0.4
3+
version: 0.1.0
44
homepage: https://github.com/r8/shelf_flutter_asset
55
repository: https://github.com/r8/shelf_flutter_asset
66
issue_tracker: https://github.com/r8/shelf_flutter_asset/issues

test/cache_support_test.dart

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:shelf_flutter_asset/shelf_flutter_asset.dart';
3+
4+
import 'test_util.dart';
5+
6+
void main() {
7+
setUpAll(() async {
8+
setUpAssets();
9+
});
10+
11+
group('Cache Support', () {
12+
test('cache-control headers not present by default', () async {
13+
final handler = createAssetHandler();
14+
final request = makeRequest(path: '/index.html');
15+
final response = await handler(request);
16+
17+
expect(response.statusCode, equals(200));
18+
expect(response.headers['cache-control'], isNull);
19+
});
20+
21+
test('cache-control headers present when enabled', () async {
22+
final handler = createAssetHandler(enableCaching: true, maxAge: 3600);
23+
final request = makeRequest(path: '/index.html');
24+
final response = await handler(request);
25+
26+
expect(response.statusCode, equals(200));
27+
expect(response.headers['cache-control'], equals('max-age=3600, public'));
28+
});
29+
30+
test('cache-control headers use custom maxAge', () async {
31+
final handler = createAssetHandler(enableCaching: true, maxAge: 86400);
32+
final request = makeRequest(path: '/index.html');
33+
final response = await handler(request);
34+
35+
expect(response.statusCode, equals(200));
36+
expect(
37+
response.headers['cache-control'], equals('max-age=86400, public'));
38+
});
39+
40+
test('cache-control headers are not added for 404 responses', () async {
41+
final handler = createAssetHandler(enableCaching: true);
42+
final request = makeRequest(path: '/assets/not-found.html');
43+
final response = await handler(request);
44+
45+
expect(response.statusCode, equals(404));
46+
expect(response.headers['cache-control'], isNull);
47+
});
48+
});
49+
}

test/default_document_test.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ void main() {
1717
final request = makeRequest();
1818

1919
final response = await handler(request);
20-
expect(response.statusCode, HttpStatus.notFound);
20+
expect(response.statusCode, equals(HttpStatus.notFound));
2121
});
2222
});
2323

@@ -28,7 +28,7 @@ void main() {
2828
final request = makeRequest();
2929

3030
final response = await handler(request);
31-
expect(response.statusCode, HttpStatus.ok);
31+
expect(response.statusCode, equals(HttpStatus.ok));
3232
});
3333
});
3434
}

0 commit comments

Comments
 (0)