summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-05-13 16:39:02 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-05-17 15:23:43 +0200
commit19b2664fd3f1f09eecae667230a29f1756e36481 (patch)
treec85bfff7e0d97df5c322a070d37b0522bfa2f7e7
parentafd8189b33517e327bd302dbcd20fa852cdb6da4 (diff)
downloadmullvadvpn-19b2664fd3f1f09eecae667230a29f1756e36481.tar.xz
mullvadvpn-19b2664fd3f1f09eecae667230a29f1756e36481.zip
REST: drop response decoder and introduce response handler result
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift46
-rw-r--r--ios/MullvadVPN/REST/RESTAPIProxy.swift4
-rw-r--r--ios/MullvadVPN/REST/RESTAccountsProxy.swift4
-rw-r--r--ios/MullvadVPN/REST/RESTAuthenticationProxy.swift4
-rw-r--r--ios/MullvadVPN/REST/RESTDevicesProxy.swift4
-rw-r--r--ios/MullvadVPN/REST/RESTError.swift136
-rw-r--r--ios/MullvadVPN/REST/RESTNetworkOperation.swift43
-rw-r--r--ios/MullvadVPN/REST/RESTProxy.swift4
-rw-r--r--ios/MullvadVPN/REST/RESTResponseDecoder.swift46
-rw-r--r--ios/MullvadVPN/REST/RESTResponseHandler.swift35
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
+ )
+ )
}
}
}