GetDutchie / brick

An intuitive way to work with persistent data in Dart
https://getdutchie.github.io/brick/#/
361 stars 28 forks source link

Handling Expired Access Keys in Offline Request Queue #390

Closed devj3ns closed 4 months ago

devj3ns commented 4 months ago

I encountered the issue that when a request is queued while the user is offline and more than an hour passes before the user is back online, the JWT expires, leading to a 401 error which means the request will never be deleted from the request queue. (My list of status codes for reattempting contains 401 to avoid data loss).

I see some possible solutions:

What is the intended way of this library to handle this? And are there any examples showcasing this?

Thank you!

tshedor commented 4 months ago

@devj3ns The best thing to do would be your second solution - attach the JWT at the time of sending/retry. The offline queue directly accesses RestProvider#client. So I'd recommend attaching your JWT at the HTTP client level and composing it within an HTTP client. I know that sounds like a lot of jargon, so please take a look at this example and let me know if that all makes sense.

Once you've got your inner client, you can create your RestProvider with RestProvider('endpoint', modelDictionary: restModelDictionary, client: jwtClient)

devj3ns commented 4 months ago

Thanks, @tshedor! The example was very helpful, and I was able to get it working quickly.

For anyone looking to implement this with Supabase as the auth provider, here is the code:

import 'package:http/http.dart' as http;
import 'package:supabase_flutter/supabase_flutter.dart';

/// Client which adds the current JWT access token (auto refreshed by the supabase auth client) to the request headers.
class JWTClient extends http.BaseClient {
  JWTClient({
    http.Client? innerClient,
    this.resourceName = 'dart.http',
  }) : _innerClient = innerClient ?? http.Client();

  /// Populates APM's "RESOURCE" column. Defaults to `dart.http`.
  final String resourceName;

  /// A normal HTTP client, treated like a manual `super`
  /// as detailed by [the Dart team](https://github.com/dart-lang/http/blob/378179845420caafbf7a34d47b9c22104753182a/README.md#using)
  ///
  /// By default, a new [http.Client] will be instantiated and used.
  final http.Client _innerClient;

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    // the access token is automatically refreshed by the supabase client
    final accessToken = Supabase.instance.client.auth.currentSession?.accessToken;

    if (accessToken != null) {
         request.headers.addAll({'Authorization': 'Bearer $accessToken'});
    }

    return _innerClient.send(request);
  }

  @override
  void close() {
    _innerClient.close();
    super.close();
  }
}