Skip to content
This repository has been archived by the owner on Oct 16, 2022. It is now read-only.

Commit

Permalink
Add an option to only send OK (200) responses for GET and HEAD requests
Browse files Browse the repository at this point in the history
Fixes https://github.com/dart-lang/shelf_static/issues/53

Also prepare to release v1.1.0
  • Loading branch information
kevmoo committed May 7, 2021
1 parent b8e5137 commit 8d14ad7
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 36 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## 1.1.0-dev

* Added optional `onlyGetMethods` parameter to `createStaticHandler` and
`createFileHandler`. When `true`, only sends an OK (200) response when the
HTTP request method is "GET" or "HEAD".
* Correctly handle `HEAD` requests.

## 1.0.0
Expand Down
2 changes: 1 addition & 1 deletion lib/shelf_static.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

export 'src/static_handler.dart';
export 'src/static_handler.dart' show createFileHandler, createStaticHandler;
99 changes: 66 additions & 33 deletions lib/src/static_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ final _defaultMimeTypeResolver = MimeTypeResolver();
///
/// Specify a custom [contentTypeResolver] to customize automatic content type
/// detection.
Handler createStaticHandler(String fileSystemPath,
{bool serveFilesOutsidePath = false,
String? defaultDocument,
bool listDirectories = false,
bool useHeaderBytesForContentType = false,
MimeTypeResolver? contentTypeResolver}) {
///
/// If [onlyGetMethods] is `true`, then OK (200) responses are only sent for GET
/// and HEAD requests. Others receive a "not found" (404).
Handler createStaticHandler(
String fileSystemPath, {
bool serveFilesOutsidePath = false,
String? defaultDocument,
bool listDirectories = false,
bool useHeaderBytesForContentType = false,
MimeTypeResolver? contentTypeResolver,
bool onlyGetMethods = false,
}) {
final rootDir = Directory(fileSystemPath);
if (!rootDir.existsSync()) {
throw ArgumentError('A directory corresponding to fileSystemPath '
Expand Down Expand Up @@ -82,7 +88,7 @@ Handler createStaticHandler(String fileSystemPath,
}

if (fileFound == null) {
return Response.notFound('Not Found');
return notFound();
}
final file = fileFound;

Expand All @@ -91,7 +97,7 @@ Handler createStaticHandler(String fileSystemPath,

// Do not serve a file outside of the original fileSystemPath
if (!p.isWithin(fileSystemPath, resolvedPath)) {
return Response.notFound('Not Found');
return notFound();
}
}

Expand All @@ -103,31 +109,37 @@ Handler createStaticHandler(String fileSystemPath,
return _redirectToAddTrailingSlash(uri);
}

return _handleFile(request, file, () async {
if (useHeaderBytesForContentType) {
final length =
math.min(mimeResolver.magicNumbersMaxLength, file.lengthSync());

final byteSink = ByteAccumulatorSink();

await file.openRead(0, length).listen(byteSink.add).asFuture();

return mimeResolver.lookup(file.path, headerBytes: byteSink.bytes);
} else {
return mimeResolver.lookup(file.path);
}
});
return _handleFile(
request,
file,
() async {
if (useHeaderBytesForContentType) {
final length =
math.min(mimeResolver.magicNumbersMaxLength, file.lengthSync());

final byteSink = ByteAccumulatorSink();

await file.openRead(0, length).listen(byteSink.add).asFuture();

return mimeResolver.lookup(file.path, headerBytes: byteSink.bytes);
} else {
return mimeResolver.lookup(file.path);
}
},
onlyGetMethods: onlyGetMethods,
);
};
}

Response _redirectToAddTrailingSlash(Uri uri) {
final location = Uri(
scheme: uri.scheme,
userInfo: uri.userInfo,
host: uri.host,
port: uri.port,
path: '${uri.path}/',
query: uri.query);
scheme: uri.scheme,
userInfo: uri.userInfo,
host: uri.host,
port: uri.port,
path: '${uri.path}/',
query: uri.query,
);

return Response.movedPermanently(location.toString());
}
Expand All @@ -154,7 +166,15 @@ File? _tryDefaultFile(String dirPath, String? defaultFile) {
/// This uses the given [contentType] for the Content-Type header. It defaults
/// to looking up a content type based on [path]'s file extension, and failing
/// that doesn't sent a [contentType] header at all.
Handler createFileHandler(String path, {String? url, String? contentType}) {
///
/// If [onlyGetMethods] is `true`, then OK (200) responses are only sent for GET
/// and HEAD requests. Others receive a "not found" (404).
Handler createFileHandler(
String path, {
String? url,
String? contentType,
bool onlyGetMethods = false,
}) {
final file = File(path);
if (!file.existsSync()) {
throw ArgumentError.value(path, 'path', 'does not exist.');
Expand All @@ -166,8 +186,13 @@ Handler createFileHandler(String path, {String? url, String? contentType}) {
url ??= p.toUri(p.basename(path)).toString();

return (request) {
if (request.url.path != url) return Response.notFound('Not Found');
return _handleFile(request, file, () => mimeType);
if (request.url.path != url) return notFound();
return _handleFile(
request,
file,
() => mimeType,
onlyGetMethods: onlyGetMethods,
);
};
}

Expand All @@ -176,8 +201,16 @@ Handler createFileHandler(String path, {String? url, String? contentType}) {
/// This handles caching, and sends a 304 Not Modified response if the request
/// indicates that it has the latest version of a file. Otherwise, it calls
/// [getContentType] and uses it to populate the Content-Type header.
Future<Response> _handleFile(Request request, File file,
FutureOr<String?> Function() getContentType) async {
Future<Response> _handleFile(
Request request,
File file,
FutureOr<String?> Function() getContentType, {
required bool onlyGetMethods,
}) async {
if (onlyGetMethods && !getMethods.contains(request.method)) {
return notFound();
}

final stat = file.statSync();
final ifModifiedSince = request.ifModifiedSince;

Expand Down
7 changes: 7 additions & 0 deletions lib/src/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:shelf/shelf.dart';

DateTime toSecondResolution(DateTime dt) {
if (dt.millisecond == 0) return dt;
return dt.subtract(Duration(milliseconds: dt.millisecond));
}

Response notFound() => Response.notFound('Not Found');

/// HTTP methods which return OK (200) responses with static file servers.
const getMethods = {'GET', 'HEAD'};
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: shelf_static
version: 1.1.0-dev
version: 1.1.0
description: Static file server support for shelf
repository: https://github.com/dart-lang/shelf_static

Expand Down
41 changes: 41 additions & 0 deletions test/basic_file_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:http_parser/http_parser.dart';
import 'package:mime/mime.dart' as mime;
import 'package:path/path.dart' as p;
import 'package:shelf_static/shelf_static.dart';
import 'package:shelf_static/src/util.dart';
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

Expand Down Expand Up @@ -233,4 +234,44 @@ void main() {
expect(response.mimeType, 'image/webp');
});
});

group('HTTP method', () {
for (var method in _httpMethods) {
group(method, () {
test('with', () async {
final handler = createStaticHandler(d.sandbox);

final response =
await makeRequest(handler, '/root.txt', method: method);
expect(response.statusCode, HttpStatus.ok);
expect(response.contentLength, 8);
expect(
await response.readAsString(),
method == 'HEAD' ? isEmpty : 'root txt',
);
});

test('without', () async {
final handler = createStaticHandler(d.sandbox, onlyGetMethods: true);

final response =
await makeRequest(handler, '/root.txt', method: method);

if (getMethods.contains(method)) {
expect(response.statusCode, HttpStatus.ok);
expect(response.contentLength, 8);
expect(
await response.readAsString(),
method == 'HEAD' ? isEmpty : 'root txt',
);
} else {
expect(response.statusCode, HttpStatus.notFound);
expect(await response.readAsString(), 'Not Found');
}
});
});
}
});
}

const _httpMethods = {'PUT', 'POST', 'DELETE', 'PATCH', ...getMethods};
2 changes: 1 addition & 1 deletion test/test_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Handler _rootHandler(String? path, Handler handler) {

return (Request request) {
if (!_ctx.isWithin('/$path', request.requestedUri.path)) {
return Response.notFound('not found');
return notFound();
}
assert(request.handlerPath == '/');

Expand Down

0 comments on commit 8d14ad7

Please sign in to comment.