Skip to content

Commit

Permalink
chore: Pass stack-trace along with Exception (#108)
Browse files Browse the repository at this point in the history
* pass stack trace along with error

* fix failing tests

* fix passing unknown route as parametric

* todo: not needed anymore

* default res.send function

* no longer need this test

* fix code formatting issues
  • Loading branch information
codekeyz authored Jan 28, 2024
1 parent f01f099 commit a307621
Show file tree
Hide file tree
Showing 46 changed files with 625 additions and 330 deletions.
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

0 comments on commit a307621

Please sign in to comment.