Skip to content

Instantly share code, notes, and snippets.

@NunciosChums
Last active June 8, 2023 04:25
Show Gist options
  • Save NunciosChums/841b2c3935b2028b2162842d479de143 to your computer and use it in GitHub Desktop.
Save NunciosChums/841b2c3935b2028b2162842d479de143 to your computer and use it in GitHub Desktop.
Moya + renewal token when Unauthorized(401)
import Foundation
import Moya
import RxSwift
/// 인증 관련 API
final class AuthService: BaseService<AuthAPI> {
static let shared = AuthService()
private override init() {}
/// 토큰 재발급
func renewalToken(refreshToken: String) -> Single<Response> {
return request(.renewalToken(refreshToken))
}
}
// MARK: - API
enum AuthAPI {
/// 토큰 재발급
case renewalToken(String)
}
extension AuthAPI: BaseAPI {
var path: String {
let apiPath = "/api-as/v1"
switch self {
case .renewalToken:
return "\(apiPath)/\("renewalToken".lowercased())"
}
}
var method: Moya.Method {
switch self {
case .renewalToken:
return .post
}
}
var task: Task {
switch self {
case let .renewalToken(refreshToken):
return .requestParameters(
parameters: ["refreshToken": refreshToken],
encoding: JSONEncoding.default
)
}
}
}
import Foundation
import Moya
protocol BaseAPI: TargetType {}
extension BaseAPI {
var baseURL: URL { URL(string: "https://api.github.com")! }
var method: Moya.Method { .get }
var sampleData: Data { Data() }
var task: Task { .requestPlain }
var headers: [String: String]? { nil }
}
import Moya
import RxMoya
import RxSwift
/// 네트워크 호출 상속용
/// https://github.com/Moya/Moya
class BaseService<API: TargetType> {
// moya에서 지원하는 로깅 플러그인
// private let provider = MoyaProvider<API>(plugins: [NetworkLoggerPlugin()])
// 커스텀 로깅 플러그인
private let provider = MoyaProvider<API>(plugins: [RequestLoggingPlugin()])
/// 네트워크 호출
/// help from https://github.com/Moya/Moya/issues/1177#issuecomment-345132374
func request(_ api: API) -> Single<Response> {
return provider.rx.request(api)
.flatMap {
// 401(Unauthorized) 발생 시 자동으로 토큰을 재발급 받는다
if $0.statusCode == 401 {
throw TokenError.tokenExpired
} else {
return Single.just($0)
}
}
.retryWhen { (error: Observable<TokenError>) in
error.flatMap { error -> Single<Response> in
AuthService.shared.renewalToken() // 토큰 재발급 받기
}
}
.handleResponse()
.filterSuccessfulStatusCodes()
.retry(2)
}
}
import Foundation
import Moya
import RxSwift
/// 서버에서 보내주는 오류 문구 파싱용
extension PrimitiveSequence where Trait == SingleTrait, Element == Response {
func handleResponse() -> Single<Element> {
return flatMap { response in
// 토큰 재발급 받았을 때 토큰 변경함
if let newToken = try? response.map(Token.self) {
UserDefaults.accessToken = newToken.accessToken
UserDefaults.refreshToken = newToken.refreshToken
}
if (200 ... 299) ~= response.statusCode {
return Single.just(response)
}
if var error = try? response.map(ResponseError.self) {
error.statusCode = response.statusCode
return Single.error(error)
}
// Its an error and can't decode error details from server, push generic message
let genericError = ResponseError(statusCode: response.statusCode
serverName: "unknown Server Name",
error: "unknown error",
message: "empty message")
return Single.error(genericError)
}
}
}
/// 토큰 만료 에러
enum TokenError: Swift.Error {
case tokenExpired
}
TestService.shared.user(name: "Moya")
.map(TestModel.self)
.subscribe(
onSuccess: {
print("=== user: \($0)")
},
onError: {
print("==== error: \($0)")
}
).disposed(by: disposeBag)
import Foundation
import Moya
/// 네트워크 호출 결과 로그 표시
final class RequestLoggingPlugin: PluginType {
/// API를 보내기 직전에 호출
func willSend(_ request: RequestType, target: TargetType) {
guard let httpRequest = request.request else {
print("--> invalid request")
return
}
let url = httpRequest.description
let method = httpRequest.httpMethod ?? "unknown method"
var log = "--> \(method) \(url)\n"
log.append("API: \(target)\n")
if let headers = httpRequest.allHTTPHeaderFields, !headers.isEmpty {
log.append("header: \(headers)\n")
}
if let body = httpRequest.httpBody, let bodyString = String(bytes: body, encoding: String.Encoding.utf8) {
log.append("\(bodyString)\n")
}
log.append("--> END \(method)")
print(log)
}
/// API Response
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case let .success(response):
onSuceed(response, target: target, isFromError: false)
case let .failure(error):
onFail(error, target: target)
}
}
func onSuceed(_ response: Response, target: TargetType, isFromError: Bool) {
let request = response.request
let url = request?.url?.absoluteString ?? "nil"
let statusCode = response.statusCode
var log = "<-- \(statusCode) \(url)\n"
log.append("API: \(target)\n")
response.response?.allHeaderFields.forEach {
log.append("\($0): \($1)\n")
}
if let reString = String(bytes: response.data, encoding: String.Encoding.utf8) {
log.append("\(reString)\n")
}
log.append("<-- END HTTP (\(response.data.count)-byte body)")
print(log)
}
func onFail(_ error: MoyaError, target: TargetType) {
if let response = error.response {
onSuceed(response, target: target, isFromError: true)
return
}
var log = "<-- \(error.errorCode) \(target)\n"
log.append("\(error.failureReason ?? error.errorDescription ?? "unknown error")\n")
log.append("<-- END HTTP")
print(log)
}
}
struct ResponseError: Decodable, Error {
var statusCode: Int?
let message: String
let documentation_url: String
}
import Foundation
struct TestModel: Decodable {
let id: Int
let name: String
let login: String
}
import Foundation
import Moya
import RxSwift
final class TestService: BaseService<TestAPI> {
static let shared = TestService()
private override init() {}
/// 아이디로 사용자 정보 가져오기
/// - Parameters:
/// - name: 로그인 아이디
func user(name: String) -> Single<Response> {
return request(.profile(name))
}
}
enum TestAPI {
case profile(String)
}
extension TestAPI: BaseAPI {
var path: String {
switch self {
case let .profile(name):
return "/users/\(name)"
}
}
}
struct Token: Decodable {
let tokenType: String
let accessToken: String
let refreshToken: String
let expiresAt: TimeInterval
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment