Обработка статуса 401 с RxSwift & URLSession - PullRequest
1 голос
/ 26 сентября 2019

У меня сейчас есть сетевой клиент, который выглядит следующим образом:

class Client<R: ResourceType> {

    let engine: ClientEngineType
    var session: URLSession

    init(engine: ClientEngineType = ClientEngine()) {
        self.engine = engine
        self.session = URLSession.shared
    }

    func request<T: Codable>(_ resource: R) -> Single<T> {
        let request = URLRequest(resource: resource)

        return Single<T>.create { [weak self] single in
            guard let self = self else { return Disposables.create() }
            let response = self.session.rx.response(request: request)

            return response.subscribe(
                onNext: { response, data in

                    if let error = self.error(from: response) {
                        single(.error(error))
                        return
                    }

                    do {
                        let decoder = JSONDecoder()
                        let value = try decoder.decode(T.self, from: data)
                        single(.success(value))
                    } catch let error {
                        single(.error(error))
                    }
            },
                onError: { error in
                    single(.error(error))
            })
        }
    }

    struct StatusCodeError: LocalizedError {
        let code: Int

        var errorDescription: String? {
            return "An error occurred communicating with the server. Please try again."
        }
    }

    private func error(from response: URLResponse?) -> Error? {
        guard let response = response as? HTTPURLResponse else { return nil }

        let statusCode = response.statusCode

        if 200..<300 ~= statusCode {
            return nil
        } else {
            return StatusCodeError(code: statusCode)
        }
    }
}

, который я могу затем вызвать как-то вроде

let client = Client<MyRoutes>()
client.request(.companyProps(params: ["collections": "settings"]))
    .map { props -> CompanyModel in return props }
    .subscribe(onSuccess: { props in
      // do something with props
    }) { error in
        print(error.localizedDescription)
}.disposed(by: disposeBag)

Я бы хотел начать обработку 401ответы и обновляя мой токен и повторяя запрос.

Я изо всех сил пытаюсь найти хороший способ сделать это.чтобы добиться этого, однако я изо всех сил пытаюсь реализовать это в моем текущем клиенте.

Буду очень признателен за любые советы или указатели.

1 Ответ

1 голос
/ 27 сентября 2019

Это моя суть!(Спасибо, что назвали это отлично.) Вы видели статью, которая сопровождала это?https://medium.com/@danielt1263/retrying-a-network-request-despite-having-an-invalid-token-b8b89340d29

В обработке 401 повторных попыток есть два ключевых элемента.Во-первых, вам нужен способ вставить токены в ваши запросы и запустить конвейер запросов с Observable.deferred { tokenAcquisitionService.token.take(1) }.В вашем случае это означает, что вам нужен URLRequest.init, который будет принимать Ресурс и токен, а не только ресурс.

Во-вторых, выдается ошибка TokenAcquisitionError.unauthorized, когда вы получаете 401 и заканчиваетеконвейер запроса с .retryWhen { $0.renewToken(with: tokenAcquisitionService) }

Итак, учитывая то, что у вас есть выше, для обработки повторных попыток токена все, что вам нужно сделать, это включить мой TokenAcquisitionService в ваш проект и использовать это:

func getToken(_ oldToken: Token) -> Observable<(response: HTTPURLResponse, data: Data)> {
    fatalError("this function needs to be able to request a new token from the server. It has access to the old token if it needs that to request the new one.")
}

func extractToken(_ data: Data) -> Token {
    fatalError("This function needs to be able to extract the new token using the data returned from the previous function.")
}

let tokenAcquisitionService = TokenAcquisitionService<Token>(initialToken: Token(), getToken: getToken, extractToken: extractToken)

final class Client<R> where R: ResourceType {
    let session: URLSession

    init(session: URLSession = URLSession.shared) {
        self.session = session
    }

    func request<T>(_ resource: R) -> Single<T> where T: Decodable {
        return Observable.deferred { tokenAcquisitionService.token.take(1) }
            .map { token in URLRequest(resource: resource, token: token) }
            .flatMapLatest { [session] request in session.rx.response(request: request) }
            .do(onNext: { response, _ in
                if response.statusCode == 401 {
                    throw TokenAcquisitionError.unauthorized
                }
            })
            .map { (_, data) -> T in
                return try JSONDecoder().decode(T.self, from: data)
        }
        .retryWhen { $0.renewToken(with: tokenAcquisitionService) }
        .asSingle()
    }
}

Обратите внимание, это может быть случай, когда функция getToken должна, например, представить контроллер представления, который запрашивает учетные данные пользователя.Это означает, что вам нужно представить свой контроллер представления входа в систему (или UIAlertController) для сбора данных.Или, возможно, при входе в систему вы получаете как токен авторизации, так и токен обновления с вашего сервера.В этом случае TokenAcquisitionService должен удерживать их обоих (т. Е. Его T должен быть (token: String, refresh: String). Любой из них в порядке.

Единственная проблема со службой состоит в том, что если получение нового токена завершается неудачно, вся служба закрывается. Я еще не исправил это.

...