Merge pull request #115 from dukefirehawk/bug-fix/72

Bug fix/72
This commit is contained in:
Thomas 2023-11-13 09:45:55 +08:00 committed by GitHub
commit a1c65849b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 220 additions and 124 deletions

View file

@ -1,5 +1,10 @@
# Change Log
## 8.2.0
* Add `addResponseHeader` to `AngelHttp` to add headers to HTTP default response
* Add `removeResponseHeader` to `AngelHttp` to remove headers from HTTP default response
## 8.1.1
* Updated broken image on README.md

View file

@ -22,9 +22,6 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
HttpRequestContext, HttpResponseContext> {
@override
Uri get uri {
//if (server == null) {
// throw ArgumentError("[AngelHttp] Server instance not intialised");
//}
return Uri(
scheme: 'http', host: server?.address.address, port: server?.port);
}
@ -38,7 +35,7 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
/// An instance mounted on a server started by the [serverGenerator].
factory AngelHttp.custom(Angel app, ServerGeneratorType serverGenerator,
{bool useZone = true}) {
{bool useZone = true, Map<String, String> headers = const {}}) {
return AngelHttp._(app, serverGenerator, useZone);
}
@ -67,15 +64,6 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
return AngelHttp.fromSecurityContext(app, serverContext, useZone: useZone);
}
/// Use [server] instead.
//@deprecated
//HttpServer get httpServer {
//if (server == null) {
// throw ArgumentError("[AngelHttp] Server instance not initialised");
//}
// return server;
//}
Future handleRequest(HttpRequest request) =>
handleRawRequest(request, request.response);
@ -88,6 +76,20 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
return await super.close();
}
/// Remove headers from HTTP Response
void removeResponseHeader(Map<String, Object> headers) {
headers.forEach((key, value) {
server?.defaultResponseHeaders.remove(key, value);
});
}
/// Add headers to HTTP Response
void addResponseHeader(Map<String, Object> headers) {
headers.forEach((key, value) {
server?.defaultResponseHeaders.add(key, value);
});
}
@override
Future closeResponse(HttpResponse response) => response.close();

View file

@ -1,5 +1,5 @@
name: angel3_framework
version: 8.1.1
version: 8.2.0
description: A high-powered HTTP server extensible framework with dependency injection, routing and much more.
homepage: https://angel3-framework.web.app/
repository: https://github.com/dukefirehawk/angel/tree/master/packages/framework

View file

