summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-05-07 15:21:44 +0200
committerAndrej Mihajlov <and@mullvad.net>2021-05-12 10:04:08 +0200
commitaaf5e6915bdfd1e42ea2f4c70eac988beae34572 (patch)
tree4b66cc44c93856922c5ea02708db4a04ef7fd843
parent3e2062338a6e934085e1fca54ab6609e37b79d9a (diff)
downloadmullvadvpn-aaf5e6915bdfd1e42ea2f4c70eac988beae34572.tar.xz
mullvadvpn-aaf5e6915bdfd1e42ea2f4c70eac988beae34572.zip
Rework response handling and add support for HTTP caching via ETag header
-rw-r--r--ios/MullvadVPN/MullvadRest.swift322
-rw-r--r--ios/MullvadVPN/RelayCache.swift53
2 files changed, 284 insertions, 91 deletions
diff --git a/ios/MullvadVPN/MullvadRest.swift b/ios/MullvadVPN/MullvadRest.swift
index 1afe08e923..cf01e1bc37 100644
--- a/ios/MullvadVPN/MullvadRest.swift
+++ b/ios/MullvadVPN/MullvadRest.swift
@@ -23,8 +23,24 @@ enum HttpMethod: String {
case delete = "DELETE"
}
+// HTTP status codes
+enum HttpStatus {
+ static let ok = 200
+ static let created = 201
+ static let noContent = 204
+ static let notModified = 304
+}
+
+/// HTTP headers
+enum HttpHeader {
+ static let authorization = "Authorization"
+ static let contentType = "Content-Type"
+ static let etag = "ETag"
+ static let ifNoneMatch = "If-None-Match"
+}
+
/// A struct that represents a server response in case of error (any HTTP status code except 2xx).
-struct ServerErrorResponse: LocalizedError, Decodable, RestResponse, Equatable {
+struct ServerErrorResponse: LocalizedError, Decodable, Equatable {
/// A list of known server error codes
enum Code: String, Equatable {
case invalidAccount = "INVALID_ACCOUNT"
@@ -113,29 +129,6 @@ protocol RestPayload {
func inject(into request: inout URLRequest) throws
}
-/// Types conforming to this protocol can act as REST response types.
-protocol RestResponse {
- associatedtype Output
-
- static func decodeResponse(_ data: Data) throws -> Output
-}
-
-/// Any `Decodable` can be REST response
-extension Decodable where Self: RestResponse {
- static func decodeResponse(_ data: Data) throws -> Self {
- try MullvadRest.makeJSONDecoder().decode(Self.self, from: data)
- }
-}
-
-/// An empty REST response type that cannot be instantiated and is only used to produce an empty
-/// output.
-enum EmptyResponse {}
-extension EmptyResponse: RestResponse {
- static func decodeResponse(_ data: Data) throws -> () {
- return ()
- }
-}
-
/// Any `Encodable` type can be injected as JSON payload
extension RestPayload where Self: Encodable {
func inject(into request: inout URLRequest) throws {
@@ -146,9 +139,9 @@ extension RestPayload where Self: Encodable {
// MARK: - Operations
final class RestOperation<Input, Response>: AsyncOperation, InputOperation, OutputOperation
- where Input: RestPayload, Response: RestResponse
+ where Input: RestPayload
{
- typealias Output = Result<Response.Output, RestError>
+ typealias Output = Result<Response, RestError>
private let endpoint: RestEndpoint<Input, Response>
private let session: URLSession
@@ -187,24 +180,140 @@ final class RestOperation<Input, Response>: AsyncOperation, InputOperation, Outp
}
}
+// MARK: - Response handlers
+
+/// Types conforming to this protocol can be used as response handlers to decode the raw server response to the data
+/// type expected by the caller.
+protocol ResponseHandler {
+ associatedtype Response
+
+ /// Decode the response.
+ /// The implementation is expected to throw `BadResponseError` in case of failure to handle the HTTP response,
+ /// or any other `Error` in case of failure to decode the data.
+ func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<Response, ResponseHandlerError>
+}
+
+enum ResponseHandlerError: Error {
+ /// A failure to handle the response due to unexpected status code
+ case badResponse(_ statusCode: Int)
+
+ /// A failure to decode data
+ case decodeData(Error)
+}
+
+/// A placeholder error used to indicate that the server returned unexpected response.
+fileprivate struct BadResponseError: Error {
+ let statusCode: Int
+}
+
+/// Type-erasing response handler.
+struct AnyResponseHandler<Response>: ResponseHandler {
+ private let decodeResponseBlock: (HTTPURLResponse, Data) -> Result<Response, ResponseHandlerError>
+
+ init<T: ResponseHandler>(_ wrappedHandler: T) where T.Response == Response {
+ self.decodeResponseBlock = { (response, data) -> Result<Response, ResponseHandlerError> in
+ return wrappedHandler.decodeResponse(response, data: data)
+ }
+ }
+
+ func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<Response, ResponseHandlerError> {
+ return self.decodeResponseBlock(httpResponse, data)
+ }
+}
+
+/// A REST response handler that decides when response contains the successful result based on the given status code and
+/// decodes the value in response.
+struct DecodingResponseHandler<Response: Decodable>: ResponseHandler {
+ private let expectedStatus: Int
+
+ init(expectedStatus: Int) {
+ self.expectedStatus = expectedStatus
+ }
+
+ func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<Response, ResponseHandlerError> {
+ if httpResponse.statusCode == expectedStatus {
+ return Result { try MullvadRest.makeJSONDecoder().decode(Response.self, from: data) }
+ .mapError { (error) -> ResponseHandlerError in
+ return .decodeData(error)
+ }
+ } else {
+ return .failure(.badResponse(httpResponse.statusCode))
+ }
+ }
+}
+
+/// A REST response handler that decides when response contains the successful result based on the given status code but
+/// never decodes the value in response as it anticipates it to be empty.
+struct EmptyResponseHandler: ResponseHandler {
+ private let expectedStatus: Int
+
+ init(expectedStatus: Int) {
+ self.expectedStatus = expectedStatus
+ }
+
+ func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<(), ResponseHandlerError> {
+ if httpResponse.statusCode == expectedStatus {
+ return .success(())
+ } else {
+ return .failure(.badResponse(httpResponse.statusCode))
+ }
+ }
+}
+
+/// A REST response handler that takes into account ETag and 200 and 304 response codes to produce the output result.
+struct HttpCacheDecodingResponseHandler<WrappedType: Decodable>: ResponseHandler {
+ typealias Response = HttpResourceCacheResponse<WrappedType>
+
+ private let etag: String?
+
+ init(etag: String?) {
+ self.etag = etag
+ }
+
+ func decodeResponse(_ httpResponse: HTTPURLResponse, data: Data) -> Result<Response, ResponseHandlerError> {
+ switch httpResponse.statusCode {
+ case HttpStatus.ok:
+ return Result { try MullvadRest.makeJSONDecoder().decode(WrappedType.self, from: data) }
+ .mapError { (error) -> ResponseHandlerError in
+ return .decodeData(error)
+ }.map { (relays) -> Response in
+ let etag = httpResponse.value(forCaseInsensitiveHTTPHeaderField: HttpHeader.etag)
+
+ return .newContent(etag, relays)
+ }
+
+ case HttpStatus.notModified where etag != nil:
+ return .success(.notModified)
+
+ case let statusCode:
+ return .failure(.badResponse(statusCode))
+ }
+ }
+}
+
// MARK: - Endpoints
/// A struct that describes the REST endpoint, including the expected input and output
-struct RestEndpoint<Input, Response> where Input: RestPayload, Response: RestResponse {
+struct RestEndpoint<Input, Response> where Input: RestPayload {
let endpointURL: URL
let httpMethod: HttpMethod
+ let makeResponseHandler: (Input) -> AnyResponseHandler<Response>
- init(endpointURL: URL, httpMethod: HttpMethod) {
+ init<Handler: ResponseHandler>(endpointURL: URL, httpMethod: HttpMethod, responseHandlerFactory: @escaping (Input) -> Handler) where Handler.Response == Response {
self.endpointURL = endpointURL
self.httpMethod = httpMethod
+ self.makeResponseHandler = { (input) -> AnyResponseHandler<Response> in
+ return AnyResponseHandler(responseHandlerFactory(input))
+ }
}
/// Create `URLSessionDataTask` that automatically parses the HTTP response and returns the
/// expected response type or error upon completion.
- func dataTask(session: URLSession, payload: Input, completionHandler: @escaping (Result<Response.Output, RestError>) -> Void) -> Result<URLSessionDataTask, RestError> {
+ func dataTask(session: URLSession, payload: Input, completionHandler: @escaping (Result<Response, RestError>) -> Void) -> Result<URLSessionDataTask, RestError> {
return makeURLRequest(payload: payload).map { (request) -> URLSessionDataTask in
return session.dataTask(with: request) { (responseData, urlResponse, error) in
- let result = Self.handleURLResponse(urlResponse, data: responseData, error: error)
+ let handler = self.makeResponseHandler(payload)
+ let result = Self.handleURLResponse(urlResponse, data: responseData, error: error, responseHandler: handler)
completionHandler(result)
}
}
@@ -236,13 +345,13 @@ struct RestEndpoint<Input, Response> where Input: RestPayload, Response: RestRes
timeoutInterval: kNetworkTimeout
)
request.httpShouldHandleCookies = false
- request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.addValue("application/json", forHTTPHeaderField: HttpHeader.contentType)
request.httpMethod = httpMethod.rawValue
return request
}
/// A private HTTP response handler
- private static func handleURLResponse(_ urlResponse: URLResponse?, data: Data?, error: Error?) -> Result<Response.Output, RestError> {
+ private static func handleURLResponse(_ urlResponse: URLResponse?, data: Data?, error: Error?, responseHandler: AnyResponseHandler<Response>) -> Result<Response, RestError> {
if let error = error {
let networkError = error as? URLError ?? URLError(.unknown)
@@ -255,30 +364,26 @@ struct RestEndpoint<Input, Response> where Input: RestPayload, Response: RestRes
let data = data ?? Data()
- // Treat all 2xx responses as success despite the subtle meaning they may convey
- if (200..<300).contains(httpResponse.statusCode) {
- return Self.decodeSuccessResponse(data)
- } else {
- return Self.decodeErrorResponse(data)
- .flatMap { (serverErrorResponse) -> Result<Response.Output, RestError> in
- return .failure(.server(serverErrorResponse))
- }
- }
- }
+ return responseHandler.decodeResponse(httpResponse, data: data)
+ .flatMapError { (error) -> Result<Response, RestError> in
+ switch error {
+ case .badResponse:
+ // Try decoding the server error response in case when unexpected response is returned
+ return Self.decodeErrorResponse(httpResponse: httpResponse, data: data)
+ .flatMap { (serverErrorResponse) -> Result<Response, RestError> in
+ return .failure(.server(serverErrorResponse))
+ }
- /// A private helper that parses the JSON response in case of success (HTTP 2xx)
- private static func decodeSuccessResponse(_ responseData: Data) -> Result<Response.Output, RestError> {
- return Result { () -> Response.Output in
- return try Response.decodeResponse(responseData)
- }.mapError({ (error) -> RestError in
- return .decodeSuccessResponse(error)
- })
+ case .decodeData(let decodingError):
+ return .failure(.decodeSuccessResponse(decodingError))
+ }
+ }
}
/// A private helper that parses the JSON response in case of error (Any HTTP code except 2xx)
- private static func decodeErrorResponse(_ responseData: Data) -> Result<ServerErrorResponse, RestError> {
+ private static func decodeErrorResponse(httpResponse: HTTPURLResponse, data: Data) -> Result<ServerErrorResponse, RestError> {
return Result { () -> ServerErrorResponse in
- return try ServerErrorResponse.decodeResponse(responseData)
+ return try MullvadRest.makeJSONDecoder().decode(ServerErrorResponse.self, from: data)
}.mapError({ (error) -> RestError in
return .decodeErrorResponse(error)
})
@@ -286,7 +391,7 @@ struct RestEndpoint<Input, Response> where Input: RestPayload, Response: RestRes
}
/// A convenience class for `RestEndpoint` that transparently provides it with the `URLSession`
-struct RestSessionEndpoint<Input, Response> where Input: RestPayload, Response: RestResponse {
+struct RestSessionEndpoint<Input, Response> where Input: RestPayload {
let session: URLSession
let endpoint: RestEndpoint<Input, Response>
@@ -297,7 +402,7 @@ struct RestSessionEndpoint<Input, Response> where Input: RestPayload, Response:
/// Create `URLSessionDataTask` that automatically parses the HTTP response and returns the
/// expected response type or error upon completion.
- func dataTask(payload: Input, completionHandler: @escaping (Result<Response.Output, RestError>) -> Void) -> Result<URLSessionDataTask, RestError> {
+ func dataTask(payload: Input, completionHandler: @escaping (Result<Response, RestError>) -> Void) -> Result<URLSessionDataTask, RestError> {
return endpoint.dataTask(session: session, payload: payload, completionHandler: completionHandler)
}
@@ -321,7 +426,7 @@ struct MullvadRest {
return RestSessionEndpoint(session: session, endpoint: Self.createAccount())
}
- func getRelays() -> RestSessionEndpoint<EmptyPayload, ServerRelaysResponse> {
+ func getRelays() -> RestSessionEndpoint<ETagPayload<EmptyPayload>, HttpResourceCacheResponse<ServerRelaysResponse>> {
return RestSessionEndpoint(session: session, endpoint: Self.getRelays())
}
@@ -341,7 +446,7 @@ struct MullvadRest {
return RestSessionEndpoint(session: session, endpoint: Self.replaceWireguardKey())
}
- func deleteWireguardKey() -> RestSessionEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, EmptyResponse> {
+ func deleteWireguardKey() -> RestSessionEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, ()> {
return RestSessionEndpoint(session: session, endpoint: Self.deleteWireguardKey())
}
@@ -349,7 +454,7 @@ struct MullvadRest {
return RestSessionEndpoint(session: session, endpoint: Self.createApplePayment())
}
- func sendProblemReport() -> RestSessionEndpoint<ProblemReportRequest, EmptyResponse> {
+ func sendProblemReport() -> RestSessionEndpoint<ProblemReportRequest, ()> {
return RestSessionEndpoint(session: session, endpoint: Self.sendProblemReport())
}
}
@@ -359,15 +464,21 @@ extension MullvadRest {
static func createAccount() -> RestEndpoint<EmptyPayload, AccountResponse> {
return RestEndpoint(
endpointURL: kRestBaseURL.appendingPathComponent("accounts"),
- httpMethod: .post
+ httpMethod: .post,
+ responseHandlerFactory: { (input) in
+ return DecodingResponseHandler(expectedStatus: HttpStatus.created)
+ }
)
}
/// GET /v1/relays
- static func getRelays() -> RestEndpoint<EmptyPayload, ServerRelaysResponse> {
+ static func getRelays() -> RestEndpoint<ETagPayload<EmptyPayload>, HttpResourceCacheResponse<ServerRelaysResponse>> {
return RestEndpoint(
endpointURL: kRestBaseURL.appendingPathComponent("relays"),
- httpMethod: .get
+ httpMethod: .get,
+ responseHandlerFactory: { (input) in
+ return HttpCacheDecodingResponseHandler(etag: input.etag)
+ }
)
}
@@ -375,14 +486,20 @@ extension MullvadRest {
static func getAccountExpiry() -> RestEndpoint<TokenPayload<EmptyPayload>, AccountResponse> {
return RestEndpoint(
endpointURL: kRestBaseURL.appendingPathComponent("me"),
- httpMethod: .get
+ httpMethod: .get,
+ responseHandlerFactory: { (input) in
+ return DecodingResponseHandler(expectedStatus: HttpStatus.ok)
+ }
)
}
/// GET /v1/wireguard-keys/{pubkey}
static func getWireguardKey() -> RestEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, WireguardAddressesResponse> {
return RestEndpoint(
endpointURL: kRestBaseURL.appendingPathComponent("wireguard-keys"),
- httpMethod: .get
+ httpMethod: .get,
+ responseHandlerFactory: { (input) in
+ return DecodingResponseHandler(expectedStatus: HttpStatus.ok)
+ }
)
}
@@ -390,7 +507,10 @@ extension MullvadRest {
static func pushWireguardKey() -> RestEndpoint<TokenPayload<PushWireguardKeyRequest>, WireguardAddressesResponse> {
return RestEndpoint(
endpointURL: kRestBaseURL.appendingPathComponent("wireguard-keys"),
- httpMethod: .post
+ httpMethod: .post,
+ responseHandlerFactory: { (input) in
+ return DecodingResponseHandler(expectedStatus: HttpStatus.created)
+ }
)
}
@@ -398,15 +518,21 @@ extension MullvadRest {
static func replaceWireguardKey() -> RestEndpoint<TokenPayload<ReplaceWireguardKeyRequest>, WireguardAddressesResponse> {
return RestEndpoint(
endpointURL: kRestBaseURL.appendingPathComponent("replace-wireguard-key"),
- httpMethod: .post
+ httpMethod: .post,
+ responseHandlerFactory: { (input) in
+ return DecodingResponseHandler(expectedStatus: HttpStatus.created)
+ }
)
}
/// DELETE /v1/wireguard-keys/{pubkey}
- static func deleteWireguardKey() -> RestEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, EmptyResponse> {
+ static func deleteWireguardKey() -> RestEndpoint<PublicKeyPayload<TokenPayload<EmptyPayload>>, ()> {
return RestEndpoint(
endpointURL: kRestBaseURL.appendingPathComponent("wireguard-keys"),
- httpMethod: .delete
+ httpMethod: .delete,
+ responseHandlerFactory: { (input) in
+ return EmptyResponseHandler(expectedStatus: HttpStatus.noContent)
+ }
)
}
@@ -414,14 +540,20 @@ extension MullvadRest {
static func createApplePayment() -> RestEndpoint<TokenPayload<CreateApplePaymentRequest>, CreateApplePaymentResponse> {
return RestEndpoint(
endpointURL: kRestBaseURL.appendingPathComponent("create-apple-payment"),
- httpMethod: .post
+ httpMethod: .post,
+ responseHandlerFactory: { (input) in
+ return DecodingResponseHandler(expectedStatus: HttpStatus.created)
+ }
)
}
- static func sendProblemReport() -> RestEndpoint<ProblemReportRequest, EmptyResponse> {
+ static func sendProblemReport() -> RestEndpoint<ProblemReportRequest, ()> {
return RestEndpoint(
endpointURL: kRestBaseURL.appendingPathComponent("problem-report"),
- httpMethod: .post
+ httpMethod: .post,
+ responseHandlerFactory: { (input) in
+ return EmptyResponseHandler(expectedStatus: HttpStatus.noContent)
+ }
)
}
@@ -458,7 +590,7 @@ struct TokenPayload<Payload: RestPayload>: RestPayload {
}
func inject(into request: inout URLRequest) throws {
- request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
+ request.addValue("Token \(token)", forHTTPHeaderField: HttpHeader.authorization)
try payload.inject(into: &request)
}
}
@@ -482,6 +614,30 @@ struct PublicKeyPayload<Payload: RestPayload>: RestPayload {
}
}
+/// A payload that adds the ETag header to the request
+struct ETagPayload<Payload: RestPayload>: RestPayload {
+ let etag: String?
+ let enforceWeakValidator: Bool
+ let payload: Payload
+
+ init(etag: String?, enforceWeakValidator: Bool, payload: Payload) {
+ self.etag = etag
+ self.enforceWeakValidator = enforceWeakValidator
+ self.payload = payload
+ }
+
+ func inject(into request: inout URLRequest) throws {
+ if var etag = etag {
+ // Enforce weak validator to account for some backend caching quirks.
+ if enforceWeakValidator && etag.starts(with: "\"") {
+ etag.insert(contentsOf: "W/", at: etag.startIndex)
+ }
+ request.setValue(etag, forHTTPHeaderField: HttpHeader.ifNoneMatch)
+ }
+ try payload.inject(into: &request)
+ }
+}
+
/// An empty payload placeholder type.
/// Use it in places where the payload is not expected
struct EmptyPayload: RestPayload {
@@ -492,7 +648,7 @@ struct EmptyPayload: RestPayload {
// MARK: - Response types
-struct AccountResponse: Decodable, RestResponse {
+struct AccountResponse: Decodable {
let token: String
let expires: Date
}
@@ -524,7 +680,27 @@ struct ServerWireguardTunnels: Codable {
let relays: [ServerRelay]
}
-struct ServerRelaysResponse: Codable, RestResponse {
+private extension HTTPURLResponse {
+ func value(forCaseInsensitiveHTTPHeaderField headerField: String) -> String? {
+ if #available(iOS 13.0, *) {
+ return self.value(forHTTPHeaderField: headerField)
+ } else {
+ for case let key as String in self.allHeaderFields.keys {
+ if case .orderedSame = key.caseInsensitiveCompare(headerField) {
+ return self.allHeaderFields[key] as? String
+ }
+ }
+ return nil
+ }
+ }
+}
+
+enum HttpResourceCacheResponse<T: Decodable> {
+ case notModified
+ case newContent(_ etag: String?, _ value: T)
+}
+
+struct ServerRelaysResponse: Codable {
let locations: [String: ServerLocation]
let wireguard: ServerWireguardTunnels
}
@@ -533,7 +709,7 @@ struct PushWireguardKeyRequest: Encodable, RestPayload {
let pubkey: Data
}
-struct WireguardAddressesResponse: Decodable, RestResponse {
+struct WireguardAddressesResponse: Decodable {
let id: String
let pubkey: Data
let ipv4Address: IPAddressRange
@@ -549,7 +725,7 @@ struct CreateApplePaymentRequest: Encodable, RestPayload {
let receiptString: Data
}
-struct CreateApplePaymentResponse: Decodable, RestResponse {
+struct CreateApplePaymentResponse: Decodable {
let timeAdded: Int
let newExpiry: Date
diff --git a/ios/MullvadVPN/RelayCache.swift b/ios/MullvadVPN/RelayCache.swift
index 2d15f9a1fd..a41e64c3ac 100644
--- a/ios/MullvadVPN/RelayCache.swift
+++ b/ios/MullvadVPN/RelayCache.swift
@@ -191,39 +191,53 @@ class RelayCache {
let nextUpdate = Self.nextUpdateDate(lastUpdatedAt: cachedRelays.updatedAt)
if let nextUpdate = nextUpdate, nextUpdate <= Date() {
- self.downloadRelays()
+ self.downloadRelays(previouslyCachedRelays: cachedRelays)
}
case .failure(let readError):
self.logger.error(chainedError: readError, message: "Failed to read the relay cache to determine if it needs to be updated")
if Self.shouldDownloadRelaysOnReadFailure(readError) {
- self.downloadRelays()
+ self.downloadRelays(previouslyCachedRelays: nil)
}
}
}
- private func downloadRelays() {
- let taskResult = makeDownloadTask { (result) in
- let result = result.flatMap { (relays) -> Result<CachedRelays, RelayCacheError> in
- let cachedRelays = CachedRelays(relays: relays, updatedAt: Date())
-
- return Self.write(cacheFileURL: self.cacheFileURL, record: cachedRelays)
- .map { cachedRelays }
- }
-
+ private func downloadRelays(previouslyCachedRelays: CachedRelays?) {
+ let taskResult = makeDownloadTask(etag: previouslyCachedRelays?.etag) { (result) in
switch result {
- case .success(let cachedRelays):
- let numRelays = cachedRelays.relays.wireguard.relays.count
+ case .success(.newContent(let etag, let relays)):
+ let numRelays = relays.wireguard.relays.count
self.logger.info("Downloaded \(numRelays) relays")
- self.observerList.forEach { (observer) in
- observer.relayCache(self, didUpdateCachedRelays: cachedRelays)
+ let cachedRelays = CachedRelays(etag: etag, relays: relays, updatedAt: Date())
+ switch Self.write(cacheFileURL: self.cacheFileURL, record: cachedRelays) {
+ case .success:
+ self.observerList.forEach { (observer) in
+ observer.relayCache(self, didUpdateCachedRelays: cachedRelays)
+ }
+
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to store downloaded relays")
+ }
+
+ case .success(.notModified):
+ self.logger.info("Relays haven't changed since last check.")
+
+ var cachedRelays = previouslyCachedRelays!
+ cachedRelays.updatedAt = Date()
+
+ switch Self.write(cacheFileURL: self.cacheFileURL, record: cachedRelays) {
+ case .success:
+ break
+
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to update cached relays timestamp")
}
case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to update the relays")
+ self.logger.error(chainedError: error, message: "Failed to download relays")
}
}
@@ -256,8 +270,8 @@ class RelayCache {
self.timerSource = timerSource
}
- private func makeDownloadTask(completionHandler: @escaping (Result<ServerRelaysResponse, RelayCacheError>) -> Void) -> Result<URLSessionDataTask, RestError> {
- return rest.getRelays().dataTask(payload: EmptyPayload()) { (result) in
+ private func makeDownloadTask(etag: String?, completionHandler: @escaping (Result<HttpResourceCacheResponse<ServerRelaysResponse>, RelayCacheError>) -> Void) -> Result<URLSessionDataTask, RestError> {
+ return rest.getRelays().dataTask(payload: ETagPayload(etag: etag, enforceWeakValidator: true, payload: EmptyPayload())) { (result) in
self.dispatchQueue.async {
completionHandler(result.mapError { RelayCacheError.rest($0) })
}
@@ -369,6 +383,9 @@ class RelayCache {
/// A struct that represents the relay cache on disk
struct CachedRelays: Codable {
+ /// E-tag returned by server
+ var etag: String?
+
/// The relay list stored within the cache entry
var relays: ServerRelaysResponse