Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#69): allow to pass values to the next function if the middleware closes the request - closes #69 #87

Merged
merged 4 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .website/core/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ class IsPublic extends Metadata {
}
```

If the metadata value will be set when a request is received then you can create a ContextualizedMetadata.

```dart
import 'package:serinus/serinus.dart';

class MyController extends Controller {

MyController({super.path = '/'});

@override
List<Metadata> get metadata => [
ContextualizedMetadata(
name: 'IsPublic',
value: (context) async => context.request.headers['authorization'] == null,
)
];

}
```

In the example above, the `IsPublic` metadata will be set to `true` if the `authorization` header is not present in the request.

## Add Metadata to a Controller

To add metadata to a controller, you must override the `metadata` getter.
Expand Down
28 changes: 25 additions & 3 deletions .website/core/middlewares.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:serinus/serinus.dart';

class MyMiddleware extends Middleware {
@override
Future<void> use(RequestContext context, InternalResponse response, NextFunction next) async {
Future<void> use(RequestContext context, NextFunction next) async {
print('Middleware executed');
return next();
}
Expand All @@ -33,7 +33,7 @@ import 'package:serinus/serinus.dart';

class MyMiddleware extends Middleware {
@override
Future<void> use(RequestContext context, InternalResponse response, NextFunction next) async {
Future<void> use(RequestContext context, NextFunction next) async {
print('Middleware executed');
return next();
}
Expand Down Expand Up @@ -66,11 +66,33 @@ class MyMiddleware extends Middleware {
MyMiddleware() : super(routes: ['/']);

@override
Future<void> use(RequestContext context, InternalResponse response, NextFunction next) async {
Future<void> use(RequestContext context, NextFunction next) async {
print('Middleware executed');
return next();
}
}
```

This will make the middleware only be applied to the routes that match the pattern `/`.

## Request Blocking Middleware

You can also create a middleware that blocks the request from reaching the controller.

This can be useful if, for example, you want to block requests from a certain IP address or if you want to block requests that don't have a certain header and return early.

The values passed to the `next` function will be returned as the response body and the execution will stop.

```dart
import 'package:serinus/serinus.dart';

class MyMiddleware extends Middleware {
@override
Future<void> use(RequestContext context, NextFunction next) async {
if (context.request.headers['x-custom-header'] != 'value') {
return next('Request blocked');
}
return next();
}
}
```
1 change: 1 addition & 0 deletions .website/core/tracer.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The `TraceEvent` class has the following properties:
| `traced` | The traced event. |

The `traced` property follows a naming convention of:

- `r-*` for route-related events (e.g. route handler, route hooks)
- `m-*` for middleware-related events
- `h-*` for hooks-related events (e.g. global hooks)
4 changes: 2 additions & 2 deletions packages/cors/example/lib/app_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class AppController extends Controller {
on(HelloWorldRoute(), _handleEcho);
}

Future<Response> _handleEcho(RequestContext context) async {
return Response.text('Echo');
Future<String> _handleEcho(RequestContext context) async {
return 'Echo';
}
}
4 changes: 2 additions & 2 deletions packages/rate_limiter/example/lib/app_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class AppController extends Controller {
on(HelloWorldRoute(), _handleEcho);
}

Future<Response> _handleEcho(RequestContext context) async {
return Response.text('Echo');
Future<String> _handleEcho(RequestContext context) async {
return 'Echo';
}
}
2 changes: 1 addition & 1 deletion packages/serinus/lib/src/contexts/request_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import '../http/http.dart';
import 'base_context.dart';

