generated from ttipo/project-base
fix(flutter): 토큰 갱신 429 로 로그아웃되지 않게, 갱신 뒤 사진 다시 보내기
리뷰에서 나온 앱 쪽 문제를 고친다. - 토큰 갱신이 429 를 받으면 세션을 끊던 문제 — 거절(401·400)만 세션을 끊고, 429 는 REQ_001·그 밖은 SYS_001 로 알린다 (서버는 갱신을 IP 당 분당 10회로 막아 통신사 망처럼 여럿이 한 IP 를 쓰면 실제로 생긴다) - 액세스 토큰이 만료된 뒤 사진을 올리면 다시 보낼 때 FormData 가 이미 쓰여 실패하던 문제 — 복사본으로 다시 보낸다 - 에러 사전 계약 테스트에 앱이 직접 쓰는 도메인 코드(FILE·DIARY·CARD·SCHED)를 더한다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,8 @@ const String _kRetriedExtra = 'authRetried';
|
||||
const String _kRefreshUnreachableExtra = 'refreshUnreachable';
|
||||
|
||||
/// 토큰 갱신 결과
|
||||
enum _RefreshResult { refreshed, rejected, unreachable }
|
||||
/// 토큰 갱신 결과 — 거절(401·400)만 세션을 끊는다. 서버에 닿지 못했거나 너무 잦아 막혔으면(429) 세션은 그대로 둔다
|
||||
enum _RefreshResult { refreshed, rejected, unreachable, throttled }
|
||||
|
||||
/// 토큰 갱신 경로 (backend AuthController `POST /auth/tokens`)
|
||||
const String kRefreshPath = '/auth/tokens';
|
||||
@@ -134,12 +135,17 @@ class DioClient {
|
||||
if (sent != 'Bearer ${tokens.accessToken}') return true;
|
||||
|
||||
final result = await _refreshOnce();
|
||||
if (result == _RefreshResult.unreachable) options.extra[_kRefreshUnreachableExtra] = true;
|
||||
// 갱신하지 못했지만 다시 로그인할 일은 아닐 때 — 이번 요청이 어떤 코드로 실패할지 남겨 둔다
|
||||
if (result == _RefreshResult.unreachable) options.extra[_kRefreshUnreachableExtra] = 'SYS_001';
|
||||
if (result == _RefreshResult.throttled) options.extra[_kRefreshUnreachableExtra] = 'REQ_001';
|
||||
return result == _RefreshResult.refreshed;
|
||||
}
|
||||
|
||||
RequestOptions _withFreshToken(RequestOptions options) {
|
||||
options.extra[_kRetriedExtra] = true;
|
||||
// 사진 올리기(FormData)는 한 번 보내면 다시 쓸 수 없다 — 다시 보낼 때는 복사본으로
|
||||
final data = options.data;
|
||||
if (data is FormData) options.data = data.clone();
|
||||
final token = _session.tokens?.accessToken;
|
||||
if (token != null) options.headers['Authorization'] = 'Bearer $token';
|
||||
return options;
|
||||
@@ -162,9 +168,12 @@ class DioClient {
|
||||
await _session.save(AuthTokens.fromJson(data));
|
||||
return _RefreshResult.refreshed;
|
||||
} on DioException catch (e) {
|
||||
final response = e.response;
|
||||
if (response == null || (response.statusCode ?? 0) >= 500) return _RefreshResult.unreachable;
|
||||
return _RefreshResult.rejected;
|
||||
final status = e.response?.statusCode;
|
||||
// 서버가 리프레시 토큰을 받아들이지 않았을 때(401·형식 오류 400)만 세션을 끊는다.
|
||||
// 429(한 IP 를 여럿이 쓰는 통신사 망에서도 생긴다)·5xx·응답 없음은 잠깐의 문제라 세션을 둔다
|
||||
if (status == 401 || status == 400) return _RefreshResult.rejected;
|
||||
if (status == 429) return _RefreshResult.throttled;
|
||||
return _RefreshResult.unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +203,9 @@ class DioClient {
|
||||
// 갱신하려다 서버에 닿지 못했으면 다시 로그인할 일이 아니므로 일시적 오류(SYS_001)로 둔다.
|
||||
// AUTH_002 는 서버가 내려주지 않는 클라이언트 전용 코드다.
|
||||
if (code == 'AUTH_001' && e.requestOptions.headers['Authorization'] != null) {
|
||||
if (e.requestOptions.extra[_kRefreshUnreachableExtra] == true) {
|
||||
code = 'SYS_001';
|
||||
final kept = e.requestOptions.extra[_kRefreshUnreachableExtra];
|
||||
if (kept is String) {
|
||||
code = kept;
|
||||
} else {
|
||||
if (_session.hasSession) await _session.clear(expired: true);
|
||||
code = 'AUTH_002';
|
||||
|
||||
Reference in New Issue
Block a user