├── test ├── e2e │ ├── public │ │ └── index.html │ ├── server.dart │ └── http_test.dart └── unit │ ├── route_test.dart │ ├── http_test.dart │ └── router_test.dart ├── lib ├── src │ ├── app_mode.dart │ ├── validation_exception.dart │ ├── isolate_message.dart │ ├── server.dart │ ├── isolate_entry_point.dart │ ├── isolate_supervisor.dart │ ├── route.dart │ ├── response.dart │ ├── servers │ │ └── shelf.dart │ ├── router.dart │ ├── request.dart │ ├── http_dev.dart │ └── http.dart └── utopia_http.dart ├── .gitpod.yml ├── CHANGELOG.md ├── analysis_options.yaml ├── .github └── workflows │ ├── pub_publish.yml │ ├── test.yml │ └── analyze.yml ├── example ├── pubspec.yaml ├── utopia_http_example.dart └── dev_server_example.dart ├── pubspec.yaml ├── .gitpod.Dockerfile ├── .gitignore ├── LICENSE └── README.md /test/e2e/public/index.html: -------------------------------------------------------------------------------- 1 | Hello World Html! -------------------------------------------------------------------------------- /lib/src/app_mode.dart: -------------------------------------------------------------------------------- 1 | /// Application mode 2 | enum AppMode { development, stage, production } 3 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | tasks: 4 | - init: dart pub get 5 | 6 | vscode: 7 | extensions: 8 | - dart-code.dart-code -------------------------------------------------------------------------------- /lib/src/validation_exception.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | /// ValidationException 4 | class ValidationException extends HttpException { 5 | ValidationException(super.message); 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | 3 | - Improve isolates handling 4 | - Update doc 5 | - Fix repository URL 6 | 7 | ## 0.1.0 8 | 9 | - Multi threaded HTTP server 10 | - Easy to use 11 | - Customizable 12 | - Inbuilt dependency injection 13 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | linter: 3 | rules: 4 | - prefer_relative_imports 5 | - avoid_relative_lib_imports 6 | - require_trailing_commas 7 | - always_declare_return_types 8 | - directives_ordering 9 | -------------------------------------------------------------------------------- /.github/workflows/pub_publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to pub.dev 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | publish: 10 | permissions: 11 | id-token: write 12 | uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 13 | with: 14 | environment: pub.dev 15 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: utopia_http_example 2 | description: A starting point for Dart libraries or applications. 3 | version: 1.0.0 4 | publish_to: none 5 | # homepage: https://www.example.com 6 | 7 | environment: 8 | sdk: '>=2.17.5 <4.0.0' 9 | 10 | dependencies: 11 | shelf: 12 | utopia_http: 13 | path: ../ 14 | 15 | dev_dependencies: 16 | lints: ^3.0.0 17 | test: ^1.25.2 18 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: utopia_http 2 | description: A light and easy to get started with HTTP server library for Dart 3 | version: 0.2.0 4 | homepage: https://github.com/utopia-dart/utopia_http 5 | 6 | environment: 7 | sdk: '>=2.18.0 <4.0.0' 8 | 9 | dependencies: 10 | mime: ^1.0.5 11 | http_parser: ^4.0.2 12 | string_scanner: ^1.2.0 13 | shelf: ^1.4.1 14 | shelf_static: ^1.1.2 15 | utopia_di: ^0.2.0 16 | 17 | dev_dependencies: 18 | lints: ^3.0.0 19 | test: ^1.25.2 20 | http: ^1.2.1 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | container: 16 | image: dart:latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Install dependencies 20 | run: dart pub get 21 | - name: Test 22 | run: dart test 23 | -------------------------------------------------------------------------------- /.github/workflows/analyze.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | analyze: 14 | runs-on: ubuntu-latest 15 | container: 16 | image: dart:latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Install dependencies 20 | run: dart pub get 21 | - name: analyze 22 | run: dart analyze 23 | -------------------------------------------------------------------------------- /lib/utopia_http.dart: -------------------------------------------------------------------------------- 1 | /// Utopia Framework is a Dart http framework with minimal must-have 2 | /// features for profressional, simple, advanced and secure web development 3 | /// 4 | library utopia_framework; 5 | 6 | export 'src/app_mode.dart'; 7 | export 'src/http.dart'; 8 | export 'src/http_dev.dart'; 9 | export 'src/request.dart'; 10 | export 'src/response.dart'; 11 | export 'src/route.dart'; 12 | export 'src/router.dart'; 13 | export 'src/server.dart'; 14 | export 'src/servers/shelf.dart'; 15 | export 'src/validation_exception.dart'; 16 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full:2022-06-20-19-54-55 2 | 3 | RUN sudo apt-get update \ 4 | && sudo apt-get install apt-transport-https \ 5 | && wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo gpg --dearmor -o /usr/share/keyrings/dart.gpg \ 6 | && echo 'deb [signed-by=/usr/share/keyrings/dart.gpg arch=amd64] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list 7 | 8 | RUN sudo apt-get update \ 9 | && sudo apt-get install dart 10 | -------------------------------------------------------------------------------- /lib/src/isolate_message.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:isolate' as iso; 3 | 4 | import 'server.dart'; 5 | 6 | class IsolateMessage { 7 | final Handler handler; 8 | final SecurityContext? securityContext; 9 | final String? path; 10 | final String context; 11 | final iso.SendPort sendPort; 12 | final Server server; 13 | 14 | IsolateMessage({ 15 | required this.server, 16 | required this.handler, 17 | required this.context, 18 | this.path, 19 | this.securityContext, 20 | required this.sendPort, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/server.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'request.dart'; 4 | import 'response.dart'; 5 | 6 | /// Server request handler 7 | typedef Handler = FutureOr Function(Request, String); 8 | 9 | /// Server adapter 10 | abstract class Server { 11 | /// Server port 12 | final int port; 13 | 14 | /// Server address 15 | final dynamic address; 16 | 17 | /// Server security context 18 | final SecurityContext? securityContext; 19 | 20 | Server(this.address, this.port, {this.securityContext}); 21 | 22 | /// Start the server 23 | Future start( 24 | Handler handler, { 25 | String context = 'utopia', 26 | String? path, 27 | }); 28 | 29 | /// Stop the server 30 | Future stop(); 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # Avoid committing generated Javascript files: 15 | *.dart.js 16 | *.info.json # Produced by the --dump-info flag. 17 | *.js # When generated by dart2js. Don't specify *.js if your 18 | # project includes source files written in JavaScript. 19 | *.js_ 20 | *.js.deps 21 | *.js.map 22 | 23 | **/*test.http 24 | coverage 25 | pubspec_overrides.yaml 26 | -------------------------------------------------------------------------------- /lib/src/isolate_entry_point.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer' as dev; 2 | import 'dart:isolate'; 3 | 4 | import 'isolate_message.dart'; 5 | import 'isolate_supervisor.dart'; 6 | 7 | Future entrypoint(IsolateMessage message) async { 8 | final ReceivePort receivePort = ReceivePort(); 9 | await message.server.start( 10 | message.handler, 11 | path: message.path, 12 | context: message.context, 13 | ); 14 | 15 | message.sendPort.send(receivePort.sendPort); 16 | receivePort.listen((data) async { 17 | if (data == IsolateSupervisor.messageClose) { 18 | dev.log( 19 | 'Received close message on isolate ${message.context}', 20 | name: 'FINE', 21 | ); 22 | await message.server.stop(); 23 | receivePort.close(); 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/isolate_supervisor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer' as dev; 2 | import 'dart:isolate' as iso; 3 | 4 | class IsolateSupervisor { 5 | final iso.Isolate isolate; 6 | final iso.ReceivePort receivePort; 7 | final String context; 8 | iso.SendPort? _isolateSendPort; 9 | 10 | static const String messageClose = '_CLOSE'; 11 | 12 | IsolateSupervisor({ 13 | required this.isolate, 14 | required this.receivePort, 15 | required this.context, 16 | }); 17 | 18 | void resume() { 19 | receivePort.listen(listen); 20 | isolate.resume(isolate.pauseCapability!); 21 | } 22 | 23 | void stop() { 24 | dev.log('Stopping isolate $context', name: 'FINE'); 25 | _isolateSendPort?.send(messageClose); 26 | receivePort.close(); 27 | } 28 | 29 | void listen(dynamic message) async { 30 | if (message is iso.SendPort) { 31 | _isolateSendPort = message; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 utopia-dart 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/unit/route_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:utopia_http/utopia_http.dart'; 3 | 4 | void main() async { 5 | final route = Route('GET', '/'); 6 | test('method', () { 7 | expect('GET', route.method); 8 | }); 9 | 10 | test('path', () { 11 | expect('/', route.path); 12 | }); 13 | 14 | test('description', () { 15 | expect('', route.description); 16 | route.desc('new route'); 17 | expect('new route', route.description); 18 | }); 19 | 20 | test('params', () { 21 | route.param(key: 'x', defaultValue: '').param(key: 'y', defaultValue: ''); 22 | expect(2, route.params.length); 23 | }); 24 | 25 | test('resources', () { 26 | expect([], route.injections); 27 | 28 | route.inject('user').inject('time').action(() {}); 29 | 30 | expect(2, route.injections.length); 31 | expect('user', route.injections[0]); 32 | expect('time', route.injections[1]); 33 | }); 34 | 35 | test('label', () { 36 | expect(route.getLabel('key', defaultValue: 'default'), 'default'); 37 | route.label('key', 'value'); 38 | expect(route.getLabel('key', defaultValue: 'default'), 'value'); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/unit/http_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:utopia_di/utopia_validators.dart'; 5 | import 'package:utopia_http/utopia_http.dart'; 6 | 7 | void main() async { 8 | final http = Http(ShelfServer('localhost', 8080)); 9 | http.setResource('rand', () => Random().nextInt(100)); 10 | http.setResource( 11 | 'first', 12 | (String second) => 'first-$second', 13 | injections: ['second'], 14 | ); 15 | http.setResource('second', () => 'second'); 16 | 17 | group('Http', () { 18 | test('resource injection', () async { 19 | final resource = http.getResource('rand'); 20 | 21 | final route = Route('GET', '/path'); 22 | route 23 | .inject('rand') 24 | .param( 25 | key: 'x', 26 | defaultValue: 'x-def', 27 | description: 'x param', 28 | validator: Text(length: 200), 29 | ) 30 | .param( 31 | key: 'y', 32 | defaultValue: 'y-def', 33 | description: 'y param', 34 | validator: Text(length: 200), 35 | ) 36 | .action((int rand, String x, String y) => Response("$x-$y-$rand")); 37 | final res = await http.execute( 38 | route, 39 | Request( 40 | 'GET', 41 | Uri.parse('/path'), 42 | ), 43 | 'utopia', 44 | ); 45 | expect(res.body, 'x-def-y-def-$resource'); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/route.dart: -------------------------------------------------------------------------------- 1 | import 'package:utopia_di/utopia_di.dart'; 2 | 3 | import 'request.dart'; 4 | 5 | /// Route 6 | /// 7 | /// A http route 8 | class Route extends Hook { 9 | /// HTTP method 10 | String method = ''; 11 | 12 | /// Whether or not hook is enabled 13 | bool hook = true; 14 | 15 | /// Route path 16 | String path; 17 | static int counter = 0; 18 | final List _aliases = []; 19 | final Map labels = {}; 20 | 21 | final Map _pathParams = {}; 22 | 23 | Route(this.method, this.path) : super() { 24 | Route.counter++; 25 | order = counter; 26 | } 27 | 28 | /// Get route aliases 29 | List get aliases => _aliases; 30 | Map get pathParams => _pathParams; 31 | 32 | /// Add a route alias 33 | Route alias(String path) { 34 | if (!_aliases.contains(path)) { 35 | _aliases.add(path); 36 | } 37 | 38 | return this; 39 | } 40 | 41 | /// Set path params 42 | void setPathParam(String key, int index) { 43 | _pathParams[key] = index; 44 | } 45 | 46 | /// Get values for path params 47 | Map getPathValues(Request request) { 48 | var pathValues = {}; 49 | var parts = request.url.path.split('/').where((part) => part.isNotEmpty); 50 | 51 | for (var entry in pathParams.entries) { 52 | if (entry.value < parts.length) { 53 | pathValues[entry.key] = parts.elementAt(entry.value); 54 | } 55 | } 56 | 57 | return pathValues; 58 | } 59 | 60 | /// Set route label 61 | Route label(String key, String value) { 62 | labels[key] = value; 63 | return this; 64 | } 65 | 66 | /// Get route label 67 | String? getLabel(String key, {String? defaultValue}) { 68 | return labels[key] ?? defaultValue; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/response.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | class Response { 5 | /// Response body 6 | String body; 7 | 8 | /// HTTP status 9 | int status = 200; 10 | 11 | /// Content type 12 | ContentType contentType = ContentType.text; 13 | 14 | /// Disable payload 15 | bool disablePayload = false; 16 | final Map _headers; 17 | final List _cookies = []; 18 | 19 | /// Get headers 20 | Map get headers { 21 | _headers[HttpHeaders.contentTypeHeader] = contentType.toString(); 22 | _headers[HttpHeaders.setCookieHeader] = 23 | _cookies.map((cookie) => cookie.toString()).join(','); 24 | return _headers; 25 | } 26 | 27 | /// Get cookies 28 | List get cookies => _cookies; 29 | 30 | Response(this.body, {this.status = 200, Map? headers}) 31 | : _headers = headers ?? {}; 32 | 33 | /// Add header 34 | Response addHeader(String key, String value) { 35 | _headers[key] = value; 36 | return this; 37 | } 38 | 39 | /// Remove header 40 | Response removeHeader(String key) { 41 | _headers.remove(key); 42 | return this; 43 | } 44 | 45 | /// Add cookie 46 | Response addCookie(Cookie cookie) { 47 | _cookies.add(cookie); 48 | return this; 49 | } 50 | 51 | /// Remove cookie 52 | Response removeCookie(Cookie cookie) { 53 | _cookies.removeWhere((element) => element.name == cookie.name); 54 | return this; 55 | } 56 | 57 | /// Set json response 58 | void json(Map data, {int status = HttpStatus.ok}) { 59 | contentType = ContentType.json; 60 | body = jsonEncode(data); 61 | } 62 | 63 | /// Set text response 64 | void text(String data, {int status = HttpStatus.ok}) { 65 | contentType = ContentType.text; 66 | this.status = status; 67 | body = data; 68 | } 69 | 70 | /// Set HTML response 71 | void html(String data, {int status = HttpStatus.ok}) { 72 | contentType = ContentType.html; 73 | this.status = status; 74 | body = data; 75 | } 76 | 77 | /// Set empty response 78 | void noContent() { 79 | status = HttpStatus.noContent; 80 | body = ''; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/e2e/server.dart: -------------------------------------------------------------------------------- 1 | import 'package:utopia_di/utopia_validators.dart'; 2 | import 'package:utopia_http/utopia_http.dart'; 3 | 4 | void initHttp(Http http) { 5 | http 6 | .error() 7 | .inject('error') 8 | .inject('response') 9 | .action((Exception error, Response response) { 10 | if (error is ValidationException) { 11 | response.status = 400; 12 | response.body = error.message; 13 | } 14 | return response; 15 | }); 16 | 17 | http.get('/').action(() { 18 | return Response('Hello!'); 19 | }); 20 | 21 | http.get('/empty').action(() {}); 22 | 23 | http 24 | .post('/create') 25 | .param(key: 'userId') 26 | .param(key: 'file') 27 | .inject('request') 28 | .inject('response') 29 | .action( 30 | (String userId, dynamic file, Request request, Response response) { 31 | response.text(file['filename']); 32 | return response; 33 | }); 34 | 35 | http 36 | .get('/hello') 37 | .inject('request') 38 | .inject('response') 39 | .action((Request request, Response response) { 40 | response.text('Hello World!'); 41 | return response; 42 | }); 43 | 44 | http 45 | .get('/users/:userId') 46 | .param( 47 | key: 'userId', 48 | validator: Text(length: 10), 49 | defaultValue: '', 50 | description: 'Users unique ID', 51 | ) 52 | .inject('response') 53 | .action((String userId, Response response) { 54 | response.text(userId); 55 | return response; 56 | }); 57 | 58 | http 59 | .post('/users') 60 | .param(key: 'userId') 61 | .param(key: 'name') 62 | .param(key: 'email') 63 | .inject('response') 64 | .inject('request') 65 | .action(( 66 | String userId, 67 | String name, 68 | String email, 69 | Response response, 70 | Request request, 71 | ) { 72 | response.json({ 73 | "userId": userId, 74 | "email": email, 75 | "name": name, 76 | }); 77 | return response; 78 | }); 79 | } 80 | 81 | Future shelfServer() async { 82 | final http = Http(ShelfServer('localhost', 3030), path: 'test/e2e/public'); 83 | initHttp(http); 84 | await http.start(); 85 | return http; 86 | } 87 | -------------------------------------------------------------------------------- /example/utopia_http_example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:utopia_http/utopia_http.dart'; 3 | 4 | void main() async { 5 | final address = InternetAddress.anyIPv4; 6 | final port = Http.getEnv('PORT', 8080); 7 | final app = Http(ShelfServer(address, port), threads: 8); 8 | 9 | app.get('/').inject('request').inject('response').action( 10 | (Request request, Response response) { 11 | response.text('Hello world'); 12 | return response; 13 | }, 14 | ); 15 | app 16 | .get('/hello-world') 17 | .inject('request') 18 | .inject('response') 19 | .action((Request request, Response response) { 20 | response.text('Hello world'); 21 | return response; 22 | }); 23 | 24 | app 25 | .get('/users/:userId') 26 | .param(key: 'userId', defaultValue: '', description: 'Users unique ID') 27 | .inject('response') 28 | .action((String userId, Response response) { 29 | response.text(userId); 30 | return response; 31 | }); 32 | 33 | app 34 | .get('/users/:userId/jhyap/:messing') 35 | .param(key: 'userId', defaultValue: '', description: 'Users unique ID') 36 | .param(key: 'messing', defaultValue: 'messing') 37 | .inject('response') 38 | .action((String userId, String messing, Response response) { 39 | response.text('tap tap'); 40 | return response; 41 | }); 42 | 43 | app 44 | .post('/users') 45 | .param(key: 'userId') 46 | .param(key: 'name') 47 | .param(key: 'email') 48 | .inject('response') 49 | .inject('request') 50 | .action(( 51 | String userId, 52 | String name, 53 | String email, 54 | Response response, 55 | Request request, 56 | ) { 57 | response.json({ 58 | 'userId': userId, 59 | 'name': name, 60 | 'email': email, 61 | }); 62 | return response; 63 | }); 64 | 65 | app 66 | .get('/users/:userId/jhyap') 67 | .param(key: 'userId', defaultValue: '', description: 'Users unique ID') 68 | .inject('response') 69 | .action((String userId, Response response) { 70 | print(userId); 71 | response.text('Jhyap'); 72 | return response; 73 | }); 74 | 75 | await app.start(); 76 | print("server started at http://${address.address}:$port"); 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/servers/shelf.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:shelf/shelf.dart' as shelf; 5 | import 'package:shelf/shelf_io.dart' as shelf_io; 6 | import 'package:shelf_static/shelf_static.dart'; 7 | 8 | import '../request.dart'; 9 | import '../response.dart'; 10 | import '../server.dart'; 11 | 12 | /// ShelfServer 13 | /// 14 | /// Create a server 15 | class ShelfServer extends Server { 16 | HttpServer? _server; 17 | ShelfServer(super.address, super.port, {super.securityContext}); 18 | 19 | /// Start the server 20 | @override 21 | Future start( 22 | Handler handler, { 23 | String context = 'utopia', 24 | String? path, 25 | }) async { 26 | var shelfHandler = (shelf.Request request) => _handleRequest( 27 | request, 28 | context, 29 | handler, 30 | ); 31 | if (path != null) { 32 | shelfHandler = shelf.Cascade() 33 | .add(createStaticHandler(path)) 34 | .add( 35 | (request) => _handleRequest( 36 | request, 37 | context, 38 | handler, 39 | ), 40 | ) 41 | .handler; 42 | } 43 | 44 | _server = await shelf_io.serve( 45 | shelfHandler, 46 | address, 47 | port, 48 | securityContext: securityContext, 49 | shared: true, 50 | ); 51 | } 52 | 53 | /// Stop servers 54 | @override 55 | Future stop() async { 56 | await _server?.close(force: true); 57 | } 58 | 59 | FutureOr _handleRequest( 60 | shelf.Request sheflRequest, 61 | String context, 62 | Handler handler, 63 | ) async { 64 | final request = _fromShelfRequest(sheflRequest); 65 | final response = await handler.call(request, context); 66 | return _toShelfResponse(response); 67 | } 68 | 69 | Request _fromShelfRequest(shelf.Request shelfRequest) { 70 | return Request( 71 | shelfRequest.method, 72 | shelfRequest.url, 73 | body: shelfRequest.read(), 74 | headers: shelfRequest.headers, 75 | headersAll: shelfRequest.headersAll, 76 | contentType: shelfRequest.headers[HttpHeaders.contentTypeHeader], 77 | ); 78 | } 79 | 80 | shelf.Response _toShelfResponse(Response response) { 81 | final res = shelf.Response( 82 | response.status, 83 | body: response.body, 84 | headers: response.headers, 85 | ); 86 | return res; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/dev_server_example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:utopia_http/utopia_http.dart'; 3 | 4 | void main() async { 5 | await HttpDev.start( 6 | script: () async { 7 | final address = InternetAddress.anyIPv4; 8 | final port = int.tryParse(Http.getEnv('PORT', '8080')) ?? 8080; 9 | 10 | print('🚀 Starting Utopia HTTP Server with Hot Reload...'); 11 | print('🌐 Address: ${address.address}:$port'); 12 | print(''); 13 | 14 | // Create HTTP server 15 | final app = Http( 16 | ShelfServer(address, port), 17 | threads: 2, 18 | mode: AppMode.development, 19 | ); 20 | 21 | // Define routes 22 | app.get('/').inject('response').action((Response response) { 23 | response.text( 24 | 'Hello from Utopia HTTP with Hot Reload! 🔥 [AUTO-RELOAD Active?]', 25 | ); 26 | return response; 27 | }); 28 | 29 | app.get('/api/status').inject('response').action((Response response) { 30 | response.json({ 31 | 'status': 'running', 32 | 'mode': 'development', 33 | 'hot_reload': true, 34 | 'version': '2.0', 35 | 'timestamp': DateTime.now().toIso8601String(), 36 | }); 37 | return response; 38 | }); 39 | 40 | app 41 | .get('/api/hello/:name') 42 | .param( 43 | key: 'name', 44 | defaultValue: 'World', 45 | description: 'Name to greet', 46 | ) 47 | .inject('response') 48 | .action((String name, Response response) { 49 | response.json({ 50 | 'greeting': 'Hello, $name! 👋', 51 | 'timestamp': DateTime.now().toIso8601String(), 52 | }); 53 | return response; 54 | }); 55 | 56 | // Add request logging 57 | app.init().inject('request').action((Request request) { 58 | final timestamp = DateTime.now().toIso8601String(); 59 | final method = request.method.padRight(6); 60 | print('📥 $timestamp - $method ${request.url}'); 61 | }); 62 | 63 | // Add error handling 64 | app.error().inject('error').inject('response').action( 65 | (Exception error, Response response) { 66 | response.json( 67 | { 68 | 'error': error.toString(), 69 | 'timestamp': DateTime.now().toIso8601String(), 70 | }, 71 | status: HttpStatus.internalServerError, 72 | ); 73 | return response; 74 | }, 75 | ); 76 | 77 | try { 78 | await app.start(); 79 | 80 | // Keep the server running 81 | await ProcessSignal.sigint.watch().first; 82 | } catch (e) { 83 | print('❌ Failed to start server: $e'); 84 | exit(1); 85 | } finally { 86 | await app.stop(); 87 | app.dispose(); 88 | } 89 | }, 90 | watchPaths: ['lib', 'example'], 91 | watchExtensions: ['.dart'], 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /lib/src/router.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'route.dart'; 4 | 5 | /// Router 6 | class Router { 7 | /// Placeholder token for route 8 | static const String placeholderToken = ':::'; 9 | 10 | Map> _routes = { 11 | 'GET': {}, 12 | 'POST': {}, 13 | 'PUT': {}, 14 | 'PATCH': {}, 15 | 'DELETE': {}, 16 | }; 17 | 18 | List _params = []; 19 | 20 | /// Get list of all the routes 21 | UnmodifiableMapView> getRoutes() { 22 | return UnmodifiableMapView(_routes); 23 | } 24 | 25 | /// Add a route 26 | void addRoute(Route route) { 27 | List result = preparePath(route.path); 28 | String path = result[0]; 29 | Map params = result[1]; 30 | 31 | if (!_routes.containsKey(route.method)) { 32 | throw Exception("Method (${route.method}) not supported."); 33 | } 34 | 35 | if (_routes[route.method]!.containsKey(path)) { 36 | throw Exception("Route for (${route.method}:$path) already registered."); 37 | } 38 | 39 | params.forEach((key, index) { 40 | route.setPathParam(key, index); 41 | }); 42 | 43 | _routes[route.method]![path] = (route); 44 | 45 | for (String alias in route.aliases) { 46 | List aliasResult = preparePath(alias); 47 | String aliasPath = aliasResult[0]; 48 | _routes[route.method]![aliasPath] = route; 49 | } 50 | } 51 | 52 | /// Match a route for given method and path 53 | Route? match(String method, String path) { 54 | if (!_routes.containsKey(method)) { 55 | return null; 56 | } 57 | 58 | List parts = path.split('/').where((p) => p.isNotEmpty).toList(); 59 | int length = parts.length - 1; 60 | List filteredParams = _params.where((i) => i <= length).toList(); 61 | 62 | for (List sample in combinations(filteredParams)) { 63 | sample = sample.where((i) => i <= length).toList(); 64 | String match = parts 65 | .asMap() 66 | .entries 67 | .map( 68 | (entry) => 69 | sample.contains(entry.key) ? placeholderToken : entry.value, 70 | ) 71 | .join('/'); 72 | 73 | if (_routes[method]!.containsKey(match)) { 74 | return _routes[method]![match]!; 75 | } 76 | } 77 | 78 | return null; 79 | } 80 | 81 | Iterable> combinations(List set) { 82 | final result = >[[]]; 83 | 84 | for (final element in set) { 85 | final newCombinations = >[]; 86 | for (final combination in result) { 87 | final ret = [element, ...combination]; 88 | newCombinations.add(ret); 89 | } 90 | result.addAll(newCombinations); 91 | } 92 | 93 | return result; 94 | } 95 | 96 | /// Prepare path 97 | List preparePath(String path) { 98 | List parts = path.split('/').where((p) => p.isNotEmpty).toList(); 99 | String prepare = ''; 100 | Map params = {}; 101 | 102 | for (int key = 0; key < parts.length; key++) { 103 | String part = parts[key]; 104 | if (key != 0) { 105 | prepare += '/'; 106 | } 107 | 108 | if (part.startsWith(':')) { 109 | prepare += placeholderToken; 110 | params[part.substring(1)] = key; 111 | if (!_params.contains(key)) { 112 | _params.add(key); 113 | } 114 | } else { 115 | prepare += part; 116 | } 117 | } 118 | 119 | return [prepare, params]; 120 | } 121 | 122 | /// Reset router 123 | void reset() { 124 | _params = []; 125 | _routes = { 126 | 'GET': {}, 127 | 'POST': {}, 128 | 'PUT': {}, 129 | 'PATCH': {}, 130 | 'DELETE': {}, 131 | }; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /test/unit/router_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:utopia_http/utopia_http.dart'; 3 | 4 | void main() { 5 | final router = Router(); 6 | group('router', () { 7 | setUp(() { 8 | router.reset(); 9 | }); 10 | 11 | test('Can match URL', () { 12 | final routeIndex = Route(Request.get, '/'); 13 | final routeAbout = Route(Request.get, '/about'); 14 | final routeAboutMe = Route(Request.get, '/about/me'); 15 | 16 | router.addRoute(routeIndex); 17 | router.addRoute(routeAbout); 18 | router.addRoute(routeAboutMe); 19 | 20 | expect(router.match(Request.get, '/'), equals(routeIndex)); 21 | expect( 22 | router.match(Request.get, '/about'), 23 | equals(routeAbout), 24 | ); 25 | expect( 26 | router.match(Request.get, '/about/me'), 27 | equals(routeAboutMe), 28 | ); 29 | }); 30 | 31 | test('Can match URL with placeholder', () { 32 | final routeBlog = Route(Request.get, '/blog'); 33 | final routeBlogAuthors = Route(Request.get, '/blog/authors'); 34 | final routeBlogAuthorsComments = 35 | Route(Request.get, '/blog/authors/comments'); 36 | final routeBlogPost = Route(Request.get, '/blog/:post'); 37 | final routeBlogPostComments = Route(Request.get, '/blog/:post/comments'); 38 | final routeBlogPostCommentsSingle = 39 | Route(Request.get, '/blog/:post/comments/:comment'); 40 | 41 | router.addRoute(routeBlog); 42 | router.addRoute(routeBlogAuthors); 43 | router.addRoute(routeBlogAuthorsComments); 44 | router.addRoute(routeBlogPost); 45 | router.addRoute(routeBlogPostComments); 46 | router.addRoute(routeBlogPostCommentsSingle); 47 | 48 | expect(router.match(Request.get, '/blog'), equals(routeBlog)); 49 | expect( 50 | router.match(Request.get, '/blog/authors'), 51 | equals(routeBlogAuthors), 52 | ); 53 | expect( 54 | router.match(Request.get, '/blog/authors/comments'), 55 | equals(routeBlogAuthorsComments), 56 | ); 57 | expect( 58 | router.match(Request.get, '/blog/:post'), 59 | equals(routeBlogPost), 60 | ); 61 | expect( 62 | router.match(Request.get, '/blog/:post/comments'), 63 | equals(routeBlogPostComments), 64 | ); 65 | expect( 66 | router.match(Request.get, '/blog/:post/comments/:comment'), 67 | equals(routeBlogPostCommentsSingle), 68 | ); 69 | }); 70 | 71 | test('Can match HTTP method', () { 72 | final routeGET = Route(Request.get, '/'); 73 | final routePOST = Route(Request.post, '/'); 74 | 75 | router.addRoute(routeGET); 76 | router.addRoute(routePOST); 77 | 78 | expect(router.match(Request.get, '/'), equals(routeGET)); 79 | expect(router.match(Request.post, '/'), equals(routePOST)); 80 | 81 | expect(router.match(Request.post, '/'), isNot(routeGET)); 82 | expect(router.match(Request.get, '/'), isNot(routePOST)); 83 | }); 84 | 85 | test('Can match alias', () { 86 | final routeGET = Route(Request.get, '/target'); 87 | routeGET.alias('/alias').alias('/alias2'); 88 | 89 | router.addRoute(routeGET); 90 | 91 | expect(router.match(Request.get, '/target'), equals(routeGET)); 92 | expect(router.match(Request.get, '/alias'), equals(routeGET)); 93 | expect(router.match(Request.get, '/alias2'), equals(routeGET)); 94 | }); 95 | 96 | test('Cannot find unknown route by path', () { 97 | expect(router.match(Request.get, '/404'), isNull); 98 | }); 99 | 100 | test('Cannot find unknown route by method', () { 101 | final route = Route(Request.get, '/404'); 102 | 103 | router.addRoute(route); 104 | 105 | expect(router.match(Request.get, '/404'), equals(route)); 106 | }); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /test/e2e/http_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:http/http.dart' as http; 4 | import 'package:test/test.dart'; 5 | import 'package:utopia_http/utopia_http.dart'; 6 | 7 | import 'server.dart' as server; 8 | 9 | void main() { 10 | group('Http Shelf Server', () { 11 | Http? http; 12 | setUp(() async { 13 | http = await server.shelfServer(); 14 | }); 15 | 16 | test('Basic Response', basicResponseTest); 17 | 18 | test('No param, injection', noParamInjectionTest); 19 | 20 | test('Param', paramsTest); 21 | 22 | test('Param Validation', paramValidationTest); 23 | 24 | test('JSON', jsonTest); 25 | 26 | test('static', staticFileTest); 27 | 28 | test('file upload', fileUpload); 29 | 30 | tearDown(() async { 31 | await http?.stop(); 32 | }); 33 | }); 34 | } 35 | 36 | void fileUpload() async { 37 | var uri = Uri.parse('http://localhost:3030/create'); 38 | var request = http.MultipartRequest('POST', uri) 39 | ..fields['userId'] = '1' 40 | ..files.add( 41 | await http.MultipartFile.fromPath( 42 | 'file', 43 | 'test/e2e/public/index.html', 44 | filename: 'index', 45 | ), 46 | ); 47 | var res = await request.send(); 48 | final out = await utf8.decodeStream(res.stream); 49 | expect(res.statusCode, 200); 50 | expect(out, 'index'); 51 | } 52 | 53 | void staticFileTest() async { 54 | final client = HttpClient(); 55 | final req = 56 | await client.getUrl(Uri.parse('http://localhost:3030/index.html')); 57 | final res = await req.close(); 58 | final output = await utf8.decodeStream(res); 59 | assert(res.headers.contentType.toString().contains('text/html')); 60 | expect(output, 'Hello World Html!'); 61 | } 62 | 63 | void jsonTest() async { 64 | final client = HttpClient(); 65 | final req = await client.postUrl(Uri.parse('http://localhost:3030/users')); 66 | req.headers.set(HttpHeaders.contentTypeHeader, ContentType.json.toString()); 67 | final data = { 68 | "userId": "myuserid", 69 | "email": "email@gmail.com", 70 | "name": "myname", 71 | }; 72 | req.write(jsonEncode(data)); 73 | final res = await req.close(); 74 | final output = await utf8.decodeStream(res); 75 | expect(res.headers.contentType.toString(), ContentType.json.toString()); 76 | expect( 77 | output, 78 | '{"userId":"myuserid","email":"email@gmail.com","name":"myname"}', 79 | ); 80 | } 81 | 82 | void paramValidationTest() async { 83 | final client = HttpClient(); 84 | final req = await client.getUrl( 85 | Uri.parse('http://localhost:3030/users/verylonguseridnotvalidate'), 86 | ); 87 | final res = await req.close(); 88 | final output = await utf8.decodeStream(res); 89 | expect( 90 | output, 91 | 'Invalid userId: Value must be a valid string and no longer than 10 chars', 92 | ); 93 | } 94 | 95 | void paramsTest() async { 96 | final client = HttpClient(); 97 | final req = 98 | await client.getUrl(Uri.parse('http://localhost:3030/users/myuserid')); 99 | final res = await req.close(); 100 | final output = await utf8.decodeStream(res); 101 | expect(output, 'myuserid'); 102 | } 103 | 104 | void noParamInjectionTest() async { 105 | final client = HttpClient(); 106 | final req = await client.getUrl(Uri.parse('http://localhost:3030')); 107 | final res = await req.close(); 108 | final output = await utf8.decodeStream(res); 109 | expect(output, 'Hello!'); 110 | } 111 | 112 | void actionNullReturnTest() async { 113 | final client = HttpClient(); 114 | final req = await client.getUrl(Uri.parse('http://localhost:3030/empty')); 115 | final res = await req.close(); 116 | final output = await utf8.decodeStream(res); 117 | expect(output, ''); 118 | } 119 | 120 | void basicResponseTest() async { 121 | final client = HttpClient(); 122 | final req = await client.getUrl(Uri.parse('http://localhost:3030/hello')); 123 | final res = await req.close(); 124 | final output = await utf8.decodeStream(res); 125 | expect(output, 'Hello World!'); 126 | } 127 | -------------------------------------------------------------------------------- /lib/src/request.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:http_parser/http_parser.dart'; 3 | import 'package:mime/mime.dart'; 4 | import 'package:string_scanner/string_scanner.dart'; 5 | 6 | class Request { 7 | /// GET method 8 | static const String get = 'GET'; 9 | 10 | /// POST method 11 | static const String post = 'POST'; 12 | 13 | /// PUT method 14 | static const String put = 'PUT'; 15 | 16 | /// PATCH method 17 | static const String patch = 'PATCH'; 18 | 19 | /// DELETE method 20 | static const String delete = 'DELETE'; 21 | 22 | /// HEAD method 23 | static const String head = 'HEAD'; 24 | 25 | /// OPTIONS method 26 | static const String options = 'OPTIONS'; 27 | 28 | /// Uri 29 | final Uri url; 30 | 31 | /// Method 32 | final String method; 33 | 34 | /// Headers 35 | final Map headers; 36 | 37 | /// All headers 38 | final Map> headersAll; 39 | 40 | /// Encoding 41 | final Encoding? encoding; 42 | 43 | /// Content type 44 | final String? contentType; 45 | 46 | /// Body 47 | final Stream>? body; 48 | 49 | /// Payload 50 | Map? _payload; 51 | 52 | Request( 53 | this.method, 54 | this.url, { 55 | this.headers = const {}, 56 | this.headersAll = const {}, 57 | this.encoding, 58 | this.contentType, 59 | this.body, 60 | }); 61 | 62 | /// Get parameter with matching key or return default value 63 | dynamic getParam(String key, {dynamic defaultValue}) async { 64 | switch (method) { 65 | case put: 66 | case post: 67 | case patch: 68 | case delete: 69 | return getPayload(key, defaultValue: defaultValue); 70 | case get: 71 | default: 72 | return getQuery(key, defaultValue: defaultValue); 73 | } 74 | } 75 | 76 | /// Get all the parameters 77 | Future> getParams() async { 78 | switch (method) { 79 | case put: 80 | case post: 81 | case patch: 82 | case delete: 83 | return _generateInput(); 84 | case get: 85 | default: 86 | return url.queryParameters; 87 | } 88 | } 89 | 90 | /// Get payload 91 | dynamic getPayload(String key, {dynamic defaultValue}) async { 92 | await _generateInput(); 93 | return _payload![key] ?? defaultValue; 94 | } 95 | 96 | Future> _generateInput() async { 97 | if (_payload != null) return _payload!; 98 | 99 | final ctype = (contentType ?? 'text/plain').split(';').first; 100 | switch (ctype) { 101 | case 'application/json': 102 | final bodyString = await (encoding ?? utf8).decodeStream(body!); 103 | _payload = jsonDecode(bodyString) as Map; 104 | break; 105 | case 'multipart/form-data': 106 | _payload = await _multipartFormData(); 107 | break; 108 | case 'application/x-www-form-urlencoded': 109 | default: 110 | final bodyString = await (encoding ?? utf8).decodeStream(body!); 111 | _payload = Uri(query: bodyString).queryParameters; 112 | } 113 | return _payload!; 114 | } 115 | 116 | /// Get query parameter 117 | dynamic getQuery(String key, {dynamic defaultValue}) { 118 | return url.queryParameters[key] ?? defaultValue; 119 | } 120 | 121 | /// Parse multipart forma data 122 | Future> _multipartFormData() async { 123 | final data = await _parts 124 | .map<_FormData?>((part) { 125 | final rawDisposition = part.headers['content-disposition']; 126 | if (rawDisposition == null) return null; 127 | 128 | final formDataParams = 129 | _parseFormDataContentDisposition(rawDisposition); 130 | if (formDataParams == null) return null; 131 | 132 | final name = formDataParams['name']; 133 | if (name == null) return null; 134 | 135 | final filename = formDataParams['filename']; 136 | dynamic value; 137 | if (filename != null) { 138 | value = { 139 | "file": part, 140 | "filename": filename, 141 | "mimeType": part.headers['Content-Type'], 142 | }; 143 | } else { 144 | value = (encoding ?? utf8).decodeStream(part); 145 | } 146 | return _FormData._(name, filename, value); 147 | }) 148 | .where((data) => data != null) 149 | .toList(); 150 | final Map out = {}; 151 | for (final item in data) { 152 | if (item!.filename != null) { 153 | out[item.name] = await item.value; 154 | } else { 155 | out[item.name] = await item.value; 156 | } 157 | } 158 | return out; 159 | } 160 | 161 | Stream get _parts { 162 | final boundary = _extractBoundary(); 163 | if (boundary == null) { 164 | throw Exception('Not a multipart request'); 165 | } 166 | return MimeMultipartTransformer(boundary).bind(body!); 167 | } 168 | 169 | String? _extractBoundary() { 170 | if (!headers.containsKey('Content-Type')) return null; 171 | final contentType = MediaType.parse(headers['Content-Type']!); 172 | if (contentType.type != 'multipart') return null; 173 | 174 | return contentType.parameters['boundary']; 175 | } 176 | } 177 | 178 | final _token = RegExp(r'[^()<>@,;:"\\/[\]?={} \t\x00-\x1F\x7F]+'); 179 | final _whitespace = RegExp(r'(?:(?:\r\n)?[ \t]+)*'); 180 | final _quotedString = RegExp(r'"(?:[^"\x00-\x1F\x7F]|\\.)*"'); 181 | final _quotedPair = RegExp(r'\\(.)'); 182 | 183 | /// Parses a `content-disposition: form-data; arg1="val1"; ...` header. 184 | Map? _parseFormDataContentDisposition(String header) { 185 | final scanner = StringScanner(header); 186 | 187 | scanner 188 | ..scan(_whitespace) 189 | ..expect(_token); 190 | if (scanner.lastMatch![0] != 'form-data') return null; 191 | 192 | final params = {}; 193 | 194 | while (scanner.scan(';')) { 195 | scanner 196 | ..scan(_whitespace) 197 | ..scan(_token); 198 | final key = scanner.lastMatch![0]!; 199 | scanner.expect('='); 200 | 201 | String value; 202 | if (scanner.scan(_token)) { 203 | value = scanner.lastMatch![0]!; 204 | } else { 205 | scanner.expect(_quotedString, name: 'quoted string'); 206 | final string = scanner.lastMatch![0]!; 207 | 208 | value = string 209 | .substring(1, string.length - 1) 210 | .replaceAllMapped(_quotedPair, (match) => match[1]!); 211 | } 212 | 213 | scanner.scan(_whitespace); 214 | params[key] = value; 215 | } 216 | 217 | scanner.expectDone(); 218 | return params; 219 | } 220 | 221 | class _FormData { 222 | final String name; 223 | final dynamic value; 224 | final String? filename; 225 | 226 | _FormData._(this.name, this.filename, this.value); 227 | } 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Utopia HTTP Server 2 | 3 | Light and Fast Dart HTTP library to build awesome Dart server side applications. Inspired from [Utopia PHP ecosystem](https://github.com/utopia-php). 4 | 5 | ## Getting Started 6 | 7 | First add the dependency in your pubspec.yaml 8 | 9 | ```yaml 10 | dependencies: 11 | utopia_http: ^0.1.0 12 | ``` 13 | 14 | Now, in main.dart, you can 15 | 16 | ```dart 17 | import 'dart:io'; 18 | import 'package:utopia_http/utopia_http.dart'; 19 | 20 | void main() async { 21 | final address = InternetAddress.anyIPv4; 22 | final port = Http.getEnv('PORT', 8000); 23 | final app = Http(ShelfServer(address, port), threads: 8); 24 | 25 | app.get('/').inject('request').inject('response').action( 26 | (Request request, Response response) { 27 | response.text('Hello world'); 28 | return response; 29 | }, 30 | ); 31 | 32 | await app.start(); 33 | } 34 | 35 | ``` 36 | 37 | ## Hot Reload Development Server 38 | 39 | Utopia HTTP provides true hot reload functionality for faster development. Use `HttpDev.start()` to automatically restart your server when code changes: 40 | 41 | ```dart 42 | import 'dart:io'; 43 | import 'package:utopia_http/utopia_http.dart'; 44 | 45 | void main() async { 46 | await HttpDev.start( 47 | script: () async { 48 | final address = InternetAddress.anyIPv4; 49 | final port = Http.getEnv('PORT', 8000); 50 | final app = Http(ShelfServer(address, port), threads: 8); 51 | 52 | app.get('/').inject('response').action((Response response) { 53 | response.text('Hello World with Hot Reload! 🔥'); 54 | return response; 55 | }); 56 | 57 | await app.start(); 58 | }, 59 | watchPaths: ['lib', 'example'], // Directories to watch 60 | watchExtensions: ['.dart'], // File extensions to monitor 61 | ); 62 | } 63 | ``` 64 | 65 | 66 | 67 | See [HOT_RELOAD.md](HOT_RELOAD.md) for detailed documentation. 68 | 69 | ## Features 70 | 71 | ### Parameters 72 | 73 | Parameters are used to receive input into endpoint action from the HTTP request. Parameters could be defined as URL parameters or in a body with a structure such as JSON. 74 | 75 | Every parameter must have a validator defined. Validators are simple classes that verify the input and ensure the security of inputs. You can define your own validators or use some of built-in validators. 76 | 77 | Define an endpoint with params: 78 | 79 | ```dart 80 | app 81 | .get('/hello-world') 82 | .param('name', 'World', Text(255), 'Name to greet. Optional', true) 83 | .inject('response').action((String name, Response response) { 84 | response.text('Hello $name'); 85 | return response; 86 | }); 87 | ``` 88 | 89 | ```bash 90 | curl http://localhost:8000/hello-world 91 | curl http://localhost:8000/hello-world?name=Utopia 92 | curl http://localhost:8000/hello-world?name=Appwrite 93 | ``` 94 | 95 | It's always recommended to use params instead of getting params or body directly from the request resource. If you do that intentionally, always make sure to run validation right after fetching such a raw input. 96 | 97 | ### Hooks 98 | 99 | There are three types of hooks: 100 | 101 | - Init hooks are executed before the route action is executed 102 | - Shutdown hooks are executed after route action is finished, but before application shuts down 103 | - Error hooks are executed whenever there's an error in the application lifecycle. 104 | 105 | You can provide multiple hooks for each stage. If you do not assign groups to the hook, by default, the hook will be executed for every route. If a group is defined on a hook, it will only run during the lifecycle of a request that belongs to the same group. 106 | 107 | 108 | ```dart 109 | app 110 | .init() 111 | .inject('request') 112 | .action((Request request) { 113 | print('Received: ${request.method} ${request.url}'); 114 | }); 115 | 116 | app 117 | .shutdown() 118 | .inject('response') 119 | .action((Response response) { 120 | print('Responding with status code: ${response.status}'); 121 | }); 122 | 123 | app 124 | .error() 125 | .inject('error') 126 | .inject('response') 127 | .action((Exception error, Response response) { 128 | response.text(error.toString(), status: HttpStatus.internalServerError); 129 | }); 130 | 131 | ``` 132 | 133 | Hooks are designed to be actions that run during the lifecycle of requests. Hooks should include functional logic. Hooks are not designed to prepare dependencies or context for the request. For such a use case, you should use resources. 134 | 135 | ### Groups 136 | 137 | Groups allow you to define common behavior for multiple endpoints. 138 | 139 | You can start by defining a group on an endpoint. Keep in mind you can also define multiple groups on a single endpoint. 140 | 141 | ```dart 142 | app 143 | .get('/login') 144 | .group(['api', 'public']) 145 | .inject('response') 146 | .action((Response response) { 147 | response.text('OK'); 148 | return response; 149 | }); 150 | ``` 151 | 152 | Now you can define hooks that would apply only to specific groups. Remember, hooks can also be assigned to multiple groups. 153 | 154 | ```dart 155 | app 156 | .init() 157 | .group(['api']) 158 | .inject('request') 159 | .action((Request request) { 160 | final apiKey = request.headers['x-api-key'] ?? ''; 161 | if (apiKey.isEmpty) { 162 | response.text('Api key missing.', status: HttpStatus.unauthorized); 163 | } 164 | }); 165 | ``` 166 | 167 | Groups are designed to be actions that run during the lifecycle of requests to endpoints that have some logic in common. Groups allow you to prevent code duplication and are designed to be defined anywhere in your source code to allow flexibility. 168 | 169 | ### Resources 170 | Resources allow you to prepare dependencies for requests such as database connection or the user who sent the request. A new instance of a resource is created for every request. 171 | 172 | Define a resource: 173 | 174 | ```dart 175 | app.resource('timestamp', () { 176 | return DateTime.now().millisecondsSinceEpoch; 177 | }); 178 | ``` 179 | 180 | Inject resource into endpoint action: 181 | 182 | ```dart 183 | app 184 | .get('/') 185 | .inject('timestamp') 186 | .inject('response') 187 | .action((int timestamp) { 188 | final diff = DateTime.now().millisecondsSinceEpoch - timestamp; 189 | print('Request took: $difference'); 190 | }); 191 | ``` 192 | 193 | Inject resource into a hook: 194 | 195 | ```dart 196 | app 197 | .init() 198 | .inject('timestamp') 199 | .action((int timestamp) { 200 | print('Request timestamp: ${timestamp.toString()}'); 201 | }); 202 | ``` 203 | 204 | In advanced scenarios, resources can also be injected into other resources or endpoint parameters. 205 | 206 | Resources are designed to prepare dependencies or context for the request. Resources are not meant to do functional logic or return callbacks. For such a use case, you should use hooks. 207 | 208 | ## Copyright and license 209 | 210 | The MIT License (MIT) [https://www.opensource.org/licenses/mit-license.php](https://www.opensource.org/licenses/mit-license.php) 211 | -------------------------------------------------------------------------------- /lib/src/http_dev.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | /// Development server with hot reload functionality 5 | /// 6 | /// Provides automatic process restart when source files change, 7 | /// ensuring all code changes are immediately reflected. 8 | class HttpDev { 9 | HttpDev._(); 10 | 11 | /// Start a development server with hot reload 12 | /// 13 | /// This will restart the entire Dart process when files change, 14 | /// ensuring all code changes (routes, handlers, imports) are picked up. 15 | /// 16 | /// Example: 17 | /// ```dart 18 | /// void main() async { 19 | /// await HttpDev.start( 20 | /// script: () async { 21 | /// final app = Http(ShelfServer(InternetAddress.anyIPv4, 8080)); 22 | /// app.get('/').inject('response').action((Response response) { 23 | /// response.text('Hello with hot reload!'); 24 | /// return response; 25 | /// }); 26 | /// await app.start(); 27 | /// }, 28 | /// watchPaths: ['lib', 'example'], 29 | /// watchExtensions: ['.dart'], 30 | /// ); 31 | /// } 32 | /// ``` 33 | static Future start({ 34 | required Future Function() script, 35 | List watchPaths = const ['lib', 'example'], 36 | List watchExtensions = const ['.dart'], 37 | List ignorePatterns = const [ 38 | '.git/', 39 | '.dart_tool/', 40 | 'build/', 41 | 'test/', 42 | ], 43 | }) async { 44 | // Check if we're already in a child process (to avoid infinite recursion) 45 | if (Platform.environment['UTOPIA_DEV_CHILD'] == 'true') { 46 | // We're in the child process, just run the script 47 | await script(); 48 | return; 49 | } 50 | 51 | // We're in the parent process, set up hot reload 52 | print('🚀 Starting Utopia HTTP Development Server with Hot Reload...'); 53 | print('📁 Watching paths: $watchPaths'); 54 | print('📄 Watching extensions: $watchExtensions'); 55 | print('🔄 Hot reload enabled - file changes will restart the process'); 56 | print(''); 57 | 58 | Process? currentProcess; 59 | bool isRestarting = false; 60 | 61 | // Handle Ctrl+C gracefully 62 | ProcessSignal.sigint.watch().listen((signal) async { 63 | print('\n🛑 Shutting down development server...'); 64 | if (currentProcess != null) { 65 | print(' Stopping child process...'); 66 | currentProcess!.kill(ProcessSignal.sigint); 67 | try { 68 | await currentProcess!.exitCode.timeout(Duration(seconds: 2)); 69 | } catch (e) { 70 | // Force kill if graceful shutdown fails 71 | currentProcess!.kill(ProcessSignal.sigkill); 72 | } 73 | } 74 | print('✅ Development server stopped'); 75 | exit(0); 76 | }); 77 | 78 | // Function to start the child process 79 | Future startChildProcess() async { 80 | if (isRestarting) return; 81 | 82 | try { 83 | print('🔄 Starting server process...'); 84 | 85 | // Get current script path and arguments 86 | final scriptPath = Platform.script.toFilePath(); 87 | final args = List.from(Platform.executableArguments); 88 | 89 | // Start child process with environment variable to indicate it's a child 90 | currentProcess = await Process.start( 91 | Platform.executable, 92 | [...args, scriptPath], 93 | environment: { 94 | ...Platform.environment, 95 | 'UTOPIA_DEV_CHILD': 'true', 96 | }, 97 | mode: ProcessStartMode.normal, 98 | ); 99 | 100 | // Forward stdout and stderr 101 | currentProcess!.stdout.listen((data) { 102 | stdout.add(data); 103 | }); 104 | 105 | currentProcess!.stderr.listen((data) { 106 | stderr.add(data); 107 | }); 108 | 109 | // Wait for process to exit 110 | final exitCode = await currentProcess!.exitCode; 111 | 112 | if (!isRestarting) { 113 | if (exitCode == 0) { 114 | print('✅ Process exited normally'); 115 | } else { 116 | print('❌ Process exited with code: $exitCode'); 117 | } 118 | } 119 | } catch (e) { 120 | print('❌ Error starting process: $e'); 121 | } 122 | } 123 | 124 | // Function to restart the child process 125 | Future restartChildProcess() async { 126 | if (isRestarting) return; 127 | 128 | isRestarting = true; 129 | print('🔄 Restarting due to file change...'); 130 | 131 | if (currentProcess != null) { 132 | currentProcess!.kill(ProcessSignal.sigterm); 133 | try { 134 | await currentProcess!.exitCode.timeout(Duration(seconds: 2)); 135 | } catch (e) { 136 | // Force kill if graceful shutdown fails 137 | currentProcess!.kill(ProcessSignal.sigkill); 138 | } 139 | } 140 | 141 | // Small delay to ensure clean shutdown 142 | await Future.delayed(Duration(milliseconds: 300)); 143 | 144 | isRestarting = false; 145 | await startChildProcess(); 146 | } 147 | 148 | // Set up file watchers 149 | final watchers = []; 150 | Timer? debounceTimer; 151 | 152 | for (final path in watchPaths) { 153 | final dir = Directory(path); 154 | if (await dir.exists()) { 155 | print('👁️ Watching directory: ${dir.absolute.path}'); 156 | 157 | final watcher = dir.watch(recursive: true).listen((event) { 158 | final filePath = event.path; 159 | 160 | // Check if file should be watched 161 | bool shouldWatch = false; 162 | for (final ext in watchExtensions) { 163 | if (filePath.endsWith(ext)) { 164 | shouldWatch = true; 165 | break; 166 | } 167 | } 168 | 169 | if (!shouldWatch) return; 170 | 171 | // Check ignore patterns 172 | for (final pattern in ignorePatterns) { 173 | if (filePath.contains(pattern)) { 174 | return; 175 | } 176 | } 177 | 178 | // Debounce file changes 179 | debounceTimer?.cancel(); 180 | debounceTimer = Timer(Duration(milliseconds: 500), () { 181 | final currentDir = Directory.current.path; 182 | String relativePath = filePath; 183 | if (filePath.startsWith(currentDir)) { 184 | relativePath = filePath.substring(currentDir.length); 185 | if (relativePath.startsWith(Platform.pathSeparator)) { 186 | relativePath = relativePath.substring(1); 187 | } 188 | } 189 | print('📝 File changed: $relativePath'); 190 | restartChildProcess(); 191 | }); 192 | }); 193 | 194 | watchers.add(watcher); 195 | } else { 196 | print('⚠️ Watch path does not exist: $path'); 197 | } 198 | } 199 | 200 | if (watchers.isEmpty) { 201 | print('❌ No valid watch paths found, running without hot reload'); 202 | await script(); 203 | return; 204 | } 205 | 206 | print(''); 207 | 208 | // Start the initial child process 209 | await startChildProcess(); 210 | 211 | // Keep the parent process alive 212 | while (true) { 213 | await Future.delayed(Duration(seconds: 1)); 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /lib/src/http.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer' as dev; 3 | import 'dart:io'; 4 | import 'dart:isolate'; 5 | 6 | import 'package:utopia_di/utopia_di.dart'; 7 | 8 | import 'app_mode.dart'; 9 | import 'isolate_entry_point.dart'; 10 | import 'isolate_message.dart'; 11 | import 'isolate_supervisor.dart'; 12 | import 'request.dart'; 13 | import 'response.dart'; 14 | import 'route.dart'; 15 | import 'router.dart'; 16 | import 'server.dart'; 17 | import 'validation_exception.dart'; 18 | 19 | final List _supervisors = []; 20 | 21 | /// Http class used to bootstrap your Http server 22 | /// You need to use one of the server adapters. Currently only 23 | /// Shelf adapter is available 24 | /// 25 | /// Example: 26 | /// ```dart 27 | /// void main() async { 28 | /// final address = InternetAddress.anyIPv4; 29 | /// final port = Http.getEnv('PORT', 8080); 30 | /// final app = Http(ShelfServer(address, port), threads: 8); 31 | /// // setup routes 32 | /// app.get('/').inject('request').inject('response').action( 33 | /// (Request request, Response response) { 34 | /// response.text('Hello world'); 35 | /// return response; 36 | /// }, 37 | /// ); 38 | /// // sart the server 39 | /// await app.start(); 40 | /// } 41 | /// ``` 42 | class Http { 43 | Http( 44 | this.server, { 45 | this.path, 46 | this.threads = 1, 47 | this.mode, 48 | }) { 49 | _di = DI(); 50 | _router = Router(); 51 | } 52 | 53 | List get supervisors => _supervisors; 54 | 55 | /// Server adapter, currently only shelf server is supported 56 | final Server server; 57 | 58 | /// Number of threads (isolates) to spawn 59 | final int threads; 60 | 61 | /// Path to server static files from 62 | final String? path; 63 | 64 | late DI _di; 65 | 66 | final Map> _routes = { 67 | Request.get: {}, 68 | Request.post: {}, 69 | Request.put: {}, 70 | Request.patch: {}, 71 | Request.delete: {}, 72 | Request.head: {}, 73 | }; 74 | 75 | /// Configured routes for different methods 76 | Map> get routes => _routes; 77 | 78 | final List _errors = []; 79 | final List _init = []; 80 | final List _shutdown = []; 81 | final List _options = []; 82 | 83 | late final Router _router; 84 | 85 | Route? _wildcardRoute; 86 | 87 | /// Application mode 88 | AppMode? mode; 89 | 90 | /// Is application running in production mode 91 | bool get isProduction => mode == AppMode.production; 92 | 93 | /// Is application running in development mode 94 | bool get isDevelopment => mode == AppMode.development; 95 | 96 | /// Is application running in staging mode 97 | bool get isStage => mode == AppMode.stage; 98 | 99 | /// Memory cached result for chosen route 100 | Route? route; 101 | 102 | /// Start the servers 103 | Future start() async { 104 | if (isDevelopment) { 105 | print('[UtopiaHttp] Starting HTTP server in DEVELOPMENT mode'); 106 | print('[UtopiaHttp] Address: \x1b[32m${server.address}\x1b[0m'); 107 | print('[UtopiaHttp] Port: \x1b[32m${server.port}\x1b[0m'); 108 | print('[UtopiaHttp] Threads: $threads'); 109 | print( 110 | '[UtopiaHttp] System: ${Platform.operatingSystem} ${Platform.operatingSystemVersion}', 111 | ); 112 | print('[UtopiaHttp] Dart Version: ${Platform.version}'); 113 | print('[UtopiaHttp] Process ID: $pid'); 114 | print('[UtopiaHttp] Working Directory: ${Directory.current.path}'); 115 | } 116 | _supervisors.clear(); 117 | for (int i = 0; i < threads; i++) { 118 | final supervisor = await _spawn( 119 | context: i.toString(), 120 | handler: run, 121 | path: path, 122 | ); 123 | _supervisors.add(supervisor); 124 | supervisor.resume(); 125 | if (isDevelopment) { 126 | print('[UtopiaHttp] Worker $i ready (development mode)'); 127 | } else { 128 | dev.log('Worker ${i.toString()} ready.', name: 'FINE'); 129 | } 130 | } 131 | if (isDevelopment) { 132 | print('[UtopiaHttp] All $threads worker(s) started successfully'); 133 | } 134 | } 135 | 136 | Future _spawn({ 137 | required String context, 138 | required Handler handler, 139 | SecurityContext? securityContext, 140 | String? path, 141 | }) async { 142 | final receivePort = ReceivePort(); 143 | final message = IsolateMessage( 144 | server: server, 145 | context: context, 146 | handler: run, 147 | securityContext: securityContext, 148 | path: path, 149 | sendPort: receivePort.sendPort, 150 | ); 151 | final isolate = await Isolate.spawn( 152 | entrypoint, 153 | message, 154 | paused: true, 155 | debugName: 'isolate_$context', 156 | ); 157 | return IsolateSupervisor( 158 | isolate: isolate, 159 | receivePort: receivePort, 160 | context: message.context, 161 | ); 162 | } 163 | 164 | /// Initialize a GET route 165 | Route get(String url) { 166 | return addRoute(Request.get, url); 167 | } 168 | 169 | /// Initialize a POST route 170 | Route post(String url) { 171 | return addRoute(Request.post, url); 172 | } 173 | 174 | /// Initialize a PATCH route 175 | Route patch(String url) { 176 | return addRoute(Request.patch, url); 177 | } 178 | 179 | /// Initialize a PUT route 180 | Route put(String url) { 181 | return addRoute(Request.put, url); 182 | } 183 | 184 | /// Initialize a DELETE route 185 | Route delete(String url) { 186 | return addRoute(Request.delete, url); 187 | } 188 | 189 | /// Initialize a wildcard route 190 | Route wildcard() { 191 | _wildcardRoute = Route('', ''); 192 | return _wildcardRoute!; 193 | } 194 | 195 | /// Initialize a init hook 196 | /// Init hooks are ran before executing each request 197 | Hook init() { 198 | final hook = Hook()..groups(['*']); 199 | _init.add(hook); 200 | return hook; 201 | } 202 | 203 | /// Initialize shutdown hook 204 | /// Shutdown hooks are ran after executing the request, before the response is sent 205 | Hook shutdown() { 206 | final hook = Hook()..groups(['*']); 207 | _shutdown.add(hook); 208 | return hook; 209 | } 210 | 211 | /// Initialize options hook 212 | /// Options hooks are ran for OPTIONS requests 213 | Hook options() { 214 | final hook = Hook()..groups(['*']); 215 | _options.add(hook); 216 | return hook; 217 | } 218 | 219 | /// Initialize error hooks 220 | /// Error hooks are ran for each errors 221 | Hook error() { 222 | final hook = Hook()..groups(['*']); 223 | _errors.add(hook); 224 | return hook; 225 | } 226 | 227 | /// Get environment variable 228 | static dynamic getEnv(String key, [dynamic def]) { 229 | return Platform.environment[key] ?? def; 230 | } 231 | 232 | /// Initialize route 233 | Route addRoute(String method, String path) { 234 | final route = Route(method, path); 235 | _router.addRoute(route); 236 | return route; 237 | } 238 | 239 | /// Set resource 240 | /// Once set, you can use `inject` to inject 241 | /// these resources to set other resources or in the hooks 242 | /// and routes 243 | void setResource( 244 | String name, 245 | Function callback, { 246 | String context = 'utopia', 247 | List injections = const [], 248 | }) => 249 | _di.set(name, callback, injections: injections, context: context); 250 | 251 | /// Get a resource 252 | T getResource( 253 | String name, { 254 | bool fresh = false, 255 | String context = 'utopia', 256 | }) => 257 | _di.get(name, fresh: fresh, context: context); 258 | 259 | /// Match route based on request 260 | Route? match(Request request) { 261 | var method = request.method; 262 | method = (method == Request.head) ? Request.get : method; 263 | route = _router.match(method, request.url.path); 264 | if (isDevelopment) { 265 | print( 266 | '[UtopiaHttp] Matching route: method=$method, path=${request.url.path}', 267 | ); 268 | if (route != null) { 269 | print('[UtopiaHttp] Matched route: ${route?.path}'); 270 | } else { 271 | print('[UtopiaHttp] No route matched for path: ${request.url.path}'); 272 | } 273 | } 274 | return route; 275 | } 276 | 277 | /// Get arguments for hooks 278 | Map _getArguments( 279 | Hook hook, { 280 | required String context, 281 | required Map requestParams, 282 | Map values = const {}, 283 | }) { 284 | final args = {}; 285 | hook.params.forEach((key, param) { 286 | final arg = requestParams[key] ?? param.defaultValue; 287 | var value = values[key] ?? arg; 288 | value = value == '' || value == null ? param.defaultValue : value; 289 | _validate(key, param, value); 290 | args[key] = value; 291 | }); 292 | 293 | for (var injection in hook.injections) { 294 | args[injection] = getResource(injection, context: context); 295 | } 296 | return args; 297 | } 298 | 299 | /// Execute list of given hooks 300 | Future _executeHooks( 301 | List hooks, 302 | List groups, 303 | Future> Function(Hook) argsCallback, { 304 | bool globalHook = false, 305 | bool globalHooksFirst = true, 306 | }) async { 307 | Future executeGlobalHook() async { 308 | for (final hook in hooks) { 309 | if (hook.getGroups().contains('*')) { 310 | final arguments = await argsCallback.call(hook); 311 | Function.apply( 312 | hook.getAction(), 313 | [...hook.argsOrder.map((key) => arguments[key])], 314 | ); 315 | } 316 | } 317 | } 318 | 319 | Future executeGroupHooks() async { 320 | for (final group in groups) { 321 | for (final hook in _init) { 322 | if (hook.getGroups().contains(group)) { 323 | final arguments = await argsCallback.call(hook); 324 | Function.apply( 325 | hook.getAction(), 326 | [...hook.argsOrder.map((key) => arguments[key])], 327 | ); 328 | } 329 | } 330 | } 331 | } 332 | 333 | if (globalHooksFirst && globalHook) { 334 | await executeGlobalHook(); 335 | } 336 | await executeGroupHooks(); 337 | if (!globalHooksFirst && globalHook) { 338 | await executeGlobalHook(); 339 | } 340 | } 341 | 342 | /// Execute request 343 | FutureOr execute( 344 | Route route, 345 | Request request, 346 | String context, 347 | ) async { 348 | final groups = route.getGroups(); 349 | final pathValues = route.getPathValues(request); 350 | 351 | try { 352 | await _executeHooks( 353 | _init, 354 | groups, 355 | (hook) async => _getArguments( 356 | hook, 357 | context: context, 358 | requestParams: await request.getParams(), 359 | values: pathValues, 360 | ), 361 | globalHook: route.hook, 362 | ); 363 | 364 | final args = _getArguments( 365 | route, 366 | context: context, 367 | requestParams: await request.getParams(), 368 | values: pathValues, 369 | ); 370 | final response = await Function.apply( 371 | route.getAction(), 372 | [...route.argsOrder.map((key) => args[key])], 373 | ); 374 | await _executeHooks( 375 | _shutdown, 376 | groups, 377 | (hook) async => _getArguments( 378 | hook, 379 | context: context, 380 | requestParams: await request.getParams(), 381 | values: pathValues, 382 | ), 383 | globalHook: route.hook, 384 | globalHooksFirst: false, 385 | ); 386 | 387 | return response ?? getResource('response', context: context); 388 | } on Exception catch (e) { 389 | _di.set('error', () => e); 390 | await _executeHooks( 391 | _errors, 392 | groups, 393 | (hook) async => _getArguments( 394 | hook, 395 | context: context, 396 | requestParams: await request.getParams(), 397 | values: pathValues, 398 | ), 399 | globalHook: route.hook, 400 | globalHooksFirst: false, 401 | ); 402 | 403 | if (e is ValidationException) { 404 | final response = getResource('response', context: context); 405 | response.status = 400; 406 | } 407 | } 408 | return getResource('response', context: context); 409 | } 410 | 411 | /// Run the execution for given request 412 | FutureOr run(Request request, String context) async { 413 | if (isDevelopment) { 414 | print('[UtopiaHttp] Handling request: ${request.method} ${request.url}'); 415 | } 416 | setResource('context', () => context, context: context); 417 | setResource('request', () => request, context: context); 418 | 419 | try { 420 | getResource('response', context: context); 421 | } catch (e) { 422 | setResource('response', () => Response(''), context: context); 423 | } 424 | 425 | var method = request.method.toUpperCase(); 426 | var route = match(request); 427 | final groups = (route is Route) ? route.getGroups() : []; 428 | 429 | if (method == Request.head) { 430 | method = Request.get; 431 | } 432 | 433 | if (route == null && _wildcardRoute != null) { 434 | route = _wildcardRoute; 435 | route!.path = request.url.path; 436 | if (isDevelopment) { 437 | print( 438 | '[UtopiaHttp] Using wildcard route for path: ${request.url.path}', 439 | ); 440 | } 441 | } 442 | 443 | if (route != null) { 444 | if (isDevelopment) { 445 | print('[UtopiaHttp] Executing route: ${route.path}'); 446 | } 447 | return execute(route, request, context); 448 | } else if (method == Request.options) { 449 | if (isDevelopment) { 450 | print( 451 | '[UtopiaHttp] Handling OPTIONS request for path: ${request.url.path}', 452 | ); 453 | } 454 | try { 455 | _executeHooks( 456 | _options, 457 | groups, 458 | (hook) async => _getArguments( 459 | hook, 460 | context: context, 461 | requestParams: await request.getParams(), 462 | ), 463 | globalHook: true, 464 | globalHooksFirst: false, 465 | ); 466 | return getResource('response', context: context); 467 | } on Exception catch (e) { 468 | for (final hook in _errors) { 469 | _di.set('error', () => e); 470 | if (hook.getGroups().contains('*')) { 471 | hook.getAction().call( 472 | _getArguments( 473 | hook, 474 | context: context, 475 | requestParams: await request.getParams(), 476 | ), 477 | ); 478 | } 479 | } 480 | return getResource('response', context: context); 481 | } 482 | } 483 | final response = getResource('response', context: context); 484 | response.text('Not Found'); 485 | response.status = 404; 486 | if (isDevelopment) { 487 | print( 488 | '[UtopiaHttp] Responding with 404 Not Found for path: ${request.url.path}', 489 | ); 490 | } 491 | 492 | // for each run, resources should be re-generated from callbacks 493 | resetResources(context); 494 | 495 | return response; 496 | } 497 | 498 | void _validate(String key, Param param, dynamic value) { 499 | if ('' != value && value != null) { 500 | final validator = param.validator; 501 | if (validator != null) { 502 | if (!validator.isValid(value)) { 503 | throw ValidationException( 504 | 'Invalid $key: ${validator.getDescription()}', 505 | ); 506 | } 507 | } 508 | } else if (!param.optional) { 509 | throw ValidationException('Param "$key" is not optional.'); 510 | } 511 | } 512 | 513 | /// Reset dependencies 514 | void resetResources([String? context]) { 515 | _di.resetResources(context); 516 | } 517 | 518 | /// Reset various resources 519 | void reset() { 520 | _router.reset(); 521 | _di.reset(); 522 | _errors.clear(); 523 | _init.clear(); 524 | _shutdown.clear(); 525 | _options.clear(); 526 | mode = null; 527 | } 528 | 529 | /// Stop servers 530 | Future stop() async { 531 | if (isDevelopment) { 532 | print('[UtopiaHttp] Stopping all server workers...'); 533 | } 534 | for (final sup in supervisors) { 535 | sup.stop(); 536 | } 537 | _supervisors.clear(); 538 | if (isDevelopment) { 539 | print('[UtopiaHttp] All server workers stopped.'); 540 | } 541 | } 542 | 543 | /// Dispose all resources 544 | void dispose() { 545 | if (isDevelopment) { 546 | print('[UtopiaHttp] Disposing server resources...'); 547 | } 548 | reset(); 549 | if (isDevelopment) { 550 | print('[UtopiaHttp] Server resources disposed.'); 551 | } 552 | } 553 | } 554 | --------------------------------------------------------------------------------