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

Add Upload Request Timeout, Fix API Error Messages #230

Merged
merged 10 commits into from
Sep 17, 2024
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Release Notes

## [Unreleased]

### Changed
* Convert network service upload function to normal async/await from AsyncThrowingStream.
* Handle requestError with URLError and return localizedDescription for user facing alert message.
* Handle httpError and provide a user facing message for the alert.

## 10.2.9

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject {
}

func onRetry() {
if let selfieFile {
if selfieFile != nil {
submitJob()
} else {
DispatchQueue.main.async { self.step = .selfie }
Expand All @@ -56,8 +56,7 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject {
func onFinished(delegate: BiometricKycResultDelegate) {
if let selfieFile = selfieFile,
let livenessFiles = livenessFiles,
let selfiePath = getRelativePath(from: selfieFile)
{
let selfiePath = getRelativePath(from: selfieFile) {
delegate.didSucceed(
selfieImage: selfiePath,
livenessImages: livenessFiles.compactMap { getRelativePath(from: $0) },
Expand All @@ -78,19 +77,19 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject {
jobId: jobId,
fileType: FileType.selfie
)

livenessFiles = try LocalStorage.getFilesByType(
jobId: jobId,
fileType: FileType.liveness
)

guard let selfieFile else {
// Set step to .selfieCapture so that the Retry button goes back to this step
DispatchQueue.main.async { self.step = .selfie }
error = SmileIDError.unknown("Error capturing selfie")
return
}

var allFiles = [URL]()
let infoJson = try LocalStorage.createInfoJsonFile(
jobId: jobId,
Expand Down Expand Up @@ -143,7 +142,7 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject {
throw error
}
}
_ = try await SmileID.api.upload(
let _ = try await SmileID.api.upload(
zip: zipData,
to: prepUploadResponse.uploadUrl
)
Expand Down Expand Up @@ -179,9 +178,11 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject {
print("Error submitting job: \(error)")
let (errorMessageRes, errorMessage) = toErrorMessage(error: error)
self.error = error
self.errorMessageRes = errorMessageRes
self.errorMessage = errorMessage
DispatchQueue.main.async { self.step = .processing(.error) }
DispatchQueue.main.async {
self.errorMessageRes = errorMessageRes
self.errorMessage = errorMessage
self.step = .processing(.error)
}
}
} catch {
didSubmitBiometricJob = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,28 +115,28 @@ internal class IOrchestratedDocumentVerificationViewModel<T, U: JobResult>: Obse
onError(error: SmileIDError.unknown("Error getting document front file"))
return
}

selfieFile = try LocalStorage.getFileByType(
jobId: jobId,
fileType: FileType.selfie
)

livenessFiles = try LocalStorage.getFilesByType(
jobId: jobId,
fileType: FileType.liveness
)

guard let selfieFile else {
// Set step to .selfieCapture so that the Retry button goes back to this step
step = .selfieCapture
onError(error: SmileIDError.unknown("Error getting selfie file"))
return
}

DispatchQueue.main.async {
self.step = .processing(.inProgress)
}

var allFiles = [URL]()
let frontDocumentUrl = try LocalStorage.createDocumentFile(
jobId: jobId,
Expand Down Expand Up @@ -212,7 +212,7 @@ internal class IOrchestratedDocumentVerificationViewModel<T, U: JobResult>: Obse
throw error
}
}
_ = try await SmileID.api.upload(
let _ = try await SmileID.api.upload(
zip: zipData,
to: prepUploadResponse.uploadUrl
)
Expand Down Expand Up @@ -316,7 +316,7 @@ internal class OrchestratedDocumentVerificationViewModel:
IOrchestratedDocumentVerificationViewModel<DocumentVerificationResultDelegate, DocumentVerificationJobResult>
{
override func onFinished(delegate: DocumentVerificationResultDelegate) {
if let savedFiles,
if let savedFiles,
let selfiePath = getRelativePath(from: selfieFile),
let documentFrontPath = getRelativePath(from: savedFiles.documentFront),
let documentBackPath = getRelativePath(from: savedFiles.documentBack)
Expand Down
7 changes: 5 additions & 2 deletions Sources/SmileID/Classes/Networking/APIError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ public enum SmileIDError: Error {
case decode(DecodingError)
case unknown(String)
case api(String, String)
case httpError(Int, Data)
case httpError(Int, String)
case jobStatusTimeOut
case consentDenied
case invalidJobId
case fileNotFound(String)
case invalidRequestBody
}

extension SmileIDError: LocalizedError {
Expand All @@ -25,7 +26,7 @@ extension SmileIDError: LocalizedError {
case .unknown(let message):
return message
case .httpError(let statusCode, let data):
return "HTTP Error with status code \(statusCode) and \(String(describing: data))"
return "HTTP Error with status code \(statusCode) and \(data)"
case .api(_, let message):
return message
case .jobStatusTimeOut:
Expand All @@ -36,6 +37,8 @@ extension SmileIDError: LocalizedError {
return "Invalid jobId or not found"
case .fileNotFound(let message):
return message
case .invalidRequestBody:
return "Invalid request body. The request data is missing or empty."
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ import Foundation
protocol RestServiceClient {
func send<T: Decodable>(request: RestRequest) async throws -> T
func multipart<T: Decodable>(request: RestRequest) async throws -> T
func upload(request: RestRequest) async throws -> AsyncThrowingStream<UploadResponse, Error>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe document this as well?

/// Uploads the given `RestRequest` asynchronously and returns the response data.
///
/// - Parameter request: The `RestRequest` object containing the details of the request to be uploaded.
/// - Returns: The response data from the server.
/// - Throws: An error if the upload fails.
func upload(request: RestRequest) async throws -> Data
}
4 changes: 2 additions & 2 deletions Sources/SmileID/Classes/Networking/ServiceRunnable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ protocol ServiceRunnable {
data: Data,
to url: String,
with restMethod: RestMethod
) async throws -> AsyncThrowingStream<UploadResponse, Error>
) async throws -> Data
}

extension ServiceRunnable {
Expand Down Expand Up @@ -145,7 +145,7 @@ extension ServiceRunnable {
data: Data,
to url: String,
with restMethod: RestMethod
) async throws -> AsyncThrowingStream<UploadResponse, Error> {
) async throws -> Data {
let uploadRequest = try await createUploadRequest(
url: url,
method: restMethod,
Expand Down
4 changes: 2 additions & 2 deletions Sources/SmileID/Classes/Networking/SmileIDService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public protocol SmileIDServiceable {
func prepUpload(request: PrepUploadRequest) async throws -> PrepUploadResponse

/// Uploads files to S3. The URL should be the one returned by `prepUpload`.
func upload(zip: Data, to url: String) async throws -> AsyncThrowingStream<UploadResponse, Error>
func upload(zip: Data, to url: String) async throws -> Data

/// Perform a synchronous SmartSelfie Enrollment. The response will include the final result of
/// the enrollment.
Expand Down Expand Up @@ -258,7 +258,7 @@ public class SmileIDService: SmileIDServiceable, ServiceRunnable {
)
}

public func upload(zip: Data, to url: String) async throws -> AsyncThrowingStream<UploadResponse, Error> {
public func upload(zip: Data, to url: String) async throws -> Data {
try await upload(data: zip, to: url, with: .put)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ class URLSessionRestServiceClient: NSObject, RestServiceClient {
typealias URLSessionResponse = (data: Data, response: URLResponse)
let session: URLSessionPublisher
let decoder = JSONDecoder()
let requestTimeout: TimeInterval

public init(
session: URLSessionPublisher = URLSession.shared
session: URLSessionPublisher,
requestTimeout: TimeInterval = SmileID.defaultRequestTimeout
) {
self.session = session
self.requestTimeout = requestTimeout
}

func send<T: Decodable>(request: RestRequest) async throws -> T {
Expand All @@ -31,24 +34,21 @@ class URLSessionRestServiceClient: NSObject, RestServiceClient {
}
}

public func upload(request: RestRequest) async throws -> AsyncThrowingStream<UploadResponse, Error> {
AsyncThrowingStream<UploadResponse, Error> { continuation in
do {
let urlRequest = try request.getUploadRequest()
let delegate = URLDelegate(continuation: continuation)
let uploadSession = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
uploadSession.uploadTask(with: urlRequest, from: request.body) { data, response, error in
if let error = error {
continuation.finish(throwing: error)
return
}
if (response as? HTTPURLResponse)?.statusCode == 200 {
continuation.yield(.response(data: data))
}
}.resume()
} catch {
continuation.finish(throwing: error)
}
public func upload(request: RestRequest) async throws -> Data {
guard let requestBody = request.body else {
throw SmileIDError.invalidRequestBody
}

do {
let urlRequest = try request.getUploadRequest()
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = requestTimeout
let uploadSession = URLSession(configuration: configuration)

let urlSessionResponse = try await uploadSession.upload(for: urlRequest, from: requestBody)
return try checkStatusCode(urlSessionResponse)
} catch {
throw mapToAPIError(error)
}
}

Expand All @@ -75,21 +75,32 @@ class URLSessionRestServiceClient: NSObject, RestServiceClient {
}
}

private struct ErrorResponse: Codable {
let error: String
}

private func checkStatusCode(_ urlSessionResponse: URLSessionResponse) throws -> Data {
let decoder = JSONDecoder()
guard let httpResponse = urlSessionResponse.response as? HTTPURLResponse,
httpResponse.isSuccess
else {
if let decodedError = try? JSONDecoder().decode(
if let decodedError = try? decoder.decode(
SmileIDErrorResponse.self,
from: urlSessionResponse.data
) {
throw SmileIDError.api(decodedError.code, decodedError.message)
}
throw SmileIDError.httpError(
(
if let httpError = try? decoder.decode(
ErrorResponse.self,
from: urlSessionResponse.data
) {
throw SmileIDError.httpError((
urlSessionResponse.response as? HTTPURLResponse
)?.statusCode ?? 500,
urlSessionResponse.data
)?.statusCode ?? 500, httpError.error)
}
throw SmileIDError.httpError(
(urlSessionResponse.response as? HTTPURLResponse)?.statusCode ?? 500,
"Unknown error occurred"
)
}

Expand Down
Loading
Loading