retrofit.dart icon indicating copy to clipboard operation
retrofit.dart copied to clipboard

feat(generator): Add ParseErrorLogger

Open Sadhorsephile opened this issue 8 months ago • 0 comments

Problem

Sometimes, the back-end changes the response structure unexpectedly. For example, our app expects this:

 {
    "id": "0",
    "name": "Name"
 }

but we get this instead:

{
    "id": 0,
    "name": "Name"
 }

Logging these discrepancies would be really useful. To make the logs as helpful as possible, we need to capture:

  • Request details (path, URI, params, etc.);
  • Parsing error details (we can use CheckedFromJsonException from json_serializable).

The most logical place for such logging is within the client.

Solution

Let's introduce a new entity - ParseErrorLogger :

import 'package:dio/dio.dart';

/// Base class for logging errors that occur during parsing of response data.
abstract class ParseErrorLogger {
  /// Logs an error that occurred during parsing of response data.
  /// 
  /// - [error] is the error that occurred.
  /// - [stackTrace] is the stack trace of the error.
  /// - [options] are the options that were used to make the request.
  void logError(Object error, StackTrace stackTrace, RequestOptions options);
}

This entity can be injected into our client:

import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';

part 'rest_client.g.dart';

@RestApi()
abstract class RestClient {
  /// API creation factory using [Dio].
  factory RestClient(
    Dio dio, {
    String baseUrl,
    ParseErrorLogger? errorLogger,
  }) = _RestClient;

  @GET('')
  Future<SomeDto> someRequest();
}

And this entity will be called if parsing fails.

For example, this is rest_client.g.dart. As shown in the After example, errorLogger is called if there are any issues with parsing:

Before After
  @override
  Future<SomeDto> someRequest() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final Map<String, dynamic>? _data = null;
    final _result = await _dio.fetch<Map<String, dynamic>>(_setStreamType<SomeDto>(Options(
      method: 'GET',
      headers: _headers,
      extra: _extra,
    )
        .compose(
          _dio.options,
          '',
          queryParameters: queryParameters,
          data: _data,
        )
        .copyWith(
            baseUrl: _combineBaseUrls(
          _dio.options.baseUrl,
          baseUrl,
        ))));
    final value = SomeDto.fromJson(_result.data!);
    return value;
  }
  @override
  Future<SomeDto> someRequest() async {
    final _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    const Map<String, dynamic>? _data = null;
    final options = _setStreamType<SomeDto>(Options(
      method: 'GET',
      headers: _headers,
      extra: _extra,
    )
        .compose(
          _dio.options,
          '',
          queryParameters: queryParameters,
          data: _data,
        )
        .copyWith(
            baseUrl: _combineBaseUrls(
          _dio.options.baseUrl,
          baseUrl,
        )));
    final _result = await _dio.fetch<Map<String, dynamic>>(options);
    late SomeDto value;
    try {
      value = SomeDto.fromJson(_result.data!);
    } on Object catch (e, s) {
      errorLogger?.logError(e, s, options);
      rethrow;
    }
    return value;
  }

Sadhorsephile avatar Jun 07 '24 13:06 Sadhorsephile