From 0a2ea05ed1d9307180710df982cbd18d3f772bce Mon Sep 17 00:00:00 2001 From: iyifr Date: Wed, 21 Aug 2024 13:09:36 +0100 Subject: [PATCH] refactored middleware logic - error catching middleware now works in async handlers smoothly - cleaned up multiple parts of the code --- bin/run.dart | 60 +++++++++------------- lib/create.dart | 73 ++++++++++++++++++++++---- lib/src/create_error.dart | 4 +- lib/src/error_middleware.dart | 4 +- lib/src/event.dart | 75 +++++++++++++++++---------- lib/src/h4.dart | 96 ++++++++++++++++++++++------------- lib/src/index.dart | 17 ++++--- 7 files changed, 215 insertions(+), 114 deletions(-) diff --git a/bin/run.dart b/bin/run.dart index 8f24d34..25f386e 100644 --- a/bin/run.dart +++ b/bin/run.dart @@ -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 countStream(int max) async* { - for (int i = 1; i <= max; i++) { - // Yield each value asynchronously - await Future.delayed(Duration(seconds: 3)); - yield '
  • hi $i
  • '; - } -} - void main(List arguments) async { - var app = createApp(port: 5173); - var router = createRouter(); - app.use(router); - - // router.get('/', (event) { - // return true; - // }); - - router.get>('/', (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); @@ -57,7 +30,24 @@ void main(List arguments) async { }); router.get>('/int', (event) async { - return Future.error(Exception('Hula ballo')); + try { + Future 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) { diff --git a/lib/create.dart b/lib/create.dart index 542033f..1c071d6 100644 --- a/lib/create.dart +++ b/lib/create.dart @@ -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 @@ -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. diff --git a/lib/src/create_error.dart b/lib/src/create_error.dart index 7182519..19de4ac 100644 --- a/lib/src/create_error.dart +++ b/lib/src/create_error.dart @@ -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 diff --git a/lib/src/error_middleware.dart b/lib/src/error_middleware.dart index f2e31a4..1ab3c62 100644 --- a/lib/src/error_middleware.dart +++ b/lib/src/error_middleware.dart @@ -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)); }; } diff --git a/lib/src/event.dart b/lib/src/event.dart index b711424..ebe2af1 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -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. /// @@ -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"] @@ -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.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) { @@ -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; diff --git a/lib/src/h4.dart b/lib/src/h4.dart index 248e9b5..765a67d 100644 --- a/lib/src/h4.dart +++ b/lib/src/h4.dart @@ -4,27 +4,50 @@ import 'package:h4/src/error_middleware.dart'; import 'package:h4/src/logger.dart'; import 'package:h4/src/port_taken.dart'; import 'package:h4/src/router.dart'; +import 'package:h4/utils/set_response_header.dart'; import '/src/index.dart'; import 'initialize_connection.dart'; import 'event.dart'; +class Either { + final T? left; + final U? right; + + Either.left(this.left) : right = null; + Either.right(this.right) : left = null; +} + /// A middleware function that takes an [H4Event] and has access to it's snapshot. typedef Middleware = void Function(H4Event event)?; +/// The [ErrorHandler] is used to process and potentially report errors that occur +/// during the execution of the application. +/// +/// Parameters: +/// - [String] errorMessage: A description of the error that occurred. +/// - [String?] stackTrace: The stack trace associated with the error, if available. +/// This parameter is optional and may be null. +/// - [H4Event?] event: An optional event object that provides additional context +/// about when or where the error occurred. This parameter is optional and may be null. +/// typedef ErrorHandler = void Function(String, String?, H4Event?); +typedef MiddlewareStack = Map?>?; + class H4 { HttpServer? server; H4Router? router; - Middleware _onRequestHandler; + Middleware _onReq; + late MiddlewareStack middlewares; + // ignore: prefer_function_declarations_over_variables void Function( - String e, - String? s, + String error, + String? stackTrace, H4Event? - event) _errorHandler = (e, s, event) => logger.severe( - 'Error stack Trace: \n$e \n$s \nError occured at path -${event?.path}'); + event) _errorHandler = (error, stackTrace, event) => logger.severe( + '$error\n $stackTrace Error occured while attempting ${event?.method.toUpperCase()} request at - ${event?.path}'); int port; @@ -49,7 +72,11 @@ class H4 { /// // Start the application on the default port (3000) /// final app = H4(); /// ``` - H4({this.port = 3000, bool autoStart = true}) { + H4({ + this.port = 3000, + bool autoStart = true, + this.middlewares, + }) { initLogger(); if (autoStart) { @@ -109,13 +136,14 @@ class H4 { /// h4.onRequest((H4Event event) { /// // Log the request details /// logRequestDetails(event); - /// /// // Validate the request data /// validateRequestData(event); /// }); + /// @deprecated /// ``` + @Deprecated('Set the middlewares in the create app constructor instead') void onRequest(Middleware func) { - _onRequestHandler = func; + _onReq = func; } /// Registers an error handling function to be executed when an error occurs. @@ -149,14 +177,7 @@ class H4 { server!.listen((HttpRequest request) { if (router == null) { logger.warning("Router instance is missing."); - defineEventHandler((event) { - event.statusCode = 404; - return { - "statusCode": 404, - "statusMessage": "Not found", - "message": "Cannot ${event.method.toUpperCase()} - ${event.path}" - }; - }, onRequest: _onRequestHandler, params: {})(request); + return404(request)(middlewares, null); return; } @@ -176,29 +197,14 @@ class H4 { // If we find no match for the request signature - 404. if (handler == null || match == null) { - defineEventHandler((event) { - event.statusCode = 404; - return { - "statusCode": 404, - "statusMessage": "Not found", - "message": "Cannot ${event.method.toUpperCase()} - ${event.path}" - }; - }, onRequest: _onRequestHandler, params: params)(request); - return; + return404(request)(middlewares, null); } // We've found a match - handle the request. else { - defineEventHandler(handler, - onRequest: _onRequestHandler, params: params)(request); - return; + defineEventHandler(handler, middlewares, params)(request); } - } - - /// 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. - on CreateError catch (e, trace) { + } on CreateError catch (e, trace) { defineErrorHandler(_errorHandler, params: params, error: e.message, @@ -217,3 +223,25 @@ class H4 { }); } } + +typedef NotFoundHandler = dynamic Function( + MiddlewareStack stack, Map? params); + +NotFoundHandler return404(HttpRequest request) { + return (stack, params) { + return defineEventHandler( + (event) { + event.statusCode = 404; + setResponseHeader(event, HttpHeaders.contentTypeHeader, + value: 'application/json'); + return { + "statusCode": 404, + "statusMessage": "Not found", + "message": "Cannot ${event.method.toUpperCase()} - ${event.path}" + }; + }, + stack, + params, + )(request); + }; +} diff --git a/lib/src/index.dart b/lib/src/index.dart index 30fc717..57e3ee8 100644 --- a/lib/src/index.dart +++ b/lib/src/index.dart @@ -1,6 +1,7 @@ import 'dart:core'; import 'dart:io'; import 'package:h4/src/event.dart'; +import 'package:h4/src/h4.dart'; /// A function that takes the following parameters /// - A [EventHandler] event handler, @@ -11,27 +12,27 @@ import 'package:h4/src/event.dart'; /// /// It should always close the request with the appropriate status code and message. Function(HttpRequest) defineEventHandler( - EventHandler handler, { + EventHandler handler, + MiddlewareStack? middlewares, + Map? params, { void Function(H4Event)? onRequest, - Map? params, }) { return (HttpRequest request) { - request.response.headers.contentType = null; - // Create an event with the incoming request. var event = H4Event(request); /// Sets the event params so it accessible in the handler. event.eventParams = params ?? {}; // If onRequest is defined, call it with the event. - - if (onRequest != null) { - onRequest(event); + if (middlewares?['onRequest'] != null) { + if (middlewares?['onRequest']?.left != null) { + middlewares?['onRequest']?.left!(event); + } } // Call the handler with the event. var handlerResult = handler(event); - event.respond(handlerResult); + event.respond(handlerResult, middlewares: middlewares); }; }