Skip to content

Commit

Permalink
Merge pull request #84 from rollbar/robust_http_error_handling
Browse files Browse the repository at this point in the history
More robust HTTP error handling
  • Loading branch information
matux authored Nov 14, 2022
2 parents 464f666 + 30ddab4 commit 0c028c5
Show file tree
Hide file tree
Showing 23 changed files with 864 additions and 252 deletions.
1 change: 1 addition & 0 deletions rollbar_common/lib/rollbar_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export 'src/data/breadcrumb_record.dart';
export 'src/http.dart';
export 'src/level.dart';
export 'src/tuple.dart';
export 'src/result.dart';
4 changes: 3 additions & 1 deletion rollbar_common/lib/src/data/breadcrumb_record.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import '../../rollbar_common.dart'
class BreadcrumbRecord implements Persistable<UUID> {
@override
final UUID id;
final String breadcrumb;
@override
final DateTime timestamp;

final String breadcrumb;

static Map<String, Datatype> get persistingKeyTypes => {
'id': Datatype.uuid,
'breadcrumb': Datatype.text,
Expand Down
4 changes: 3 additions & 1 deletion rollbar_common/lib/src/data/payload_record.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import '../../rollbar_common.dart'
class PayloadRecord implements Persistable<UUID> {
@override
final UUID id;
@override
final DateTime timestamp;

final String accessToken;
final String endpoint;
final String payload;
final DateTime timestamp;

static Map<String, Datatype> get persistingKeyTypes => {
'id': Datatype.uuid,
Expand Down
23 changes: 23 additions & 0 deletions rollbar_common/lib/src/http.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
import 'package:http/http.dart' as http;

enum HttpMethod { get, head, post, put, delete, connect, options, trace, patch }

enum HttpStatus { info, success, redirect, clientError, serverError }

typedef HttpHeaders = Map<String, String>;

extension HttpMethodName on HttpMethod {
String get name => toString().split('.').last.toUpperCase();
}

extension HttpResponseExtension on http.Response {
/// The HTTP status for this response derived from the status code.
HttpStatus get status {
if (statusCode >= 100 && statusCode < 200) {
return HttpStatus.info;
} else if (statusCode >= 200 && statusCode < 300) {
return HttpStatus.success;
} else if (statusCode >= 300 && statusCode < 400) {
return HttpStatus.redirect;
} else if (statusCode >= 400 && statusCode < 500) {
return HttpStatus.clientError;
} else if (statusCode >= 500 && statusCode < 600) {
return HttpStatus.serverError;
} else {
throw StateError('http status code $statusCode is invalid.');
}
}
}
13 changes: 13 additions & 0 deletions rollbar_common/lib/src/identifiable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ const uuidGen = Uuid();

typedef UUID = UuidValue;

final nilUUID = UUID("00000000-0000-0000-0000-000000000000");

abstract class Identifiable<T extends Object> {
T get id;
}

extension IterableIntoUUID on Iterable<int> {
UUID toUUID() => UUID.fromList(toList());
}

extension StringIntoUUID on String {
UUID toUUID() => RegExp(r'\w\w')
.allMatches(this)
.map((match) => int.parse(match[0]!, radix: 16))
.toUUID();
}
2 changes: 2 additions & 0 deletions rollbar_common/lib/src/persistable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ extension DatatypeSqlType on Datatype {
/// `(key, value)` pairs through [Serializable.fromMap] and [toMap].
abstract class Persistable<T extends Object>
implements Serializable, Comparable<Persistable<T>>, Identifiable<T> {
DateTime get timestamp;

static const _map = <Type, PersistableFor>{
Persistable: PersistableFor(),
PayloadRecord: PersistablePayloadRecord(),
Expand Down
51 changes: 51 additions & 0 deletions rollbar_common/lib/src/result.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:meta/meta.dart';

abstract class Result<T, E extends Error /*| Exception*/ > {
bool get isSuccess => this is Success<T, E>;
bool get isFailure => this is Failure<T, E>;

T get success => (this as Success<T, E>).value;
E get failure => (this as Failure<T, E>).error;

Result<U, E> map<U>(U Function(T) transform) => isSuccess
? Success(transform((this as Success<T, E>).value))
: Failure((this as Failure<T, E>).error);
}

@sealed
@immutable
class Success<T, E extends Error> extends Result<T, E> {
final T value;

Success(this.value);

@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Success<T, E> && other.value == value);

@override
int get hashCode => value.hashCode;

@override
String toString() => 'Result.Success($value)';
}

@sealed
@immutable
class Failure<T, E extends Error> extends Result<T, E> {
final E error;

Failure(this.error);

@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Failure<T, E> && other.error == error);

@override
int get hashCode => error.hashCode;

@override
String toString() => 'Result.Failure($error)';
}
16 changes: 16 additions & 0 deletions rollbar_common/lib/src/tuple.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import 'package:meta/meta.dart';

import 'zipped.dart';

@sealed
@immutable
class Tuple2<T1, T2> {
final T1 first;
final T2 second;

T1 get $1 => first;
T2 get $2 => second;

const Tuple2(this.first, this.second);

factory Tuple2.empty() => Tuple2(null as T1, null as T2);
Expand All @@ -22,6 +27,17 @@ class Tuple2<T1, T2> {
}
}

// Takes two nullables and returns a nullable of the corresponding pair.
static Tuple2<A, B>? zip<A, B>(A? first, B? second) =>
first != null && second != null ? Tuple2(first, second) : null;

// Takes two iterables and returns a single iterable of corresponding pairs.
static Iterable<Tuple2<A, B>> zipIt<A, B>(
Iterable<A> first,
Iterable<B> second,
) =>
ZippedIterable(first, second);

Tuple2<U, T2> mapFirst<U>(U Function(T1) f) => Tuple2(f(first), second);
Tuple2<T1, U> mapSecond<U>(U Function(T2) f) => Tuple2(first, f(second));

Expand Down
1 change: 1 addition & 0 deletions rollbar_common/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ environment:
sdk: '>=2.17.0 <3.0.0'

dependencies:
http: ^0.13.0
collection: ^1.16.0
sqlite3: ^1.7.0
uuid: ^3.0.0
Expand Down
3 changes: 3 additions & 0 deletions rollbar_dart/lib/src/config.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:meta/meta.dart';
import 'package:http/http.dart' as http;
import 'package:rollbar_common/rollbar_common.dart';

import '../rollbar.dart';
Expand Down Expand Up @@ -33,6 +34,7 @@ class Config implements Serializable {
final Wrangler Function(Config) wrangler;
final Transformer Function(Config) transformer;
final Sender Function(Config) sender;
final http.Client Function() httpClient;

const Config({
required this.accessToken,
Expand All @@ -49,6 +51,7 @@ class Config implements Serializable {
this.wrangler = DataWrangler.new,
this.transformer = NoopTransformer.new,
this.sender = PersistentHttpSender.new,
this.httpClient = http.Client.new,
});

Config copyWith({
Expand Down
75 changes: 28 additions & 47 deletions rollbar_dart/lib/src/data/response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,41 @@ import 'package:meta/meta.dart';
import 'package:http/http.dart' as http;
import 'package:rollbar_common/rollbar_common.dart';

/// Represents the response from the Rollbar API.
///
/// Rollbar will respond with either an error [message] xor a [Result].
@sealed
@immutable
class Response
with EquatableSerializableMixin
implements Serializable, Equatable {
final int error;
final String? message;
final UUID? result;

Response({this.error = 0, this.message, this.result}) {
if (error == 0 && message == null) {
ArgumentError.checkNotNull(result, 'result');
}
}

bool get isError => error != 0;

Response copyWith({int? error, String? message, UUID? result}) => Response(
error: error ?? this.error,
message: message ?? this.message,
result: result ?? this.result);

@override
JsonMap toMap() => {
'err': error,
'message': message,
'result': {'uuid': result?.uuid}.compact()
}.compact();

factory Response.fromMap(JsonMap map) =>
Response(error: map.error, message: map.message, result: map.uuid);
class ResponseError extends Error {
final int code;
final String message;

factory Response.from(http.Response response) =>
Response.fromMap(jsonDecode(response.body));
ResponseError(this.code, this.message);

@override
String toString() =>
'Response(error: $error, message: $message, result: $result)';
String toString() => '$code $message';
}

extension _Attributes on JsonMap {
int get error => this['err']?.toInt() ?? 0;

String? get message => this['message'];
/// Represents the response from the Rollbar API.
///
/// Rollbar will respond with either an error [message] xor a [UUID].
extension APIResponse on http.Response {
Result<UUID, ResponseError> get result => //
status == HttpStatus.success
? Success(body.uuid)
: Failure(ResponseError(
body.error ?? statusCode,
body.reason ?? reasonPhrase ?? status.name,
));
}

UUID? get uuid {
final uuid = this['result']?['uuid'] as String?;
final byteList = uuid
.map(RegExp(r'\w\w').allMatches)
?.map((match) => int.parse(match[0]!, radix: 16))
.toList();
return byteList.map(UUID.fromList);
extension _Attributes on String {
JsonMap get body {
try {
return jsonDecode(this);
} catch (_) {
return {};
}
}

int? get error => body['err'];
String? get reason => body['message'];
UUID get uuid => (body['result']['uuid'] as String?)?.toUUID() ?? nilUUID;
}
3 changes: 3 additions & 0 deletions rollbar_dart/lib/src/persistence.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ mixin Persistence<Record extends Persistable<UUID>> implements Configurable {

return _database as Database;
}());

bool didExpire(Record record) =>
record.timestamp < DateTime.now().toUtc() - config.persistenceLifetime;
}
Loading

0 comments on commit 0c028c5

Please sign in to comment.