/// The [RequestContext] class is used to create the request context.
final class RequestContext extends BaseContext {
class RequestContext extends BaseContext {
/// The [request] property contains the request of the context.
final Request request;

Expand Down
23 changes: 10 additions & 13 deletions packages/serinus/lib/src/core/middleware.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
import 'package:shelf/shelf.dart' as shelf;

import '../contexts/request_context.dart';
import '../http/http.dart';

/// The [NextFunction] type is used to define the next function of the middleware.
typedef NextFunction = Future<void> Function();
typedef NextFunction = Future<void> Function([Object? data]);

/// The [Middleware] class is used to define a middleware.
abstract class Middleware {
Expand All @@ -18,8 +17,7 @@
const Middleware({this.routes = const ['*']});

/// The [use] method is used to execute the middleware.
Future<void> use(RequestContext context, InternalResponse response,
NextFunction next) async {
Future<void> use(RequestContext context, NextFunction next) async {

Check warning on line 20 in packages/serinus/lib/src/core/middleware.dart

View check run for this annotation

Codecov / codecov/patch

packages/serinus/lib/src/core/middleware.dart#L20

Added line #L20 was not covered by tests
return next();
}

Expand Down Expand Up @@ -49,8 +47,7 @@
///
/// Let's thank [codekeyz](https://github.com/codekeyz) for his work.
@override
Future<void> use(RequestContext context, InternalResponse response,
NextFunction next) async {
Future<void> use(RequestContext context, NextFunction next) async {
final shelf.Request request = _createShelfRequest(context);
late shelf.Response shelfResponse;
if (_handler is shelf.Middleware) {
Expand All @@ -61,22 +58,22 @@
} else {
throw Exception('Handler must be a shelf.Middleware or a shelf.Handler');
}
await _responseFromShelf(context.request, response, shelfResponse);
return next();
final response = await _responseFromShelf(context, shelfResponse);
return next(response);
}

Future<void> _responseFromShelf(
Request req, InternalResponse res, shelf.Response response) async {
Future<dynamic> _responseFromShelf(
RequestContext context, shelf.Response response) async {
Map<String, String> headers = {
for (var key in response.headers.keys)
key: response.headers[key].toString()
};
response.headers.forEach((key, value) => headers[key] = value);
res.status(response.statusCode);
res.headers(headers);
context.res.statusCode = response.statusCode;
context.res.headers.addAll(headers);
final responseBody = await response.readAsString();
if (responseBody.isNotEmpty && !ignoreResponse) {
res.send(utf8.encode(responseBody));
return utf8.encode(responseBody);
}
}

Expand Down
18 changes: 17 additions & 1 deletion packages/serinus/lib/src/handlers/request_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,28 @@ class RequestHandler extends Handler {
context: context,
traced: 'm-${middlewares.elementAt(i).runtimeType}');
final middleware = middlewares.elementAt(i);
await middleware.use(context, response, () async {
await middleware.use(context, ([data]) async {
await config.tracerService.addSyncEvent(
name: TraceEvents.onMiddleware,
request: context.request,
context: context,
traced: 'm-${middlewares.elementAt(i).runtimeType}');
if (data != null) {
if (data.canBeJson()) {
data = parseJsonToResponse(data);
context.res.contentType = ContentType.json;
}
if (data is Uint8List) {
context.res.contentType = ContentType.binary;
}
await response.end(
data: data!,
config: config,
context: context,
request: context.request,
traced: 'm-${middlewares.elementAt(i).runtimeType}');
return;
}
if (i == middlewares.length - 1) {
completer.complete();
}
Expand Down
41 changes: 38 additions & 3 deletions packages/serinus/test/core/middlewares_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:typed_data';

import 'package:http/http.dart' as http;
import 'package:serinus/serinus.dart';
import 'package:shelf/shelf.dart' as shelf;
Expand All @@ -10,6 +12,23 @@
});
}

class TestValueMiddleware extends Middleware {
TestValueMiddleware({super.routes = const ['/value/:v']});

@override
Future<void> use(RequestContext context, NextFunction next) async {
switch (context.params['v']) {
case '1':
return next({'id': 'json-obj'});
case '2':
return next(Uint8List.fromList('Hello, World!'.codeUnits));
default:
context.res.headers['x-middleware'] = 'ok!';

Check warning on line 26 in packages/serinus/test/core/middlewares_test.dart

View check run for this annotation

Codecov / codecov/patch

packages/serinus/test/core/middlewares_test.dart#L26

Added line #L26 was not covered by tests
}
return next();

Check warning on line 28 in packages/serinus/test/core/middlewares_test.dart

View check run for this annotation

Codecov / codecov/patch

packages/serinus/test/core/middlewares_test.dart#L28

Added line #L28 was not covered by tests
}
}

class TestJsonObject with JsonObject {
@override
Map<String, dynamic> toJson() {
Expand All @@ -26,6 +45,7 @@
context.request.headers['x-middleware'],
'ok!'
});
on(Route.get('/value/<v>'), (context) async => 'Hello, World!');
}
}

Expand All @@ -51,16 +71,15 @@
@override
List<Middleware> get middlewares => [
TestModuleMiddleware(),
TestValueMiddleware(),
shelfMiddleware,
shelfAltMiddleware,
TestModuleMiddleware()
];
}

class TestModuleMiddleware extends Middleware {
@override
Future<void> use(RequestContext context, InternalResponse response,
NextFunction next) async {
Future<void> use(RequestContext context, NextFunction next) async {
context.request.headers['x-middleware'] = 'ok!';
return next();
}
Expand Down Expand Up @@ -99,6 +118,22 @@
expect(response.headers.containsKey('x-shelf-middleware'), true);
});

test(
'''when a request is made to a route with a shelf handler as a Middleware in the module, then the shelf middleware should be executed''',
() async {
final response = await http.get(
Uri.parse('http://localhost:3003/value/1'),
);
expect(response.statusCode, 200);
expect(response.body.contains('{"id":"json-obj"}'), true);

final response2 = await http.get(
Uri.parse('http://localhost:3003/value/2'),
);
expect(response2.statusCode, 200);
expect(response2.body.contains('Hello, World!'), true);
});

test(
'''when a request is made to a route with a shelf handler as a Middleware in the module, then the shelf middleware should be executed''',
() async {
Expand Down
3 changes: 1 addition & 2 deletions packages/serinus/test/core/tracer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ class TestMiddleware extends Middleware {
bool hasBeenCalled = false;

@override
Future<void> use(RequestContext context, InternalResponse response,
NextFunction next) async {
Future<void> use(RequestContext context, NextFunction next) async {
await Future.delayed(Duration(milliseconds: 100), () {
hasBeenCalled = true;
});
Expand Down
17 changes: 1 addition & 16 deletions packages/serinus/test/engines/view_engine_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,6 @@ class TestController extends Controller {
}
}

class TestMiddleware extends Middleware {
bool hasBeenCalled = false;

@override
Future<void> use(RequestContext context, InternalResponse response,
NextFunction next) async {
response.on(ResponseEvent.close, (p0) async {
hasBeenCalled = true;
});
next();
}
}

class TestModule extends Module {
TestModule({
super.controllers,
Expand Down Expand Up @@ -54,12 +41,10 @@ void main() async {
group('$ViewEngine', () {
SerinusApplication? app;
final controller = TestController();
final middleware = TestMiddleware();
setUpAll(() async {
app = await serinus.createApplication(
port: 3100,
entrypoint:
TestModule(controllers: [controller], middlewares: [middleware]),
entrypoint: TestModule(controllers: [controller]),
loggingLevel: LogLevel.none);
app?.useViewEngine(ViewEngineTest());
await app?.serve();
Expand Down
8 changes: 2 additions & 6 deletions packages/serinus/test/http/responses_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,7 @@ class TestMiddleware extends Middleware {
bool hasBeenCalled = false;

@override
Future<void> use(RequestContext context, InternalResponse response,
NextFunction next) async {
response.on(ResponseEvent.close, (p0) async {
hasBeenCalled = true;
});
Future<void> use(RequestContext context, NextFunction next) async {
next();
}
}
Expand Down Expand Up @@ -198,7 +194,7 @@ void main() async {
final request =
await HttpClient().getUrl(Uri.parse('http://localhost:3000/text'));
await request.close();
expect(middleware.hasBeenCalled, true);
expect(middleware.hasBeenCalled, false);
});

test(
Expand Down
3 changes: 1 addition & 2 deletions packages/serinus/test/mocks/injectables_mock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ class TestMiddleware extends Middleware {
TestMiddleware() : super(routes: ['*']);

@override
Future<void> use(RequestContext context, InternalResponse response,
NextFunction next) async {
Future<void> use(RequestContext context, NextFunction next) async {
return next();
}
}
8 changes: 5 additions & 3 deletions packages/serinus_config/example/lib/app_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ class AppModule extends Module {
imports: [ConfigModule()],
controllers: [AppController()],
providers: [
DeferredProvider(
(context) async => AppProvider(context.use<ConfigService>()),
inject: [ConfigService])
Provider.deferred(
(ConfigService configService) async => AppProvider(configService),
inject: [ConfigService],
type: AppProvider
)
],
);
}
1 change: 0 additions & 1 deletion packages/serinus_config/lib/src/config_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class ConfigModule extends Module {
final dotEnv = DotEnv(includePlatformEnvironment: true)
..load([dotEnvPath]);
providers = [ConfigService(dotEnv)];
exports = [ConfigService];
return this;
}
}
4 changes: 2 additions & 2 deletions packages/serinus_swagger/example/lib/app_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class AppController extends Controller {
on(HelloWorldRoute(), _handleHelloWorld);
}

Future<Response> _handleHelloWorld(RequestContext context) async {
return Response.text('Hello world');
Future<String> _handleHelloWorld(RequestContext context) async {
return 'Hello, World!';
}
}
Loading
Loading