@ -27,6 +27,7 @@ import 'service_map_test.dart' as service_map;
import 'services_test.dart' as services;
import 'streaming_test.dart' as streaming;
import 'view_generator_test.dart' as view_generator;
//import 'response_header_test.dart' as response_header;
import 'package:test/test.dart';
/// For running with coverage
@ -36,6 +37,7 @@ void main() {
group('accepts', accepts.main);
group('anonymous service', anonymous_service.main);
group('body', body.main);
//group('response_header', response_header.main);
group('controller', controller.main);
group('detach', detach.main);
group('di', di.main);

View file

@ -0,0 +1,58 @@
import 'dart:io';
import 'package:angel3_container/mirrors.dart';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_framework/src/http/angel_http.dart';
import 'package:test/test.dart';
void main() {
late Angel app;
late AngelHttp http;
late HttpClient client;
setUp(() async {
app = Angel(reflector: MirrorsReflector());
http = AngelHttp(app);
await http.startServer();
var formData = {'id': 100, 'name': 'William'};
app.get('/api/v1/user/list', (RequestContext req, res) async {
//await req.parseBody();
//res.write('Hello, World!');
res.json(formData);
});
client = HttpClient();
});
tearDown(() async {
client.close();
await http.close();
});
test('Remove Response Header', () async {
http.removeResponseHeader({'x-frame-options': 'SAMEORIGIN'});
var request = await client.get('localhost', 3000, '/api/v1/user/list');
HttpClientResponse response = await request.close();
//print(response.headers);
expect(response.headers['x-frame-options'], isNull);
}, skip: true);
test('Add Response Header', () async {
http.addResponseHeader({
'X-XSRF_TOKEN':
'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e'
});
var request = await client.get('localhost', 3000, '/api/v1/user/list');
HttpClientResponse response = await request.close();
//print(response.headers);
expect(
response.headers['X-XSRF_TOKEN'],
equals([
'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e'
]));
}, skip: true);
}

View file

@ -1,5 +1,10 @@
# Change Log
## 8.1.0
* Updated `vm_service` to 13.0.0
* Added configurable HTTP response header
## 8.0.0
* Require Dart >= 3.0

View file

@ -9,7 +9,7 @@
Supports *hot reloading* of Angel3 servers on file changes. This is faster and more reliable than merely reactively restarting a `Process`. This package only works with the [Angel3 framework](https://pub.dev/packages/angel3_framework).
**Not recommended to use in production, unless you are specifically intending for a "hot code push" in production..**
**Not recommended to use in production, unless you are specifically intending for a "hot code push" in production.**
## Installation
@ -23,11 +23,7 @@ dependencies:
## Usage
This package is dependent on the Dart VM service, so you *must* run Dart with the `--observe` (or `--enable-vm-service`) argument!!!
Usage is fairly simple. Pass a function that creates an `Angel` server, along with a collection of paths to watch, to the `HotReloader` constructor. The rest is history!!!
The recommended pattern is to only use hot-reloading in your application entry point. Create your `Angel` instance within a separate function, conventionally named `createServer`.
This package is dependent on the Dart VM service, so you *must* run Dart with the `--observe` (or `--enable-vm-service`) argument. Usage is fairly simple. Pass a function that creates an `Angel` server, along with a collection of paths to watch, to the `HotReloader` constructor. The recommended pattern is to only use hot-reloading in your application entry point. Create your `Angel` instance within a separate function, conventionally named `createServer`.
You can watch:

View file

@ -12,6 +12,7 @@ import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
import 'package:belatuk_html_builder/elements.dart';
import 'package:io/ansi.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:vm_service/vm_service.dart' as vm;
import 'package:vm_service/vm_service_io.dart' as vm;
@ -19,8 +20,6 @@ import 'package:watcher/watcher.dart';
/// A utility class that watches the filesystem for changes, and starts new instances of an Angel server.
class HotReloader {
late vm.VmService _client;
vm.IsolateRef? _mainIsolate;
final StreamController<WatchEvent> _onChange =
StreamController<WatchEvent>.broadcast();
final List _paths = [];
@ -28,8 +27,10 @@ class HotReloader {
final Queue<HttpRequest> _requestQueue = Queue<HttpRequest>();
late HttpServer _io;
AngelHttp? _server;
Duration? _timeout;
late Duration _timeout;
late vm.VmService _client;
vm.VM? _vmachine;
vm.IsolateRef? _mainIsolate;
/// If `true` (default), then developers can `press 'r' to reload` the application on-the-fly.
///
@ -47,7 +48,7 @@ class HotReloader {
///
/// If the timeout expires, then the request will be immediately terminated with a `502 Bad Gateway` error.
/// Default: `5s`
Duration? get timeout => _timeout;
Duration get timeout => _timeout;
/// The Dart VM service host.
///
@ -116,20 +117,22 @@ class HotReloader {
Future handleRequest(HttpRequest request) async {
if (_server != null) {
return await _handle(request);
} else if (timeout == null) {
_requestQueue.add(request);
//} else if (timeout == null) {
// _requestQueue.add(request);
} else {
_requestQueue.add(request);
Timer(timeout!, () {
Timer(timeout, () {
if (_requestQueue.remove(request)) {
// Send 502 response
sendError(request, HttpStatus.badGateway, '502 Bad Gateway',
'Request timed out after ${timeout!.inMilliseconds}ms.');
'Request timed out after ${timeout.inMilliseconds}ms.');
}
});
}
}
Logger? get _appLogger => _server?.app.logger;
Future<AngelHttp> _generateServer() async {
var s = await generator();
await Future.forEach(s.startupHooks, s.configure);
@ -138,23 +141,24 @@ class HotReloader {
}
void _logWarning(String msg) {
if (_server?.app.logger != null) {
_server?.app.logger.warning(msg);
if (_appLogger != null) {
_appLogger?.warning(msg);
} else {
print(yellow.wrap('WARNING: $msg'));
}
}
void _logInfo(String msg) {
if (_server?.app.logger != null) {
_server?.app.logger.info(msg);
if (_appLogger != null) {
_appLogger?.info(msg);
} else {
print(lightGray.wrap(msg));
}
}
/// Starts listening to requests and filesystem events.
Future<HttpServer> startServer([address, int? port]) async {
Future<HttpServer> startServer(
[String address = '127.0.0.1', int port = 3000]) async {
var isHot = true;
_server = await _generateServer();
@ -185,9 +189,10 @@ class HotReloader {
_mainIsolate ??= _vmachine?.isolates?.first;
if (_vmachine != null) {
for (var isolate in _vmachine!.isolates ?? <vm.IsolateRef>[]) {
if (isolate.id != null) {
await _client.setIsolatePauseMode(isolate.id!,
for (var isolate in _vmachine?.isolates ?? <vm.IsolateRef>[]) {
var isolateId = isolate.id;
if (isolateId != null) {
await _client.setIsolatePauseMode(isolateId,
exceptionPauseMode: 'None');
}
}
@ -203,7 +208,8 @@ class HotReloader {
while (_requestQueue.isNotEmpty) {
await _handle(_requestQueue.removeFirst());
}
var server = _io = await HttpServer.bind(address ?? '127.0.0.1', port ?? 0);
var server = _io = await HttpServer.bind(address, port);
//server.defaultResponseHeaders();
server.listen(handleRequest);
// Print a Flutter-like prompt...
@ -213,7 +219,7 @@ class HotReloader {
Uri? observatoryUri;
if (isHot) {
observatoryUri = await dev.Service.getInfo().then((i) => i.serverUri!);
observatoryUri = await dev.Service.getInfo().then((i) => i.serverUri);
}
print(styleBold.wrap(
@ -260,7 +266,7 @@ class HotReloader {
if (ch == $R) {
_logInfo('Manually restarting server...\n');
await _killServer();
await _server!.close();
await _server?.close();
var addr = _io.address.address;
var port = _io.port;
await _io.close(force: true);
@ -359,21 +365,23 @@ class HotReloader {
scheduleMicrotask(() async {
// Disconnect active WebSockets
try {
var ws = _server!.app.container.make<AngelWebSocket>();
var ws = _server?.app.container.make<AngelWebSocket>();
for (var client in ws.clients) {
try {
await client.close();
} catch (e) {
_logWarning(
'Couldn\'t close WebSocket from session #${client.request.session!.id}: $e');
if (ws != null) {
for (var client in ws.clients) {
try {
await client.close();
} catch (e) {
_logWarning(
'Couldn\'t close WebSocket from session #${client.request.session?.id}: $e');
}
}
}
// await Future.forEach(
// _server.app.shutdownHooks, _server.app.configure);
await _server!.app.close();
_server!.app.logger.clearListeners();
await _server?.app.close();
_server?.app.logger.clearListeners();
} catch (_) {
// Fail silently...
}
@ -387,11 +395,16 @@ class HotReloader {
_server = null;
if (hot) {
var report = await _client.reloadSources(_mainIsolate!.id!);
var mainIsolateId = _mainIsolate?.id;
if (mainIsolateId != null) {
var report = await _client.reloadSources(mainIsolateId);
if (report.success != null) {
_logWarning(
'Hot reload failed - perhaps some sources have not been generated yet.');
if (report.success != null) {
_logWarning(
'Hot reload failed - perhaps some sources have not been generated yet.');
}
} else {
_logWarning('Hot reload failed - isolate id does not exist.');
}
}

View file

@ -1,5 +1,5 @@
name: angel3_hot
version: 8.0.0
version: 8.1.0
description: Supports hot reloading/hot code push of Angel3 servers on file changes.
homepage: https://angel3-framework.web.app/
repository: https://github.com/dukefirehawk/angel/tree/master/packages/hot
@ -13,17 +13,17 @@ dependencies:
glob: ^2.1.0
io: ^1.0.0
path: ^1.8.0
vm_service: ^11.6.0
vm_service: ^13.0.0
watcher: ^1.0.0
logging: ^1.2.0
dev_dependencies:
http: ^1.0.0
logging: ^1.2.0
lints: ^2.1.0
# dependency_overrides:
dependency_overrides:
# angel3_container:
# path: ../container/angel_container
# angel3_framework:
# path: ../framework
angel3_framework:
path: ../framework
# angel3_http_exception:
# path: ../http_exception
# angel3_model:

View file

@ -1,5 +1,9 @@
# Change Log
## 8.1.0
* Added optional `responseHeaders` and `removeResponseHeaders` to `Runner`
## 8.0.0
* Require Dart >= 3.0

View file

@ -9,9 +9,7 @@ Helpers for concurrency, message-passing, rotating loggers, and other production
![Screenshot](angel3-screenshot.png)
This will become the de-facto way to run Angel3 applications in deployed environments, as it takes care of inter-isolate communication, respawning dead processes, and other housekeeping for you automatically.
Most users will want to use the `Runner` class.
This will become the de-facto way to run Angel3 applications in deployed environments, as it takes care of inter-isolate communication, respawning dead processes, and other housekeeping for you automatically. Most users will want to use the `Runner` class.
## `Runner`
@ -45,15 +43,11 @@ When combined with `systemd`, deploying Angel3 applications on Linux can be very
## Message Passing
The `Runner` class uses [`belatuk_pub_sub`](<https://pub.dev/packages/belatuk_pub_sub>) to coordinate
message passing between isolates.
When one isolate sends a message, all other isolates will receive the same message, except for the isolate that sent it.
It is injected into your application's `Container` as `pub_sub.Client`, so you can use it as follows:
message passing between isolates. When one isolate sends a message, all other isolates will receive the same message, except for the isolate that sent it. It is injected into your application's `Container` as `pub_sub.Client`, so you can use it as follows:
```dart
// Use the injected `pub_sub.Client` to send messages.
var client = app.container.make<pub_sub.Client>();
var client = app.container.make<Client>();
// We can listen for an event to perform some behavior.
//
@ -66,6 +60,34 @@ onGreetingChanged
});
```
## Customising Response Header
Additional parameters can be passed to the `Runner` class to:
1. Remove headers from HTTP response.
2. Add headers to HTTP response.
For example, the following code snippet removes `X-FRAME-OPTIONS` and adds `X-XSRF_TOKEN` to the response header.
```dart
void main(List<String> args) {
// Remove 'X-FRAME-OPTIONS'
var removeHeader = {'X-FRAME-OPTIONS': 'SAMEORIGIN'};
// Add 'X-XSRF_TOKEN'
var customHeader = {
'X-XSRF-TOKEN':
'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e'
};
Runner('example', configureServer,
removeResponseHeaders: removeHeader, responseHeaders: customHeader)
.run(args);
}
```
## Run-time Metadata
At run-time, you may want to know information about the currently-running instance, for example, which number instance. For this, the `InstanceInfo` class is injected into each instance:

View file

@ -2,13 +2,16 @@ import 'dart:async';
import 'dart:isolate';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_production/angel3_production.dart';
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart' as pub_sub;
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart';
void main(List<String> args) => Runner('example', configureServer).run(args);
void main(List<String> args) {
Runner('example', configureServer).run(args);
}
Future configureServer(Angel app) async {
// Use the injected `pub_sub.Client` to send messages.
var client = app.container.make<pub_sub.Client>();
var client = app.container.make<Client>();
var greeting = 'Hello! This is the default greeting.';
// We can listen for an event to perform some behavior.

View file

@ -32,13 +32,12 @@ class RunnerOptions {
..addOption('key-file', help: 'The PEM key file to read.')
..addOption('key-password', help: 'The PEM key file password.');
final String? hostname,
certificateFile,
keyFile,
certificatePassword,
keyPassword;
final String hostname;
final String? certificateFile, keyFile, certificatePassword, keyPassword;
final int concurrency, port;
final bool useZone, respawn, quiet, ssl, http2;
final Map<String, Object> removeResponseHeaders = {};
final Map<String, Object> responseHeaders = {};
RunnerOptions(
{this.hostname = '127.0.0.1',
@ -56,26 +55,18 @@ class RunnerOptions {
factory RunnerOptions.fromArgResults(ArgResults argResults) {
return RunnerOptions(
hostname: argResults['address'] as String?,
hostname: argResults['address'] as String,
port: int.parse(argResults['port'] as String),
concurrency: int.parse(argResults['concurrency'] as String),
useZone: argResults['use-zone'] as bool? ?? false,
respawn: argResults['respawn'] as bool? ?? true,
quiet: argResults['quiet'] as bool? ?? false,
certificateFile: argResults.wasParsed('certificate-file')
? argResults['certificate-file'] as String?
: null,
keyFile: argResults.wasParsed('key-file')
? argResults['key-file'] as String?
: null,
certificateFile: argResults['certificate-file'] as String?,
keyFile: argResults['key-file'] as String?,
ssl: argResults['ssl'] as bool? ?? false,
http2: argResults['http2'] as bool? ?? false,
certificatePassword: argResults.wasParsed('certificate-password')
? argResults['certificate-password'] as String?
: null,
keyPassword: argResults.wasParsed('key-password')
? argResults['key-password'] as String?
: null,
certificatePassword: argResults['certificate-password'] as String?,
keyPassword: argResults['key-password'] as String?,
);
}
}

View file

@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart';
import 'package:belatuk_pub_sub/isolate.dart';
import 'package:intl/intl.dart';
import 'package:angel3_container/angel3_container.dart';
import 'package:angel3_framework/angel3_framework.dart';
@ -10,8 +12,6 @@ import 'package:args/args.dart';
import 'package:io/ansi.dart';
import 'package:io/io.dart';
import 'package:logging/logging.dart';
import 'package:belatuk_pub_sub/isolate.dart' as pub_sub;
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart' as pub_sub;
import 'instance_info.dart';
import 'options.dart';
@ -24,35 +24,21 @@ class Runner {
final AngelConfigurer configureServer;
final Reflector reflector;
final Map<String, Object> removeResponseHeaders;
final Map<String, Object> responseHeaders;
Runner(this.name, this.configureServer,
{this.reflector = const EmptyReflector()});
{this.reflector = const EmptyReflector(),
this.removeResponseHeaders = const {},
this.responseHeaders = const {}});
static const String asciiArt2 = '''
___ _ ________________ _____
/ | / | / / ____/ ____/ / |__ /
/ /| | / |/ / / __/ __/ / / /_ <
/ ___ |/ /| / /_/ / /___/ /______/ /
/_/ |_/_/ |_/\\____/_____/_____/____/
''';
static const String asciiArt = '''
static const String _asciiArt = '''
_ _ _ ____ _____ _ _____
/ \\ | \\ | |/ ___| ____| | |___ /
/ _ \\ | \\| | | _| _| | | |_ \\
/ ___ \\| |\\ | |_| | |___| |___ ___) |
/_/ \\_\\_| \\_|\\____|_____|_____|____/
''';
static const String asciiArtOld = '''
____________ ________________________
___ |__ | / /_ ____/__ ____/__ /
__ /| |_ |/ /_ / __ __ __/ __ /
_ ___ | /| / / /_/ / _ /___ _ /___
/_/ |_/_/ |_/ ____/ /_____/ /_____/
''';
static final DateFormat _defaultDateFormat =
@ -173,11 +159,14 @@ _ ___ | /| / / /_/ / _ /___ _ /___
/// Starts a number of isolates, running identical instances of an Angel application.
Future run(List<String> args) async {
pub_sub.Server? server;
Server? server;
try {
var argResults = RunnerOptions.argParser.parse(args);
var options = RunnerOptions.fromArgResults(argResults);
options.responseHeaders.addAll(responseHeaders);
options.removeResponseHeaders.addAll(removeResponseHeaders);
if (options.ssl || options.http2) {
if (options.certificateFile == null) {
@ -188,7 +177,7 @@ _ ___ | /| / / /_/ / _ /___ _ /___
}
print(darkGray.wrap(
'$asciiArt\n\nA batteries-included, full-featured, full-stack framework in Dart.\n\nhttps://angel3-framework.web.app\n'));
'$_asciiArt\n\nA batteries-included, full-featured, full-stack framework in Dart.\n\nhttps://angel3-framework.web.app\n'));
if (argResults['help'] == true) {
stdout
@ -199,12 +188,12 @@ _ ___ | /| / / /_/ / _ /___ _ /___
print('Starting `$name` application...');
var adapter = pub_sub.IsolateAdapter();
server = pub_sub.Server([adapter]);
var adapter = IsolateAdapter();
server = Server([adapter]);
// Register clients
for (var i = 0; i < Platform.numberOfProcessors; i++) {
server.registerClient(pub_sub.ClientInfo('client$i'));
server.registerClient(ClientInfo('client$i'));
}
server.start();
@ -240,11 +229,10 @@ _ ___ | /| / / /_/ / _ /___ _ /___
));
zone.run(() async {
var client =
pub_sub.IsolateClient('client${argsWithId.id}', args.pubSubSendPort);
var client = IsolateClient('client${argsWithId.id}', args.pubSubSendPort);
var app = Angel(reflector: args.reflector)
..container.registerSingleton<pub_sub.Client>(client)
..container.registerSingleton<Client>(client)
..container.registerSingleton(InstanceInfo(id: argsWithId.id));
app.shutdownHooks.add((_) => client.close());
@ -292,6 +280,13 @@ _ ___ | /| / / /_/ / _ /___ _ /___
}
await driver.startServer(args.options.hostname, args.options.port);
// Only apply the headers to AngelHttp instance
if (driver is AngelHttp) {
driver.addResponseHeader(args.options.responseHeaders);
driver.removeResponseHeader(args.options.removeResponseHeaders);
}
serverUrl = driver.uri;
if (args.options.ssl || args.options.http2) {
serverUrl = serverUrl.replace(scheme: 'https');

View file

@ -1,5 +1,5 @@
name: angel3_production
version: 8.0.0
version: 8.1.0
description: Helpers for concurrency, message-passing, rotating loggers, and other production functionality in Angel3.
homepage: https://angel3-framework.web.app
repository: https://github.com/dukefirehawk/angel/tree/master/packages/production
@ -7,7 +7,7 @@ environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
angel3_container: ^8.0.0
angel3_framework: ^8.0.0
angel3_framework: ^8.2.0
belatuk_pub_sub: ^6.0.0
args: ^2.4.0
io: ^1.0.0
@ -15,11 +15,11 @@ dependencies:
intl: ^0.18.0
dev_dependencies:
lints: ^2.1.0
# dependency_overrides:
dependency_overrides:
# angel3_container:
# path: ../container/angel_container
# angel3_framework:
# path: ../framework
angel3_framework:
path: ../framework
# angel3_http_exception:
# path: ../http_exception
# angel3_model: