Device-side Replay
Re-execute failed requests from the device when the server requests it.
Why Device-Side Replay?
Some requests cannot be replayed from the server because they require resources only available on the user's device:
- Short-lived auth tokens — JWT tokens expire quickly. The original token captured with the failure is no longer valid, but the device can generate a fresh one.
- Refresh token flow — Only the device has the refresh token stored securely in the keychain to obtain new access tokens.
- Biometric/PIN authentication — Some operations require user presence verification that only the device can perform.
- Device-specific headers — Device ID, push notification tokens, or hardware identifiers that the server doesn't have.
- Local file access — If the replay response contains upload URLs, only the device has access to the original files.
Setting Up Replay
Configure the onReplayRequest callback during SDK initialization. This callback runs on the device when the server requests a replay:
await EndpointVault.init(
apiKey: 'your-api-key',
encryptionKey: 'your-32-char-key',
onReplayRequest: (eventId, request) async {
// This runs ON THE DEVICE when server requests replay.
// We can access device-only resources like:
// - Fresh auth tokens from secure storage
// - Biometric verification
// - Local files for upload
try {
// Get a fresh token - the original one has expired
final token = await authService.getFreshToken();
final response = await dio.request(
request.url,
data: request.requestBody,
options: Options(
method: request.method,
headers: {
...?request.requestHeaders?.cast<String, dynamic>(),
'Authorization': 'Bearer $token', // Fresh token
},
),
);
return response;
} catch (e) {
return null;
}
},
);
How it works: When you click "Retry on Device" in the dashboard, the server sends a replay command. Next time the device syncs with the server, the SDK calls your callback with the original request data.
Handling Post-Replay Operations
Handle any post-replay logic directly in the onReplayRequest callback. A common case is when the API response contains presigned upload URLs — only the device has access to the local files:
await EndpointVault.init(
apiKey: 'your-api-key',
encryptionKey: 'your-32-char-key',
onReplayRequest: (eventId, request) async {
try {
// Get fresh auth token from device's secure storage
final token = await authService.getFreshToken();
final response = await dio.request(
request.url,
data: request.requestBody,
options: Options(
method: request.method,
headers: {
...?request.requestHeaders?.cast<String, dynamic>(),
'Authorization': 'Bearer $token',
},
),
);
// Post-replay: upload files using URLs from response
// Only the device has access to these local files
if (response.data is Map && response.data['uploadUrls'] != null) {
final uploadUrls = response.data['uploadUrls'] as List;
final localFiles = await getLocalFilesForEvent(eventId);
for (int i = 0; i < uploadUrls.length; i++) {
await uploadLocalFile(uploadUrls[i], localFiles[i]);
}
}
return response;
} catch (e) {
print('Replay failed: $e');
return null;
}
},
);
Complete Example
Here's a full example showing initialization with replay support:
// In main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
await EndpointVault.init(
apiKey: 'your-api-key',
encryptionKey: 'your-32-char-encryption-key!',
environment: kDebugMode ? 'development' : 'production',
// This callback runs ON THE DEVICE when server requests replay
onReplayRequest: (eventId, request) async {
try {
// The original auth token has expired by now.
// Only the device can get a fresh one using the refresh token
// stored in secure storage (Keychain/Keystore).
final token = await authService.getFreshToken();
final response = await dio.request(
request.url,
data: request.requestBody,
options: Options(
method: request.method,
headers: {
...?request.requestHeaders?.cast<String, dynamic>(),
'Authorization': 'Bearer $token',
},
),
);
// Handle any post-replay workflows that need device resources
await handleSuccessfulReplay(eventId, response);
return response;
} catch (e) {
print('Replay failed: $e');
return null;
}
},
);
// Add the interceptor to your Dio instance
dio.interceptors.add(EndpointVaultInterceptor());
runApp(MyApp());
}
Manual Replay (Alternative)
If you prefer manual control, you can poll for replay requests:
// Check periodically or on app resume
Future<void> checkForReplays() async {
final request = await EndpointVault.instance.checkForReplayRequest();
if (request != null) {
final success = await EndpointVault.instance.executeReplay(
replayRequest: request,
dio: myDio,
);
print('Replay ${success ? 'succeeded' : 'failed'}');
}
}
How the Flow Works
- App captures failure with
captureFailure()(automatically via interceptor or manually) - SDK sends encrypted failure data + statistics to server
- Server responds with
{"replay": {"eventId": "..."}}when it wants a replay - SDK automatically calls your
onReplayRequestcallback - Your callback re-executes the request and handles any post-replay operations
- Return the Response to report success, or null to report failure
