fix(shelf_static): ignore suffix ranges selecting zero bytes (#541)
Restore the start > end check at the end of the suffix branch in _fileRangeResponse so suffix ranges selecting zero bytes (such as bytes=-0 or any suffix range on a 0-byte file) are ignored and served with 200 OK instead of returning 206 Partial Content with a malformed Content-Range header.
- Add if (start > end) return null; guard to the suffix byte range branch
- Add unit tests for bytes=-0, suffix ranges on 0-byte files, and bytes=-1 boundary
- Update 1.2.0-wip changelog entry
Fixes #540
diff --git a/pkgs/shelf_static/CHANGELOG.md b/pkgs/shelf_static/CHANGELOG.md
index bb5e77a..4887464 100644
--- a/pkgs/shelf_static/CHANGELOG.md
+++ b/pkgs/shelf_static/CHANGELOG.md
@@ -1,7 +1,7 @@
## 1.2.0-wip
* Fix unhandled `FormatException` / HTTP 500 error when parsing oversized integer
- values in HTTP `Range` headers.
+ values in HTTP `Range` headers, and ignore suffix ranges that select zero bytes.
* Replace static, blocking `dart:io` operations (such as `statSync()` and
`File.existsSync()`) with their asynchronous equivalents. This prevents
`createStaticHandler` from blocking the Dart isolate event loop, dramatically
diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart
index e81c5fd..e5d5ee3 100644
--- a/pkgs/shelf_static/lib/src/static_handler.dart
+++ b/pkgs/shelf_static/lib/src/static_handler.dart
@@ -324,6 +324,7 @@
if (start < 0) start = 0;
}
end = actualLength - 1;
+ if (start > end) return null;
} else {
final parsedStart = int.tryParse(startMatch);
final parsedEnd = endMatch.isEmpty ? null : int.tryParse(endMatch);
diff --git a/pkgs/shelf_static/test/create_file_handler_test.dart b/pkgs/shelf_static/test/create_file_handler_test.dart
index b4ca917..de33914 100644
--- a/pkgs/shelf_static/test/create_file_handler_test.dart
+++ b/pkgs/shelf_static/test/create_file_handler_test.dart
@@ -225,6 +225,49 @@
expect(response.statusCode, equals(HttpStatus.partialContent));
expect(response.contentLength, equals(8));
});
+
+ test('ignores request with suffix length 0 (bytes=-0)', () async {
+ final handler = createFileHandler(p.join(d.sandbox, 'file.txt'));
+ final response = await makeRequest(
+ handler,
+ '/file.txt',
+ headers: {'range': 'bytes=-0'},
+ );
+ expect(response.statusCode, equals(HttpStatus.ok));
+ expect(response.headers[HttpHeaders.contentRangeHeader], isNull);
+ expect(response.contentLength, equals(8));
+ expect(response.readAsString(), completion(equals('contents')));
+ });
+
+ test('ignores request with suffix range on empty file', () async {
+ await d.file('empty.txt', '').create();
+ final handler = createFileHandler(p.join(d.sandbox, 'empty.txt'));
+ final response = await makeRequest(
+ handler,
+ '/empty.txt',
+ headers: {'range': 'bytes=-5'},
+ );
+ expect(response.statusCode, equals(HttpStatus.ok));
+ expect(response.headers[HttpHeaders.contentRangeHeader], isNull);
+ expect(response.contentLength, equals(0));
+ expect(response.readAsString(), completion(isEmpty));
+ });
+
+ test('serves last byte for suffix range bytes=-1', () async {
+ final handler = createFileHandler(p.join(d.sandbox, 'file.txt'));
+ final response = await makeRequest(
+ handler,
+ '/file.txt',
+ headers: {'range': 'bytes=-1'},
+ );
+ expect(response.statusCode, equals(HttpStatus.partialContent));
+ expect(
+ response.headers[HttpHeaders.contentRangeHeader],
+ equals('bytes 7-7/8'),
+ );
+ expect(response.contentLength, equals(1));
+ expect(response.readAsString(), completion(equals('s')));
+ });
});
group('throws an ArgumentError for', () {