-
Notifications
You must be signed in to change notification settings - Fork 0
/
chopper.dart
180 lines (149 loc) · 4.43 KB
/
chopper.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import 'dart:async';
import 'dart:io';
import 'package:chopper/chopper.dart';
import 'package:chopper_authenticator_example/auth_repository.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'chopper.chopper.dart';
part 'chopper.g.dart';
@Riverpod(keepAlive: true)
ApiService apiService(ApiServiceRef ref) {
return ApiService.create(ref);
}
@ChopperApi()
abstract class ApiService extends ChopperService {
static ApiService create(ApiServiceRef ref) {
final client = ChopperClient(
client: mockClient(ref),
// Authenticator
authenticator: ref.read(myAuthenticatorProvider),
interceptors: [
// AuthInterceptor
ref.read(authInterceptorProvider),
],
services: [
_$ApiService(),
],
);
return _$ApiService(client);
}
static MockClient mockClient(ApiServiceRef ref) {
// Returns data if the token is valid or 401 otherwise
return MockClient(
(request) async {
await Future.delayed(const Duration(seconds: 1));
// Get currently valid token from remote server
final serverAccessToken =
ref.read(fakeRemoteServerProvider).accessToken;
// 1. If accessToken in the request doesn't match the token on the remote server,
// then return 401
if (request.headers[HttpHeaders.authorizationHeader] !=
serverAccessToken) {
return http.Response(
'Unauthorized',
401,
);
}
// 2. If tokens are equal, then return some fake data
return http.Response(
'Success',
200,
);
},
);
}
@Get(path: '/data')
Future<Response> getData();
}
//
// Auth interceptor
//
@riverpod
AuthInterceptor authInterceptor(AuthInterceptorRef ref) {
return AuthInterceptor(ref.watch(authRepositoryProvider));
}
class AuthInterceptor implements RequestInterceptor {
const AuthInterceptor(this._repo);
final AuthRepository _repo;
@override
FutureOr<Request> onRequest(Request request) {
final updatedRequest = applyHeader(
request,
HttpHeaders.authorizationHeader,
_repo.accessToken,
// Do not override existing header
override: false,
);
print(
'[AuthInterceptor] accessToken: ${updatedRequest.headers[HttpHeaders.authorizationHeader]}',
);
return updatedRequest;
}
}
//
// Authenticator
//
@riverpod
MyAuthenticator myAuthenticator(MyAuthenticatorRef ref) {
return MyAuthenticator(ref.watch(authRepositoryProvider));
}
class MyAuthenticator implements Authenticator {
MyAuthenticator(this._repo);
final AuthRepository _repo;
@override
FutureOr<Request?> authenticate(
Request request,
Response response, [
Request? originalRequest,
]) async {
print('[MyAuthenticator] response.statusCode: ${response.statusCode}');
print(
'[MyAuthenticator] request Retry-Count: ${request.headers['Retry-Count'] ?? 0}',
);
// 401
if (response.statusCode == HttpStatus.unauthorized) {
// Trying to update token only 1 time
if (request.headers['Retry-Count'] != null) {
print(
'[MyAuthenticator] Unable to refresh token, retry count exceeded',
);
return null;
}
try {
final newToken = await _refreshToken();
return applyHeaders(
request,
{
HttpHeaders.authorizationHeader: newToken,
// Setting the retry count to not end up in an infinite loop of unsuccessful updates
'Retry-Count': '1',
},
);
} catch (e) {
print('[MyAuthenticator] Unable to refresh token: $e');
return null;
}
}
return null;
}
// Completer to prevent multiple token refreshes at the same time
Completer<String>? _completer;
Future<String> _refreshToken() {
var completer = _completer;
if (completer != null && !completer.isCompleted) {
print('Token refresh is already in progress');
return completer.future;
}
completer = Completer<String>();
_completer = completer;
_repo.refreshToken().then((_) {
// Completing with a new token
completer?.complete(_repo.accessToken);
}).onError((error, stackTrace) {
// Completing with an error
completer?.completeError(error ?? 'Refresh token error', stackTrace);
});
return completer.future;
}
}