Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore: Pass stack-trace along with Exception #108

Merged
merged 7 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ scripts:
melos exec -- "dart pub global run coverage:format_coverage --check-ignore --report-on=lib --lcov -o "$MELOS_ROOT_PATH/coverage/$(echo "\$MELOS_PACKAGE_NAME")_lcov.info" -i ./coverage"
find $MELOS_ROOT_PATH/coverage -type f -empty -print -delete

format: melos exec -- "dart format . --line-length=120"
format: melos exec -- "dart format ."

analyze: melos exec -- "dart analyze . --fatal-infos"
3 changes: 2 additions & 1 deletion packages/pharaoh/lib/src/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import 'shelf_interop/shelf.dart' as shelf;

part 'core_impl.dart';

typedef OnErrorCallback = FutureOr<Response> Function(Object error, Request req, Response res);
typedef OnErrorCallback = FutureOr<Response> Function(
Object error, Request req, Response res);

abstract class Pharaoh implements RouterContract {
factory Pharaoh() => $PharaohImpl();
Expand Down
44 changes: 29 additions & 15 deletions packages/pharaoh/lib/src/core_impl.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
part of 'core.dart';

class $PharaohImpl extends RouterContract with RouteDefinitionMixin implements Pharaoh {
class $PharaohImpl extends RouterContract
with RouteDefinitionMixin
implements Pharaoh {
late final HttpServer _server;

OnErrorCallback? _onErrorCb;
Expand Down Expand Up @@ -60,11 +62,12 @@ class $PharaohImpl extends RouterContract with RouteDefinitionMixin implements P

@override
Future<Pharaoh> listen({int port = 3000}) async {
_server = await HttpServer.bind('0.0.0.0', port, shared: true)
_server = await HttpServer.bind(InternetAddress.anyIPv4, port, shared: true)
..autoCompress = true;
_server.listen(handleRequest);

print('Server start on PORT: ${_server.port} -> ${uri.scheme}://localhost:${_server.port}');
print(
'Server start on PORT: ${_server.port} -> ${uri.scheme}://localhost:${_server.port}');
return this;
}

Expand All @@ -79,20 +82,26 @@ class $PharaohImpl extends RouterContract with RouteDefinitionMixin implements P
final request = Request.from(httpReq);
final response = Response.create();

late Object requestError;
late ({Object error, StackTrace trace}) requestError;

try {
final result = await resolveAndExecuteHandlers(request, response);
return forward(httpReq, result.res);
} catch (error) {
requestError = error;
} catch (error, trace) {
requestError = (error: error, trace: trace);
}

if (_onErrorCb == null) {
var errorResponse = response.internalServerError(requestError.toString());
if (requestError is SpannerRouteValidatorError) {
errorResponse = response.status(HttpStatus.unprocessableEntity);
}
return forward(httpReq, errorResponse);
final status = requestError.error is SpannerRouteValidatorError
? HttpStatus.unprocessableEntity
: HttpStatus.internalServerError;
return forward(
httpReq,
response.json({
'error': requestError.error.toString(),
'trace': requestError.trace.toString()
}, statusCode: status),
);
}

final result = await _onErrorCb!.call(requestError, request, response);
Expand All @@ -106,7 +115,9 @@ class $PharaohImpl extends RouterContract with RouteDefinitionMixin implements P

final routeResult = spanner.lookup(req.method, req.path);
final resolvedHandlers = routeResult?.values.cast<Middleware>() ?? [];
if (routeResult == null || resolvedHandlers.isEmpty) return reqRes.merge(routeNotFound());
if (routeResult == null || resolvedHandlers.isEmpty) {
return reqRes.merge(routeNotFound());
}

/// update request params with params resolved from spanner
for (final param in routeResult.params.entries) {
Expand Down Expand Up @@ -147,7 +158,8 @@ class $PharaohImpl extends RouterContract with RouteDefinitionMixin implements P
res_.mimeType != 'multipart/byteranges') {
// If the response isn't chunked yet and there's no other way to tell its
// length, enable `dart:io`'s chunked encoding.
request.response.headers.set(HttpHeaders.transferEncodingHeader, 'chunked');
request.response.headers
.set(HttpHeaders.transferEncodingHeader, 'chunked');
}

// headers to write to the response
Expand All @@ -159,12 +171,14 @@ class $PharaohImpl extends RouterContract with RouteDefinitionMixin implements P
request.response.headers.add(_XPoweredByHeader, 'Pharaoh');
}
if (!hders.containsKey(HttpHeaders.dateHeader)) {
request.response.headers.add(HttpHeaders.dateHeader, DateTime.now().toUtc());
request.response.headers
.add(HttpHeaders.dateHeader, DateTime.now().toUtc());
}
if (!hders.containsKey(HttpHeaders.contentLengthHeader)) {
final contentLength = res_.contentLength;
if (contentLength != null) {
request.response.headers.add(HttpHeaders.contentLengthHeader, contentLength);
request.response.headers
.add(HttpHeaders.contentLengthHeader, contentLength);
}
}

Expand Down
7 changes: 5 additions & 2 deletions packages/pharaoh/lib/src/http/cookie.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class CookieOpts {

void validate() {
if (signed && secret == null) {
throw PharaohException.value('CookieOpts("secret") required for signed cookies');
throw PharaohException.value(
'CookieOpts("secret") required for signed cookies');
}
}
}
Expand All @@ -77,7 +78,9 @@ extension CookieExtension on Cookie {

bool get signed => decodedValue.startsWith('s:');

String get actualStr => signed ? decodedValue.substring(2) : decodedValue; // s:foo-bar-baz --> foo-bar-bar
String get actualStr => signed
? decodedValue.substring(2)
: decodedValue; // s:foo-bar-baz --> foo-bar-bar

bool get jsonEncoded => actualStr.startsWith('j:');

Expand Down
6 changes: 4 additions & 2 deletions packages/pharaoh/lib/src/http/request_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class RequestImpl extends Request<dynamic> {
String get path => actual.uri.path;

@override
String get ipAddr => actual.connectionInfo?.remoteAddress.address ?? 'Unknown';
String get ipAddr =>
actual.connectionInfo?.remoteAddress.address ?? 'Unknown';

@override
HTTPMethod get method => getHttpMethod(actual);
Expand All @@ -63,7 +64,8 @@ class RequestImpl extends Request<dynamic> {
List<Cookie> get cookies => _context[RequestContext.cookies] ?? [];

@override
List<Cookie> get signedCookies => _context[RequestContext.signedCookies] ?? [];
List<Cookie> get signedCookies =>
_context[RequestContext.signedCookies] ?? [];

@override
Session? get session => _context[RequestContext.session];
Expand Down
25 changes: 15 additions & 10 deletions packages/pharaoh/lib/src/http/response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,32 @@ import 'message.dart';

part 'response_impl.dart';

final applicationOctetStreamType = ContentType('application', 'octet-stream');

abstract class Response extends Message<shelf.Body?> {
Response(super.body, {super.headers = const {}});

/// Constructs an HTTP Response
static Response create({int? statusCode, Object? body, Encoding? encoding, Map<String, dynamic>? headers}) =>
ResponseImpl._(
body: body == null ? null : Body(body),
ended: false,
statusCode: statusCode,
headers: headers ?? {},
);
static Response create({
int? statusCode,
Object? body,
Encoding? encoding,
Map<String, dynamic>? headers,
}) {
return ResponseImpl._(
body: body == null ? null : Body(body),
ended: false,
statusCode: statusCode,
headers: headers ?? {},
);
}

Response header(String headerKey, String headerValue);

/// Creates a new cookie setting the name and value.
///
/// [name] and [value] must be composed of valid characters according to RFC
/// 6265.
Response cookie(String name, Object? value, [CookieOpts opts = const CookieOpts()]);
Response cookie(String name, Object? value,
[CookieOpts opts = const CookieOpts()]);

Response withCookie(Cookie cookie);

Expand Down
61 changes: 31 additions & 30 deletions packages/pharaoh/lib/src/http/response_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,22 @@ class ResponseImpl extends Response {
}

@override
ResponseImpl movedPermanently(String url) => redirect(url, HttpStatus.movedPermanently);
ResponseImpl movedPermanently(String url) =>
redirect(url, HttpStatus.movedPermanently);

@override
ResponseImpl notModified({Map<String, dynamic>? headers}) {
final existingHeaders = this.headers;
if (headers != null) headers.forEach((key, val) => existingHeaders[key] = val);
if (headers != null) {
headers.forEach((key, val) => existingHeaders[key] = val);
}

return ResponseImpl._(
ended: true,
statusCode: HttpStatus.notModified,
headers: existingHeaders
..removeWhere((name, _) => name.toLowerCase() == HttpHeaders.contentLengthHeader)
..removeWhere(
(name, _) => name.toLowerCase() == HttpHeaders.contentLengthHeader)
..[HttpHeaders.dateHeader] = formatHttpDate(DateTime.now()),
);
}
Expand Down Expand Up @@ -124,24 +128,29 @@ class ResponseImpl extends Response {
}

@override
ResponseImpl notFound([String? message]) => json(error(message ?? 'Not found'), statusCode: HttpStatus.notFound);
ResponseImpl notFound([String? message]) =>
json(error(message ?? 'Not found'), statusCode: HttpStatus.notFound);

@override
ResponseImpl unauthorized({Object? data}) => json(data ?? error('Unauthorized'), statusCode: HttpStatus.unauthorized);
ResponseImpl unauthorized({Object? data}) =>
json(data ?? error('Unauthorized'), statusCode: HttpStatus.unauthorized);

@override
ResponseImpl internalServerError([String? message]) =>
json(error(message ?? 'Internal Server Error'), statusCode: HttpStatus.internalServerError);
json(error(message ?? 'Internal Server Error'),
statusCode: HttpStatus.internalServerError);

@override
ResponseImpl ok([String? data]) => this.end()
..headers[HttpHeaders.contentTypeHeader] = ContentType.text.toString()
..body = shelf.Body(data, encoding);

@override
ResponseImpl send(Object data) => this.end()
..headers[HttpHeaders.contentTypeHeader] = _getContentType(data, valueWhenNull: ContentType.html).toString()
..body = shelf.Body(data);
ResponseImpl send(Object data) {
return this.end()
..headers[HttpHeaders.contentTypeHeader] ??= ContentType.binary.toString()
..body = shelf.Body(data);
}

@override
ResponseImpl end() => ResponseImpl._(
Expand All @@ -151,30 +160,21 @@ class ResponseImpl extends Response {
statusCode: statusCode,
);

ContentType _getContentType(Object data, {required ContentType valueWhenNull}) {
final isBuffer = _isBuffer(data);
final mType = mediaType;
if (mType == null) {
return isBuffer ? applicationOctetStreamType : valueWhenNull;
}

/// Always use charset :utf-8 unless
/// we have to deal with buffers.
final charset = isBuffer ? mType.parameters['charset'] : 'utf-8';
return ContentType.parse('${mType.mimeType}; charset=$charset');
}

/// TODO research on how to tell if an object is a buffer
bool _isBuffer(Object object) => object is! String;

@override
ResponseImpl format(Request request, Map<String, Function(ResponseImpl res)> options) {
ResponseImpl format(
Request request,
Map<String, Function(ResponseImpl res)> options,
) {
var reqAcceptType = request.headers[HttpHeaders.acceptHeader];
if (reqAcceptType is Iterable) reqAcceptType = reqAcceptType.join();

final handler = options[reqAcceptType] ?? options['_'];

if (handler == null) {
return json(error('Not Acceptable'), statusCode: HttpStatus.notAcceptable);
return json(
error('Not Acceptable'),
statusCode: HttpStatus.notAcceptable,
);
}

return handler.call(this);
Expand All @@ -198,7 +198,8 @@ class ResponseImpl extends Response {
..headers[HttpHeaders.setCookieHeader] = _cookies;

@override
Response render(String name, [Map<String, dynamic> data = const {}]) => this.end()
..viewToRender = ViewRenderData(name, data)
..headers[HttpHeaders.contentTypeHeader] = ContentType.html.toString();
Response render(String name, [Map<String, dynamic> data = const {}]) =>
this.end()
..viewToRender = ViewRenderData(name, data)
..headers[HttpHeaders.contentTypeHeader] = ContentType.html.toString();
}
3 changes: 2 additions & 1 deletion packages/pharaoh/lib/src/middleware/body_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import '../router/router_handler.dart';

class MimeType {
static const String multiPartForm = 'multipart/form-data';
static const String applicationFormUrlEncoded = 'application/x-www-form-urlencoded';
static const String applicationFormUrlEncoded =
'application/x-www-form-urlencoded';
static const String applicationJson = 'application/json';
static const String textPlain = 'text/plain';
}
Expand Down
4 changes: 3 additions & 1 deletion packages/pharaoh/lib/src/middleware/cookie_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ Middleware cookieParser({CookieOpts opts = const CookieOpts()}) {

for (final cookie in signedCookies) {
var realValue = unsignValue(cookie.actualStr, secret);
if (realValue != null) verifiedCookies.add(cookie..value = Uri.encodeComponent(realValue));
if (realValue != null) {
verifiedCookies.add(cookie..value = Uri.encodeComponent(realValue));
}
}
signedCookies = verifiedCookies;
}
Expand Down
6 changes: 4 additions & 2 deletions packages/pharaoh/lib/src/middleware/session_mw.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ Middleware sessionMdw({
if (!req.path.startsWith(opts.path)) return next();
if (req.session?.valid ?? false) return next();

final reqSid = req.signedCookies.firstWhereOrNull((e) => e.name == name)?.value;
final reqSid =
req.signedCookies.firstWhereOrNull((e) => e.name == name)?.value;
if (reqSid != null) {
var result = await sessionStore.get(reqSid);
if (result != null && result.valid) {
Expand All @@ -94,7 +95,8 @@ Middleware sessionMdw({
final ReqResHook sessionPreResponseHook = (ReqRes reqRes) async {
var req = reqRes.req, res = reqRes.res;
final session = req.session;
if (session != null && (session.saveUninitialized || session.resave || session.modified)) {
if (session != null &&
(session.saveUninitialized || session.resave || session.modified)) {
await session.save();
res = res.withCookie(session.cookie!);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/pharaoh/lib/src/router/_handler_executor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ final class Executor {
}

Future<void> _resetStream() async {
void newStream() => _streamCtrl = StreamController<Middleware>()..add(_handler);
void newStream() =>
_streamCtrl = StreamController<Middleware>()..add(_handler);
final ctrl = _streamCtrl;
if (ctrl == null) return newStream();
if (ctrl.hasListener && ctrl.isClosed) await ctrl.close();
Expand Down
8 changes: 6 additions & 2 deletions packages/pharaoh/lib/src/router/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import 'package:spanner/spanner.dart';
import 'router_contract.dart';
import 'router_handler.dart';

typedef _PendingRouteIntent = (HTTPMethod method, ({String path, Middleware handler}));
typedef _PendingRouteIntent = (
HTTPMethod method,
({String path, Middleware handler})
);

class GroupRouter extends RouterContract {
final List<_PendingRouteIntent> _pendingRouteIntents = [];
Expand Down Expand Up @@ -89,7 +92,8 @@ class GroupRouter extends RouterContract {
}

@override
GroupRouter on(String path, Middleware func, {HTTPMethod method = HTTPMethod.ALL}) {
GroupRouter on(String path, Middleware func,
{HTTPMethod method = HTTPMethod.ALL}) {
if (method == HTTPMethod.ALL) path = '$path/*';
_pendingRouteIntents.add((method, (path: path, handler: func)));
return this;
Expand Down
3 changes: 2 additions & 1 deletion packages/pharaoh/lib/src/router/router_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ extension HandlerChainExtension on Middleware {
);
}

Middleware useRequestHandler(RequestHandler handler) => (req, res, next_) async {
Middleware useRequestHandler(RequestHandler handler) =>
(req, res, next_) async {
final result = await handler(req, res);
next_(result);
};
Loading
Loading