① 3) 에러가 났을 때 먼저 작업해보기
onError를 작성하면 자동완성 됨.
에러가 발생했을 땐 어떤 상황을 캐치하고싶은지가 중요하다.
현재 나는 토큰에 문제가 있을 때 401 에러 났을 때 캐치하고싶음!
super.onError(err, handler); 앞에 return 붙여주고
그 위에 어떤 에러를 캐치하고싶은지 작성하면 됨.
// 3) 에러가 났을 때
void onError(DioException err, ErrorInterceptorHandler handler) {
// 401 에러 발생 시(statue code)
// 토큰을 재발급 받는 시도를 하고 토큰이 재발급되면
// 다시 새로운 토큰으로 요청을 함
return super.onError(err, handler);
}
}
}
401 에러가 만약 났다면 이를 인지해서
새로 토큰을 발급받을 수 있는 Auth Token URL에 요청을 넣고,
그렇게 새로 발급받은 accessToken으로 저장해서
원래 보내려고 했던 요청(= accessToken에서 에러나서 제대로된 응답을 받지 못한 그 요청!)에
새로운 토큰으로 요청을 넣어서 결과 값(return super.onError(err, handler); 이거!)을
다시 반환해 주는 것!
이걸 해 볼 건데...
일단 에러가 난 상황이라고 가정해보자!
// 3) 에러가 났을 때
void onError(DioException err, ErrorInterceptorHandler handler) {
// 401 에러 발생 시(statue code)
// 토큰을 재발급 받는 시도를 하고 토큰이 재발급되면
// 다시 새로운 토큰으로 요청을 함
// 프린트로 어떤 요청, 어떤 URL의 요청에서 에러가 났는지 확인
print('[ERR] [${err.requestOptions.method}] ${err.requestOptions.uri}');
return super.onError(err, handler);
}
}
}
② refreshToken 새로 가져오기!
근데 refreshToken 왜 가져올까? 어떻게든 써야하기 때문이다.
그게 무슨 말이냐면, 401 에러가 만약 났다면 이 refreshToken 사용해서
accessToken을 새로 발급받아야하기 때문이라는 것!
// 3) 에러가 났을 때
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 401 에러 발생 시(statue code)
// 토큰을 재발급 받는 시도를 하고 토큰이 재발급되면
// 다시 새로운 토큰으로 요청을 함
// 프린트로 어떤 요청, 어떤 URL의 요청에서 에러가 났는지 확인
print('[ERR] [${err.requestOptions.method}] ${err.requestOptions.uri}');
// 3)-1. refreshToken 가져오기
final refreshToken = await storage.read(key: REFRESH_TOKEN_KEY);
// 3)-2. 가져왔는데 refreshToken이 아예 없으면 에러를 던진다
if (refreshToken == null) {
// 에러를 던질 때는 handler.reject 사용(dio 룰임)
return handler.reject(err);
}
// 3)-3. 401 상태인지 확인
// requestOptions => 요청의 모든 값을 가져올 수 있음
// response => 응답의 모든 값을 가져올 수 있음
// (응답이 없을 수도 있으니깐 물음표)
// statusCode가 401이면 isStatus401는 true
final isStatus401 = err.response?.statusCode == 401;
// 3)-4. 에러가 난 요청이 토큰을 리프레시하려다 에러가 났는지를 확인
// 여기서 true를 반환받으면 '/auth/token'에 accessToken을
// 새로 발급받으려다 에러가 난 거라서 refreshToken 자체에 문제가 있다는 의미!
// => 결국 새로 요청을 보내봤자 에러가 남...
// 이럴 땐, 리젝트를 또 해줘야 함!
final isPathRefresh = err.requestOptions.path == '/auth/token';
// 3)-5. isStatus401이 true고 isPathRefresh가 false면
// (즉, 토큰을 새로 리프레시 하려는 의도가 아니었는데 401에러가 발생했다면)
if (isStatus401 && !isPathRefresh) {
// 3)-6. dio를 새로 생성하고, dio로 토큰 리프레시 요청
final dio = Dio();
// resp 요청을 보내면 dio로 post 요청을 보내서
// auth/token에 refreshToken을 사용해서
// 새로운 accessToken을 발급받을 수 있게 됨!
final resp = await dio.post(
'http://$ip/auth/token',
options: Options(
headers: {
'authorization': 'Bearer $refreshToken',
},
),
);
// 3)-7. 실제 accessToken 가져오기
// 그럴려면 데이터에서 accessToken이라는 값을 가져와야 함!
final accessToken = resp.data['accessToken'];
}
// response => 무언가 응답을 받아와서 이 안에 넣어줘야 함!
// 그렇게 되면, onError가 났음에도 불구하고
// 실제로 요청을 실행한 화면에서는 에러가 나지 않을 것처럼 인식됨.
// return handler.resolve(response);
return super.onError(err, handler);
}
}
}
그런데 resp에서 에러를 반환받았다면??
에러를 잡아주자!
드래그한 코드를 try-catch문으로 옮겨준다.
import 'package:dio/dio.dart';
import 'package:flutter_actual/common/const/data.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class CustomInterceptor extends Interceptor {
final FlutterSecureStorage storage;
// 스토리지에 넣어주기
CustomInterceptor({
required this.storage,
});
// 1) 요청을 보낼 때
// 요청이 보내질 때마다
// 만약에 요청의 Header에 accessToken: true 라는 값이 있다면,
// 실제 토큰을 storage에서 가져와서
// authorization : Bearer $token으로 Header를 변경
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
// 이때 method는 get, post, delete를 의미
print('[REQ] [${options.method}] ${options.uri}');
// headers = 실제 요청의 헤더 => @Headers({'accessToken': 'true'}, )
// headers에서 accessToken이란 값이 true라면 accessToken이라는 키를 삭제!
if (options.headers['accessToken'] == 'true') {
options.headers.remove('accessToken');
// 그리고 진짜 토큰으로 바꿔주기
final token = await storage.read(key: ACCESS_TOKEN_KEY);
// 토큰을 가져왔다면 어떻게 넣어야 할까?
//return super.onRequest(options, handler);
options.headers.addAll({'authorization': 'Bearer $token'});
return super.onRequest(options, handler); // handler.next(options); 반드시 호출
}
// 2) 응답을 받을 때
// 3) 에러가 났을 때
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 401 에러 발생 시(statue code)
// 토큰을 재발급 받는 시도를 하고 토큰이 재발급되면
// 다시 새로운 토큰으로 요청을 함
// 프린트로 어떤 요청, 어떤 URL의 요청에서 에러가 났는지 확인
print('[ERR] [${err.requestOptions.method}] ${err.requestOptions.uri}');
// 3)-1. refreshToken 가져오기
final refreshToken = await storage.read(key: REFRESH_TOKEN_KEY);
// 3)-2. 가져왔는데 refreshToken이 아예 없으면 에러를 던진다
if (refreshToken == null) {
// 에러를 던질 때는 handler.reject 사용(dio 룰임)
return handler.reject(err);
}
// 3)-3. 401 상태인지 확인
// requestOptions => 요청의 모든 값을 가져올 수 있음
// response => 응답의 모든 값을 가져올 수 있음
// (응답이 없을 수도 있으니깐 물음표)
// statusCode가 401이면 isStatus401는 true
final isStatus401 = err.response?.statusCode == 401;
// 3)-4. 에러가 난 요청이 토큰을 리프레시하려다 에러가 났는지를 확인
// 여기서 true를 반환받으면 '/auth/token'에 accessToken을
// 새로 발급받으려다 에러가 난 거라서 refreshToken 자체에 문제가 있다는 의미!
// => 결국 새로 요청을 보내봤자 에러가 남...
// 이럴 땐, 리젝트를 또 해줘야 함!
final isPathRefresh = err.requestOptions.path == '/auth/token';
// 3)-5. isStatus401이 true고 isPathRefresh가 false면
// (즉, 토큰을 새로 리프레시 하려는 의도가 아니었는데 401에러가 발생했다면)
if (isStatus401 && !isPathRefresh) {
// 3)-6. dio를 새로 생성하고, dio로 토큰 리프레시 요청
final dio = Dio();
// 3)-8. 그런데 resp에서 에러를 반환받았다면?
// 에러를 잡아주자!
// 어떤 이유든 여기서 에러가 나면,
// 더이상 토큰을 리프레시할 수 있는 상황이 아님...
try {
// resp 요청을 보내면 dio로 post 요청을 보내서
// auth/token에 refreshToken을 사용해서
// 새로운 accessToken을 발급받을 수 있게 됨!
final resp = await dio.post(
'http://$ip/auth/token',
options: Options(
headers: {
'authorization': 'Bearer $refreshToken',
},
),
);
// 3)-7. 실제 accessToken 가져오기
// 그럴려면 데이터에서 accessToken이라는 값을 가져와야 함!
final accessToken = resp.data['accessToken'];
// 3)-11. 만약 에러가 나지 않았다면?
// requestOptions를 가져온다.
final options = err.requestOptions;
// 3)-12. accessToken 새로 넣어주기(토큰 변경하기)
// 그럼 토큰을 넣을 수 있음.
// 하지만 새로 토큰을 발급받았기 때문에
// final FlutterSecureStorage storage 안에도 업데이트가 필요!
options.headers.addAll(
{
'authorization': 'Bearer $accessToken',
},
);
// 3)-13. final FlutterSecureStorage storage 업데이트
await storage.write(key: ACCESS_TOKEN_KEY, value: accessToken);
// 3)-14.원래 보냈던 요청 다시 보내기(요청 재전송)
// 위에 생성해준 dio에 fetch 붙여주기
// 그러면 (requestOptions)이 자동완성 되는데,
// 이를 통해 실제 요청을 보낼 때 필요한 모든 값들은
// requestOptions 안에 들어있는 걸 알 수 있음!
// 3)-15. requestOptions에 그냥 options 넣기
// 그러면 실제 err를 발생시킨 모든 요청과 관련된 옵션들을 다 받아서
// 토큰만 바꾼 다음 다시 요청을 다시 보내는 것!
final response = await dio.fetch(options);
// 3)-16. final response = await dio.fetch(options); 이렇게
// 응답이 오면 onError가 불렸지만, 실제로 반환해야 하는 값은
// 응답(요청)이 잘 왔다고 await dio.fetch(options); 에서
// 받은 응답을 다시 되돌려줘야 함!
} on DioException catch (e) {
// 3)-10. 그냥 catch에서 수정
// on DioException catch (e)로 바꿔주면 (아래 reject도 e를 넣어줌)
// 그냥 catch로 에러 전체를 잡아도 되지만,
// 예상되는 건 DioError니까 이렇게 따로 잡으면 좀 더 합리적임
// 3)-9. 더이상 토큰을 리프레시할 수 있는
// 상황이 아니라면 그냥 에러를 던져줌.
return handler.reject(e);
}
}
// response => 무언가 응답을 받아와서 이 안에 넣어줘야 함!
// 그렇게 되면, onError가 났음에도 불구하고
// 실제로 요청을 실행한 화면에서는 에러가 나지 않을 것처럼 인식됨.
// handler.resolve => 요청이 잘 끝났다는 의미
return handler.resolve(response);
}
}
}
final response = await dio.fetch(options); 이렇게 응답이 오면
onError가 불렸지만, 실제로 반환해야 하는 값은
응답(요청)이 잘 왔다고 await dio.fetch(options); 에서
받은 응답을 다시 되돌려줘야 함!
return handler.resolve(response); 이 코드를
final response = await dio.fetch(options); 아래에 집어 넣어준다.
import 'package:dio/dio.dart';
import 'package:flutter_actual/common/const/data.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class CustomInterceptor extends Interceptor {
final FlutterSecureStorage storage;
// 스토리지에 넣어주기
CustomInterceptor({
required this.storage,
});
// 1) 요청을 보낼 때
// 요청이 보내질 때마다
// 만약에 요청의 Header에 accessToken: true 라는 값이 있다면,
// 실제 토큰을 storage에서 가져와서
// authorization : Bearer $token으로 Header를 변경
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
// 이때 method는 get, post, delete를 의미
print('[REQ] [${options.method}] ${options.uri}');
// headers = 실제 요청의 헤더 => @Headers({'accessToken': 'true'}, )
// headers에서 accessToken이란 값이 true라면 accessToken이라는 키를 삭제!
if (options.headers['accessToken'] == 'true') {
options.headers.remove('accessToken');
// 그리고 진짜 토큰으로 바꿔주기
final token = await storage.read(key: ACCESS_TOKEN_KEY);
// 토큰을 가져왔다면 어떻게 넣어야 할까?
//return super.onRequest(options, handler);
options.headers.addAll({'authorization': 'Bearer $token'});
return super.onRequest(options, handler); // handler.next(options); 반드시 호출
}
// 2) 응답을 받을 때
// 3) 에러가 났을 때
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 401 에러 발생 시(statue code)
// 토큰을 재발급 받는 시도를 하고 토큰이 재발급되면
// 다시 새로운 토큰으로 요청을 함
// 프린트로 어떤 요청, 어떤 URL의 요청에서 에러가 났는지 확인
print('[ERR] [${err.requestOptions.method}] ${err.requestOptions.uri}');
// 3)-1. refreshToken 가져오기
final refreshToken = await storage.read(key: REFRESH_TOKEN_KEY);
// 3)-2. 가져왔는데 refreshToken이 아예 없으면 에러를 던진다
if (refreshToken == null) {
// 에러를 던질 때는 handler.reject 사용(dio 룰임)
return handler.reject(err);
}
// 3)-3. 401 상태인지 확인
// requestOptions => 요청의 모든 값을 가져올 수 있음
// response => 응답의 모든 값을 가져올 수 있음
// (응답이 없을 수도 있으니깐 물음표)
// statusCode가 401이면 isStatus401는 true
final isStatus401 = err.response?.statusCode == 401;
// 3)-4. 에러가 난 요청이 토큰을 리프레시하려다 에러가 났는지를 확인
// 여기서 true를 반환받으면 '/auth/token'에 accessToken을
// 새로 발급받으려다 에러가 난 거라서 refreshToken 자체에 문제가 있다는 의미!
// => 결국 새로 요청을 보내봤자 에러가 남...
// 이럴 땐, 리젝트를 또 해줘야 함!
final isPathRefresh = err.requestOptions.path == '/auth/token';
// 3)-5. isStatus401이 true고 isPathRefresh가 false면
// (즉, 토큰을 새로 리프레시 하려는 의도가 아니었는데 401에러가 발생했다면)
if (isStatus401 && !isPathRefresh) {
// 3)-6. dio를 새로 생성하고, dio로 토큰 리프레시 요청
final dio = Dio();
// 3)-8. 그런데 resp에서 에러를 반환받았다면?
// 에러를 잡아주자!
// 어떤 이유든 여기서 에러가 나면,
// 더이상 토큰을 리프레시할 수 있는 상황이 아님...
try {
// resp 요청을 보내면 dio로 post 요청을 보내서
// auth/token에 refreshToken을 사용해서
// 새로운 accessToken을 발급받을 수 있게 됨!
final resp = await dio.post(
'http://$ip/auth/token',
options: Options(
headers: {
'authorization': 'Bearer $refreshToken',
},
),
);
// 3)-7. 실제 accessToken 가져오기
// 그럴려면 데이터에서 accessToken이라는 값을 가져와야 함!
final accessToken = resp.data['accessToken'];
// 3)-11. 만약 에러가 나지 않았다면?
// requestOptions를 가져온다.
final options = err.requestOptions;
// 3)-12. accessToken 새로 넣어주기(토큰 변경하기)
// 그럼 토큰을 넣을 수 있음.
// 하지만 새로 토큰을 발급받았기 때문에
// final FlutterSecureStorage storage 안에도 업데이트가 필요!
options.headers.addAll(
{
'authorization': 'Bearer $accessToken',
},
);
// 3)-13. final FlutterSecureStorage storage 업데이트
await storage.write(key: ACCESS_TOKEN_KEY, value: accessToken);
// 3)-14.원래 보냈던 요청 다시 보내기(요청 재전송)
// 위에 생성해준 dio에 fetch 붙여주기
// 그러면 (requestOptions)이 자동완성 되는데,
// 이를 통해 실제 요청을 보낼 때 필요한 모든 값들은
// requestOptions 안에 들어있는 걸 알 수 있음!
// 3)-15. requestOptions에 그냥 options 넣기
// 그러면 실제 err를 발생시킨 모든 요청과 관련된 옵션들을 다 받아서
// 토큰만 바꾼 다음 다시 요청을 다시 보내는 것!
final response = await dio.fetch(options);
// 3)-16. final response = await dio.fetch(options); 이렇게
// 응답이 오면 onError가 불렸지만, 실제로 반환해야 하는 값은
// 응답(요청)이 잘 왔다고 await dio.fetch(options); 에서
// 받은 응답을 다시 되돌려줘야 함!
return handler.resolve(response); // handler.resolve => 요청이 잘 끝났다는 의미
} on DioException catch (e) {
// 3)-10. 그냥 catch에서 수정
// on DioException catch (e)로 바꿔주면 (아래 reject도 e를 넣어줌)
// 그냥 catch로 에러 전체를 잡아도 되지만,
// 예상되는 건 DioError니까 이렇게 따로 잡으면 좀 더 합리적임
// 3)-9. 더이상 토큰을 리프레시할 수 있는
// 상황이 아니라면 그냥 에러를 던져줌.
return handler.reject(e);
}
}
return super.onError(err, handler); // 반드시 호출
}
}
}
response는 어떤 response일까?
바로 final response = await dio.fetch(options); 새로 보낸 요청이다.
새로 보낸 요청에 대한 응답을 받아서
handler.resolve에 넣으면 에러가 난 이 상황에
똑같은 요청을 토큰만 바꿔서 새로 요청하고,
그 성공적인 요청에 대한 값을 반환할 수가 있다.
그러면 실제로는 에러가 났지만,
final response = await dio.fetch(options); 이렇게
새로 요청해서 응답을 받아온 다음 return handler.resolve(response);를
불렀기 때문에 UI(레스토랑 디테일 스크린)에서
현재 이 CustomInterceptor를 적용한 dio를 사용했을 때,
실제로 에러가 나지 않은 것처럼 착각을 줄 수 있음!