Skip to content

Commit

Permalink
refactored middleware logic - error catching middleware now works in …
Browse files Browse the repository at this point in the history
…async handlers smoothly - cleaned up multiple parts of the code
  • Loading branch information
iyifr committed Aug 21, 2024
1 parent 73883e7 commit 0a2ea05
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 114 deletions.
60 changes: 25 additions & 35 deletions bin/run.dart
Original file line number Diff line number Diff line change
@@ -1,51 +1,24 @@
// import 'package:console/console.dart' as console;

import 'dart:io';
import 'dart:math';

import 'package:h4/create.dart';
import 'package:h4/src/logger.dart';
import 'package:h4/utils/get_header.dart';
import 'package:h4/utils/get_query.dart';
import 'package:h4/utils/read_request_body.dart';
import 'package:h4/utils/set_response_header.dart';

Stream<String> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
// Yield each value asynchronously
await Future.delayed(Duration(seconds: 3));
yield '<li>hi $i</li>';
}
}

void main(List<String> arguments) async {
var app = createApp(port: 5173);

var router = createRouter();

app.use(router);

// router.get<bool>('/', (event) {
// return true;
// });

router.get<Stream<String>>('/', (event) {
setResponseHeader(event, HttpHeaders.contentTypeHeader,
value: 'text/event-stream');

setResponseHeader(event, HttpHeaders.cacheControlHeader,
value:
"private, no-cache, no-store, no-transform, must-revalidate, max-age=0");

setResponseHeader(event, HttpHeaders.transferEncodingHeader,
value: 'chunked');

setResponseHeader(event, "x-accel-buffering", value: "no");

setResponseHeader(event, 'connection', value: 'keep-alive');
var app = createApp(
port: 5173,
onRequest: (event) => logger.info('$event'),
);

print(event.node["value"]?.response.headers);

return countStream(8);
});
app.use(router);

router.post("/vamos/:id/**", (event) async {
var body = await readRequestBody(event);
Expand All @@ -57,7 +30,24 @@ void main(List<String> arguments) async {
});

router.get<Future<dynamic>>('/int', (event) async {
return Future.error(Exception('Hula ballo'));
try {
Future<String> unreliableFunction() async {
// Simulate some async operation
await Future.delayed(Duration(seconds: 1));

// Randomly succeed or fail
if (Random().nextBool()) {
return "Operation succeeded";
} else {
throw Exception("Random failure occurred");
}
}

String result = await unreliableFunction();
return result;
} catch (e) {
throw CreateError(message: "Error: $e");
}
});

router.get("/vamos", (event) {
Expand Down
73 changes: 64 additions & 9 deletions lib/create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@ import 'package:h4/src/router.dart';
/// Constructs an instance of the `H4` class, which is the main entry point for
/// your application.
///
/// The `H4` constructor initializes the application with an optional port
/// number. If no port is provided, the application will default to using port
/// 3000.
/// It initializes the application with the provided configuration and optionally
/// starts it on the specified [port].
///
/// After creating the `H4` instance, the `start` method is called to begin running the application and listening to requests
/// Parameters:
/// - [port]: The HTTP port to start the application on. Defaults to 3000.
/// - [autoStart]: Whether to immediately start the app once invoked. If set to `false`,
/// you must start the app manually by calling `app.start()`. Defaults to true.
/// - [onRequest]: A middleware function to handle incoming requests before they are
/// processed by the main application logic.
/// - [onError]: An error handler function to process and report errors that occur
/// during the execution of the application.
/// - [afterResponse]: A middleware function to handle outgoing responses before they
/// are sent back to the client.
///
/// To opt out of this behaviour set `autoStart` property to `false`
/// Returns:
/// An instance of [H4] configured with the provided parameters.
///
/// Example usage:
/// ```dart
Expand All @@ -22,11 +31,57 @@ import 'package:h4/src/router.dart';
/// final app = createApp();
///
/// // Start the application manually
/// final app = createApp(autoStart: false)
/// await app.start().then((h4) => print('App started on ${h4.port}'))
/// final app = createApp(autoStart: false);
/// await app.start().then((h4) => print('App started on ${h4.port}'));
///
/// // Using custom middleware and error handling
/// final app = createApp(
/// port: 8080,
/// onRequest: (request) {
/// print('Received request: ${request.method} ${request.url}');
/// return request;
/// },
/// onError: (error, stackTrace, event) {
/// print('Error occurred: $error');
/// if (stackTrace != null) print('Stack trace: $stackTrace');
/// if (event != null) print('Event: ${event.toString()}');
/// },
/// afterResponse: (response) {
/// print('Sending response with status: ${response.statusCode}');
/// return response;
/// },
/// );
/// ```
H4 createApp({int port = 3000, bool autoStart = true}) {
return H4(port: port, autoStart: autoStart);
H4 createApp({
/// The HTTP port to start the application on
int port = 3000,

/// Whether to immediately start the app once invoked.
/// If set to `false`, you must start the app manually by calling `app.start()`
bool autoStart = true,

/// Middleware function to handle incoming requests
Middleware? onRequest,

/// Error handler function to process and report errors
///
/// Parameters:
/// - [String] errorMessage: A description of the error that occurred.
/// - [String?] stackTrace: The stack trace associated with the error, if available.
/// - [H4Event?] event: The event object that provides additional context
/// about the request being processed when the error occurred.
ErrorHandler? onError,

/// Middleware function to handle outgoing responses
Middleware? afterResponse,
}) {
MiddlewareStack middlewares = {
'onRequest': Either.left(onRequest),
'onError': Either.right(onError),
'afterResponse': Either.left(afterResponse),
};

return H4(port: port, autoStart: autoStart, middlewares: middlewares);
}

/// Create a router instance for mapping requests.
Expand Down
4 changes: 3 additions & 1 deletion lib/src/create_error.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:io';

/// Custom HTTP exception class for creating and throwing errors.
/// Handles a specific type of error, `CreateError` when it is thrown explicitly in a catch block
///
/// It returns a function that is invoked with the incoming request which sends a JSON payload to the client with the error details.
class CreateError implements HttpException {
/// Message to send to the client.
@override
Expand Down
4 changes: 3 additions & 1 deletion lib/src/error_middleware.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ Function(HttpRequest) defineErrorHandler(ErrorHandler handler,
var event = H4Event(request);
event.eventParams = params;

// Call the void event handler.
// Call the error middleware.
handler(error, trace.toString(), event);

event.statusCode = statusCode;

setResponseHeader(event, HttpHeaders.contentTypeHeader,
value: 'application/json');
var response = {"statusCode": statusCode, "message": error.toString()};

event.respondWith(jsonEncode(response));
};
}
75 changes: 49 additions & 26 deletions lib/src/event.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'dart:io';
import 'package:h4/create.dart';
import 'package:h4/src/h4.dart';
import 'package:h4/src/logger.dart';
import 'package:h4/utils/set_response_header.dart';

/// Represents an event in the H4 framework.
///
Expand Down Expand Up @@ -39,9 +39,36 @@ class H4Event {
this.params = params;
}

/// The status message associated with the HTTP response status code.
/// Gets or sets the reason phrase for the HTTP response.
///
/// This property allows you to read or modify the reason phrase
/// associated with the HTTP status code of the response.
///
/// The reason phrase must not contain newline characters and
/// should not exceed 512 characters in length.
///
/// Example:
/// ```dart
/// // Get the current status message
/// print(response.statusMessage);
///
/// // Set a custom status message
/// response.statusMessage = 'Custom Reason';
/// ```
///
/// Throws an [ArgumentError] if the provided value is invalid.
String get statusMessage => _request.response.reasonPhrase;

set statusMessage(String value) {
if (value.contains('\n')) {
throw ArgumentError('Reason phrase cannot contain newline characters');
}
if (value.length > 512) {
throw ArgumentError('Reason phrase cannot exceed 512 characters');
}
_request.response.reasonPhrase = value;
}

/// A way to access the request triggering the event.
///
/// The request is available through node["value"]
Expand Down Expand Up @@ -96,41 +123,36 @@ class H4Event {
///
/// If the [handlerResult] is `null`, the response will be closed without writing any content.
/// The [handled] flag is set to `true` after the response is sent.
void respond(dynamic handlerResult) async {
void respond(dynamic handlerResult, {required MiddlewareStack middlewares}) {
if (_handled) {
return;
}

if (handlerResult is Stream) {
_request.response.persistentConnection = true;

final controller = StreamController<dynamic>.broadcast();

handlerResult.listen((value) {
_request.response.write('data: ${value.toString()} \n');
_request.response.flush();
}, onDone: () {
_request.response.write('data: [DONE]\n');
_shutDown();
controller.close();
});

await controller.stream.drain();
return;
}

// Handle Async Handler
if (handlerResult is Future) {
handlerResult
.then((value) => resolveRequest(this, value))
.then((value) => _resolveRequest(this, value))
.onError((error, stackTrace) {
throw CreateError(message: "Internal server error", errorCode: 500);
// Call error middleware
if (middlewares != null && middlewares['onError'] != null) {
middlewares['onError']!.right!(
'$error',
'$stackTrace',
this,
);
}

setResponseHeader(this, HttpHeaders.contentTypeHeader,
value: 'application/json');
var response = {"statusCode": 500, "message": error.toString()};

respondWith(jsonEncode(response));
});
return;
}

// Handle non-async handler.
resolveRequest(this, handlerResult);
_resolveRequest(this, handlerResult);
}

void _writeToClient(dynamic value) {
Expand All @@ -150,9 +172,10 @@ class H4Event {
_request.response.close();
}

resolveRequest(H4Event event, handlerResult) {
_resolveRequest(H4Event event, handlerResult) {
// ignore: type_check_with_null
if (handlerResult is Null) {
event.statusCode = 204;
event.setResponseFormat('null');
event._writeToClient('No content');
return;
Expand Down
Loading

0 comments on commit 0a2ea05

Please sign in to comment.