diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-05-13 16:39:02 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-05-17 15:23:43 +0200 |
| commit | 19b2664fd3f1f09eecae667230a29f1756e36481 (patch) | |
| tree | c85bfff7e0d97df5c322a070d37b0522bfa2f7e7 | |
| parent | afd8189b33517e327bd302dbcd20fa852cdb6da4 (diff) | |
| download | mullvadvpn-19b2664fd3f1f09eecae667230a29f1756e36481.tar.xz mullvadvpn-19b2664fd3f1f09eecae667230a29f1756e36481.zip | |
REST: drop response decoder and introduce response handler result
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/DisplayChainedError.swift | 46 | ||||
| -rw-r--r-- | ios/MullvadVPN/REST/RESTAPIProxy.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/REST/RESTAccountsProxy.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/REST/RESTAuthenticationProxy.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/REST/RESTDevicesProxy.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/REST/RESTError.swift | 136 | ||||
| -rw-r--r-- | ios/MullvadVPN/REST/RESTNetworkOperation.swift | 43 | ||||
| -rw-r--r-- | ios/MullvadVPN/REST/RESTProxy.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/REST/RESTResponseDecoder.swift | 46 | ||||
| -rw-r--r-- | ios/MullvadVPN/REST/RESTResponseHandler.swift | 35 |
11 files changed, 129 insertions, 201 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 21cc7f746b..4fdfac7208 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -104,7 +104,6 @@ 5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; }; 5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; }; 58554F73280AFA5A00013055 /* RESTAuthenticationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F72280AFA5A00013055 /* RESTAuthenticationProxy.swift */; }; - 58554F75280AFAE900013055 /* RESTResponseDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F74280AFAE900013055 /* RESTResponseDecoder.swift */; }; 58554F77280AFD5C00013055 /* RESTTaskIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */; }; 58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F78280B037400013055 /* RESTAccessTokenManager.swift */; }; 58554F7B280B125F00013055 /* RESTAccountsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F7A280B125F00013055 /* RESTAccountsProxy.swift */; }; @@ -424,7 +423,6 @@ 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Markdown.swift"; sourceTree = "<group>"; }; 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IPAddressRange+Codable.swift"; sourceTree = "<group>"; }; 58554F72280AFA5A00013055 /* RESTAuthenticationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAuthenticationProxy.swift; sourceTree = "<group>"; }; - 58554F74280AFAE900013055 /* RESTResponseDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTResponseDecoder.swift; sourceTree = "<group>"; }; 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTaskIdentifier.swift; sourceTree = "<group>"; }; 58554F78280B037400013055 /* RESTAccessTokenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAccessTokenManager.swift; sourceTree = "<group>"; }; 58554F7A280B125F00013055 /* RESTAccountsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAccountsProxy.swift; sourceTree = "<group>"; }; @@ -779,7 +777,6 @@ 58F97A1A280EEBC00050C2FC /* RESTProxyFactory.swift */, 58B5A894280AACC4009FDE99 /* RESTRequestFactory.swift */, 58F97A1D280FDE230050C2FC /* RESTRequestHandler.swift */, - 58554F74280AFAE900013055 /* RESTResponseDecoder.swift */, 588BCF272816D664009ADCEC /* RESTResponseHandler.swift */, 58095C582762155700890776 /* RESTRetryStrategy.swift */, 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */, @@ -1496,7 +1493,6 @@ 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */, 58B43C1925F77DB60002C8C3 /* ConnectMainContentView.swift in Sources */, 58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, - 58554F75280AFAE900013055 /* RESTResponseDecoder.swift in Sources */, 58F97A1E280FDE230050C2FC /* RESTRequestHandler.swift in Sources */, 58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */, 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */, diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift index 5e3d154f28..d1474f4864 100644 --- a/ios/MullvadVPN/DisplayChainedError.swift +++ b/ios/MullvadVPN/DisplayChainedError.swift @@ -22,44 +22,34 @@ extension REST.Error: DisplayChainedError { "NETWORK_ERROR", tableName: "REST", value: "Network error: %@", - comment: "Network error. Use %@ placeholder to place localized failure description." + comment: "" ), urlError.localizedDescription ) - case .server(let serverError): - if let knownErrorDescription = serverError.errorDescription { - return knownErrorDescription - } else { - return String( - format: NSLocalizedString( - "SERVER_ERROR", - tableName: "REST", - value: "Server error: %@", - comment: "Server error. Use %@ placeholder to place localized failure description." - ), - serverError.error ?? "(empty)" - ) - } - case .encodePayload: + case .unhandledResponse(let statusCode, let serverResponse): + return String( + format: NSLocalizedString( + "SERVER_ERROR", + tableName: "REST", + value: "Unexpected server response: %1$@ (HTTP status: %2$d)", + comment: "" + ), + serverResponse?.code.rawValue ?? "(no code)", + statusCode + ) + case .createURLRequest: return NSLocalizedString( "SERVER_REQUEST_ENCODING_ERROR", tableName: "REST", - value: "Server request encoding error", - comment: "Failure to encode the server request." + value: "Failure to create URL request", + comment: "" ) - case .decodeSuccessResponse: + case .decodeResponse: return NSLocalizedString( "SERVER_SUCCESS_RESPONSE_DECODING_ERROR", tableName: "REST", - value: "Server success response decoding error", - comment: "Failure to decode the server success response." - ) - case .decodeErrorResponse: - return NSLocalizedString( - "SERVER_FAILURE_RESPONSE_DECODING_ERROR", - tableName: "REST", - value: "Server error response decoding error", - comment: "Failure to decode the server failure response." + value: "Server response decoding error", + comment: "" ) } } diff --git a/ios/MullvadVPN/REST/RESTAPIProxy.swift b/ios/MullvadVPN/REST/RESTAPIProxy.swift index 15cc807c16..6e94e6a883 100644 --- a/ios/MullvadVPN/REST/RESTAPIProxy.swift +++ b/ios/MullvadVPN/REST/RESTAPIProxy.swift @@ -21,9 +21,7 @@ extension REST { pathPrefix: "/app/v1", bodyEncoder: Coding.makeJSONEncoder() ), - responseDecoder: ResponseDecoder( - decoder: Coding.makeJSONDecoder() - ) + responseDecoder: Coding.makeJSONDecoder() ) } diff --git a/ios/MullvadVPN/REST/RESTAccountsProxy.swift b/ios/MullvadVPN/REST/RESTAccountsProxy.swift index 2862b586d6..997c3b0502 100644 --- a/ios/MullvadVPN/REST/RESTAccountsProxy.swift +++ b/ios/MullvadVPN/REST/RESTAccountsProxy.swift @@ -18,9 +18,7 @@ extension REST { pathPrefix: "/accounts/v1", bodyEncoder: Coding.makeJSONEncoder() ), - responseDecoder: ResponseDecoder( - decoder: Coding.makeJSONDecoder() - ) + responseDecoder: Coding.makeJSONDecoder() ) } diff --git a/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift b/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift index a5fb192d99..f457e28858 100644 --- a/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift +++ b/ios/MullvadVPN/REST/RESTAuthenticationProxy.swift @@ -18,9 +18,7 @@ extension REST { pathPrefix: "/auth/v1", bodyEncoder: Coding.makeJSONEncoder() ), - responseDecoder: ResponseDecoder( - decoder: Coding.makeJSONDecoder() - ) + responseDecoder: Coding.makeJSONDecoder() ) } diff --git a/ios/MullvadVPN/REST/RESTDevicesProxy.swift b/ios/MullvadVPN/REST/RESTDevicesProxy.swift index d7e79b2236..d246ba8dd6 100644 --- a/ios/MullvadVPN/REST/RESTDevicesProxy.swift +++ b/ios/MullvadVPN/REST/RESTDevicesProxy.swift @@ -20,9 +20,7 @@ extension REST { pathPrefix: "/accounts/v1", bodyEncoder: Coding.makeJSONEncoder() ), - responseDecoder: ResponseDecoder( - decoder: Coding.makeJSONDecoder() - ) + responseDecoder: Coding.makeJSONDecoder() ) } diff --git a/ios/MullvadVPN/REST/RESTError.swift b/ios/MullvadVPN/REST/RESTError.swift index 6ee7e74d26..3b6f24c49a 100644 --- a/ios/MullvadVPN/REST/RESTError.swift +++ b/ios/MullvadVPN/REST/RESTError.swift @@ -12,117 +12,71 @@ extension REST { /// An error type returned by REST API classes. enum Error: ChainedError { - /// A failure to encode the payload - case encodePayload(Swift.Error) + /// A failure to create URL request. + case createURLRequest(Swift.Error) - /// A failure during networking + /// A failure during networking. case network(URLError) - /// A failure reported by server - case server(REST.ServerErrorResponse) + /// A failure to handle response. + case unhandledResponse(_ statusCode: Int, _ serverResponse: ServerErrorResponse?) - /// A failure to decode the error response from server - case decodeErrorResponse(Swift.Error) - - /// A failure to decode the success response from server - case decodeSuccessResponse(Swift.Error) + /// A failure to decode server response. + case decodeResponse(Swift.Error) var errorDescription: String? { switch self { - case .encodePayload: - return "Failure to encode the payload." + case .createURLRequest: + return "Failure to create URL request." case .network: return "Network error." - case .server: - return "Server error." - case .decodeErrorResponse: - return "Failure to decode error response from server." - case .decodeSuccessResponse: - return "Failure to decode success response from server." - } - } - } + case .unhandledResponse(let statusCode, let serverResponse): + var str = "Failure to handle server response: HTTP/\(statusCode)." - /// A struct that represents a server response in case of error (any HTTP status code except 2xx). - struct ServerErrorResponse: LocalizedError, Decodable, Equatable { - /// A list of known server error codes - enum Code: String, Equatable { - case invalidAccount = "INVALID_ACCOUNT" - case keyLimitReached = "KEY_LIMIT_REACHED" - case pubKeyNotFound = "PUBKEY_NOT_FOUND" - case invalidAccessToken = "INVALID_ACCESS_TOKEN" + if let code = serverResponse?.code { + str += " Error code: \(code)." + } - static func ~= (pattern: Self, value: REST.ServerErrorResponse) -> Bool { - return pattern.rawValue == value.code + if let detail = serverResponse?.detail { + str += " Detail: \(detail)." + } + + return str + case .decodeResponse: + return "Failure to decode URL response data." } } + } - static var invalidAccount: Code { - return .invalidAccount - } - static var keyLimitReached: Code { - return .keyLimitReached - } - static var pubKeyNotFound: Code { - return .pubKeyNotFound - } - static var invalidAccessToken: Code { - return .invalidAccessToken - } + struct ServerErrorResponse: Decodable { + let code: ServerResponseCode + let detail: String? - let code: String - let error: String? + private enum CodingKeys: String, CodingKey { + case code, detail, error + } - var errorDescription: String? { - switch code { - case Code.keyLimitReached.rawValue: - return NSLocalizedString( - "KEY_LIMIT_REACHED_ERROR_DESCRIPTION", - tableName: "REST", - value: "Too many WireGuard keys in use.", - comment: "" - ) - case Code.invalidAccount.rawValue: - return NSLocalizedString( - "INVALID_ACCOUNT_ERROR_DESCRIPTION", - tableName: "REST", - value: "Invalid account.", - comment: "" - ) + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let rawValue = try container.decode(String.self, forKey: .code) - case Code.invalidAccessToken.rawValue: - return NSLocalizedString( - "INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION", - tableName: "REST", - value: "Invalid access token.", - comment: "") - default: - let localizedString = NSLocalizedString( - "UNKNOWN_ERROR_DESCRIPTION", - tableName: "REST", - value: "Unknown error: %@", - comment: "Use %@ placeholder to place the error code into the localized string." - ) - return String(format: localizedString, code) - } + code = ServerResponseCode(rawValue: rawValue) + detail = try container.decodeIfPresent(String.self, forKey: .detail) + ?? container.decodeIfPresent(String.self, forKey: .error) } + } - var recoverySuggestion: String? { - switch code { - case Code.keyLimitReached.rawValue: - return NSLocalizedString( - "KEY_LIMIT_REACHED_ERROR_RECOVERY_SUGGESTION", - tableName: "REST", - value: "Please visit the website to revoke a key before login is possible.", - comment: "" - ) - default: - return nil - } - } + struct ServerResponseCode: RawRepresentable, Equatable { + static let invalidAccount = ServerResponseCode(rawValue: "INVALID_ACCOUNT") + static let keyLimitReached = ServerResponseCode(rawValue: "KEY_LIMIT_REACHED") + static let publicKeyNotFound = ServerResponseCode(rawValue: "PUBKEY_NOT_FOUND") + static let publicKeyInUse = ServerResponseCode(rawValue: "PUBKEY_IN_USE") + static let maxDevicesReached = ServerResponseCode(rawValue: "MAX_DEVICES_REACHED") + static let invalidAccessToken = ServerResponseCode(rawValue: "INVALID_ACCESS_TOKEN") - static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.code == rhs.code + let rawValue: String + init(rawValue: String) { + self.rawValue = rawValue } } diff --git a/ios/MullvadVPN/REST/RESTNetworkOperation.swift b/ios/MullvadVPN/REST/RESTNetworkOperation.swift index 97e7c0e9a6..ca484f7b40 100644 --- a/ios/MullvadVPN/REST/RESTNetworkOperation.swift +++ b/ios/MullvadVPN/REST/RESTNetworkOperation.swift @@ -248,19 +248,38 @@ extension REST { private func didReceiveURLResponse(_ response: HTTPURLResponse, data: Data, endpoint: AnyIPEndpoint) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) - let result = responseHandler.handleURLResponse(response, data: data) - if case .server(.invalidAccessToken) = result.error, - requiresAuthorization, retryInvalidAccessTokenError - { - logger.debug( - "Received invalid access token error. Retry once.", - metadata: loggerMetadata - ) - retryInvalidAccessTokenError = false - startRequest() - } else { - finish(completion: OperationCompletion(result: result)) + let handlerResult = responseHandler.handleURLResponse(response, data: data) + + switch handlerResult { + case .success(let output): + // Response handler produced value. + finish(completion: .success(output)) + + case .decoding(let decoderBlock): + // Response handler returned a block decoding value. + let decodeResult = Result { try decoderBlock() } + .mapError { error -> REST.Error in + return .decodeResponse(error) + } + finish(completion: OperationCompletion(result: decodeResult)) + + case .unhandledResponse(let serverErrorResponse): + // Response handler couldn't handle the response. + if serverErrorResponse?.code == .invalidAccessToken, + requiresAuthorization, + retryInvalidAccessTokenError + { + logger.debug("Received invalid access token error. Retry once.") + retryInvalidAccessTokenError = false + startRequest() + } else { + finish( + completion: .failure( + .unhandledResponse(response.statusCode, serverErrorResponse) + ) + ) + } } } } diff --git a/ios/MullvadVPN/REST/RESTProxy.swift b/ios/MullvadVPN/REST/RESTProxy.swift index 611f4d9c34..f166338976 100644 --- a/ios/MullvadVPN/REST/RESTProxy.swift +++ b/ios/MullvadVPN/REST/RESTProxy.swift @@ -25,13 +25,13 @@ extension REST { let requestFactory: REST.RequestFactory /// URL response decoder. - let responseDecoder: REST.ResponseDecoder + let responseDecoder: JSONDecoder init( name: String, configuration: ConfigurationType, requestFactory: REST.RequestFactory, - responseDecoder: REST.ResponseDecoder + responseDecoder: JSONDecoder ) { dispatchQueue = DispatchQueue(label: "REST.\(name).dispatchQueue") diff --git a/ios/MullvadVPN/REST/RESTResponseDecoder.swift b/ios/MullvadVPN/REST/RESTResponseDecoder.swift deleted file mode 100644 index 2e79bbc9e3..0000000000 --- a/ios/MullvadVPN/REST/RESTResponseDecoder.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// RESTResponseDecoder.swift -// MullvadVPN -// -// Created by pronebird on 16/04/2022. -// Copyright © 2022 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -extension REST { - struct ResponseDecoder { - let decoder: JSONDecoder - - init(decoder: JSONDecoder) { - self.decoder = decoder - } - - // Parse JSON response into the given `Decodable` type. - func decodeSuccessResponse<T: Decodable>(_ type: T.Type, from data: Data) -> Result<T, REST.Error> { - return Result { try decoder.decode(type, from: data) } - .mapError { error in - return .decodeSuccessResponse(error) - } - } - - /// Parse server error response from JSON. - func decodeErrorResponse(from data: Data) -> Result<REST.ServerErrorResponse, REST.Error> { - return Result { () -> REST.ServerErrorResponse in - return try decoder.decode(REST.ServerErrorResponse.self, from: data) - } - .mapError { error in - return .decodeErrorResponse(error) - } - } - - /// Parse server error response from JSON and map it to `RESTError.server` error kind. - func decodeErrorResponseAndMapToServerError<T>(from data: Data) -> Result<T, REST.Error> { - return decodeErrorResponse(from: data) - .flatMap { serverError in - return .failure(.server(serverError)) - } - } - } - -} diff --git a/ios/MullvadVPN/REST/RESTResponseHandler.swift b/ios/MullvadVPN/REST/RESTResponseHandler.swift index 65ae4b6e2d..1ec56541fa 100644 --- a/ios/MullvadVPN/REST/RESTResponseHandler.swift +++ b/ios/MullvadVPN/REST/RESTResponseHandler.swift @@ -11,12 +11,25 @@ import Foundation protocol RESTResponseHandler { associatedtype Success - func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> Result<Success, REST.Error> + func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> REST.ResponseHandlerResult<Success> } extension REST { + /// Responser handler result type. + enum ResponseHandlerResult<Success> { + /// Response handler succeeded and produced a value. + case success(Success) + + /// Response handler succeeded and returned a block that decodes the value. + case decoding(_ decoderBlock: () throws -> Success) + + /// Response handler received the response that it cannot handle. + /// Server error response is attached when available. + case unhandledResponse(ServerErrorResponse?) + } + final class AnyResponseHandler<Success>: RESTResponseHandler { - typealias HandlerBlock = (HTTPURLResponse, Data) -> Result<Success, REST.Error> + typealias HandlerBlock = (HTTPURLResponse, Data) -> REST.ResponseHandlerResult<Success> private let handlerBlock: HandlerBlock @@ -24,7 +37,7 @@ extension REST { handlerBlock = block } - func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> Result<Success, REST.Error> { + func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> REST.ResponseHandlerResult<Success> { return handlerBlock(response, data) } } @@ -32,12 +45,22 @@ extension REST { /// Returns default response handler that parses JSON response into the /// given `Decodable` type when it encounters HTTP `2xx` code, otherwise /// attempts to decode the server error. - static func defaultResponseHandler<T: Decodable>(decoding type: T.Type, with decoder: REST.ResponseDecoder) -> AnyResponseHandler<T> { + static func defaultResponseHandler<T: Decodable>( + decoding type: T.Type, + with decoder: JSONDecoder + ) -> AnyResponseHandler<T> { return AnyResponseHandler { response, data in if HTTPStatus.isSuccess(response.statusCode) { - return decoder.decodeSuccessResponse(type, from: data) + return .decoding { + try decoder.decode(type, from: data) + } } else { - return decoder.decodeErrorResponseAndMapToServerError(from: data) + return .unhandledResponse( + try? decoder.decode( + ServerErrorResponse.self, + from: data + ) + ) } } } |
