Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Response error code for actual request is really 401 #1

Open
annawidera opened this issue Jan 22, 2018 · 2 comments
Open

Response error code for actual request is really 401 #1

annawidera opened this issue Jan 22, 2018 · 2 comments

Comments

@annawidera
Copy link

Hey Alexandr!
I am trying to implement such behavior like yours: in case request received response "unauthorized", refresh access token and retry the actual request. I had troubles with implementing this and that's how I came across your solution.
I have two concerns which I would like to discuss.
First: I am wondering how do you check if error code for actual request is 401, not for instance 404 (bad path or something).
Second concern is about logout. Are you receiving 401 when attempt to refresh access token failed? Shouldn't it be 400 (according to OAuth2 docs)? Refresh token request is not under authorize protection and if it's failed it's 400 - Bad request with error code: invalid_grant.
https://tools.ietf.org/html/rfc6749#section-5.2

Thank you for any clarification on that topic!
Best!
Ania

@annawidera
Copy link
Author

annawidera commented Jan 22, 2018

For the first concern I came with something like this:

extension Response {
    public func filterUnauthorized() throws -> Response {
        if statusCode == 401 {
            throw MoyaError.statusCode(self)
        }
        return self
    }
}

extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Response {
    public func filterUnauthorized() -> Single<ElementType> {
        return flatMap { response -> Single<ElementType> in
            return Single.just(try response.filterUnauthorized())
        }
    }
}

And then use it like this:

provider.rx.request(token)
            .filterUnauthorized()
            .retryWithAuthIfNeeded()
            .filterSuccessfulStatusCodes()

@axmav
Copy link
Owner

axmav commented Jan 23, 2018

Hello @annawidera !
If u use filters like filterUnauthorized() the chain will be interrupted and filterSuccessfulStatusCodes() will not work.
You could use custom class (like wrap for network request):

class Network: Networking {
    
    let provider: MoyaProvider<MyApp>
    let localProvider: MoyaProvider<MyAppLocal>
    let xAppUser: XAppUser
    let xAppRoutes: XAppRoutes
    
    init(xAppUser: XAppUser, xAppRoutes: XAppRoutes) {
        print("INIT NETWORK")
        self.xAppUser = xAppUser
        self.xAppRoutes = xAppRoutes
        
        let endpointClosure = { (target: MyApp) -> Endpoint<MyApp> in
            
            var targetURL:URL!
            targetURL = target.baseURL.appendingPathComponent(xAppRoutes.route(forKey: target.path))
            
            let endpoint: Endpoint<MyApp> = Endpoint<MyApp>(url: targetURL.absoluteString, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: target.method, task: target.task, httpHeaderFields: target.headers)
            
            let urlRequest = try! endpoint.urlRequest()
            
            switch target {
            case .token(_, _), .refreshToken(_):
                let requestBody = urlRequest.httpBody
                let authenticationHeader = Network.createAuthenticationHeader(sending: requestBody, forPath: urlRequest.url!.pathWithQuery, method: target.method.rawValue)
                
                return endpoint.adding(newHTTPHeaderFields: ["Authorization": authenticationHeader])
            default:
                if let token = xAppUser.token {
                    return endpoint.adding(newHTTPHeaderFields: ["Authorization": "Bearer \(token)"])
                } else {
                    return endpoint
                }
            }
        }
        
        provider = MoyaProvider<MyApp>(endpointClosure: endpointClosure, plugins: (MyAppConstants.debug) ? [NetworkLoggerPlugin(verbose: true)] : [])
        localProvider = MoyaProvider<MyAppLocal>(stubClosure: MoyaProvider.immediatelyStub)
    }
    
    func request(target: MyApp) -> PrimitiveSequence<SingleTrait, Response> {
        print("REQUEST", target)
        return provider.rx
            .request(target)
            .filterSuccessfulStatusCodes()
            .retryWhen({ (error: Observable<MoyaError>) in
                return error.flatMap({ [unowned self] (error) -> Single<Void> in
                    if case MoyaError.statusCode(let response) = error  {
                        switch response.statusCode {
                        case 404:
                            return self.updateRouteInfo(jwtHeader: target.jwtHeader)
                        case 401:
                            guard let refreshToken = self.xAppUser.refreshToken else {
                                XCGLogger.default.error("Undefined refresh token")
                                return Single.error(error)
                            }
                            
                            return self.provider
                                .rx.request(.refreshToken(token: refreshToken))
                                .filterSuccessfulStatusCodes()
                                .mapObject(Access.self)
                                .catchError { error in
                                    if case MoyaError.statusCode(let response) = error  {
                                        if response.statusCode == 401 || response.statusCode == 400 {
                                            NotificationCenter.default.post(name: .userLogout, object: nil)
                                        }
                                    }
                                    return Single.error(error)
                                }
                                .flatMap({ access -> Single<Void> in
                                    self.xAppUser.define(access: access)
                                    return Single.just(())
                                })
                        default:
                            XCGLogger.default.error("Server fatal error")
                            return Single.error(error)
                        }
                    } else {
                        XCGLogger.default.error("Undefined response error")
                        return Single.error(error)
                    }
                })
            })
    }
    
    func request(local target: MyAppLocal) -> PrimitiveSequence<SingleTrait, Response> {
        print("REQUEST LOCAL", target)
        return localProvider.rx
            .request(target)
    }
    
}

Than use it in your code:

network
                    .request(target: .posts(group: group.id))
                    .filterSuccessfulStatusCodes()
                    .mapArray(Post.self)
                    .trackActivity(loading)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants