Monitor server response errors using Crashlytics

  1. Throw various errors when parsing response
class ModelUtil {
    // 1. 응답이 JSONObject나 JSONArray가 아님
    // 2. 응답의 전체를 감싸는 key값으로 parsing이 안됨
    // 3. key값인 Array 각각이 JSONObject가 아님
    @discardableResult
    static func apiFormatError() -> NSError{
        let error = NSError(domain:"com.companyname.appname.API JSON Format Error", code:0, userInfo:nil)
        Crashlytics.sharedInstance().recordError(error)
        return error
    }
    @discardableResult
    static func apiFormatError(userInfo: Dictionary<String, Any>) -> NSError{
        let error = NSError(domain:"com.companyname.appname.API JSON Format Error", code:0, userInfo:userInfo)
        Crashlytics.sharedInstance().recordError(error)
        return error
    }
    
    // 필수로 받아와야 하는 값이 parsing되지 않음
    @discardableResult static func apiEssentialValueParsingError() -> NSError {
        let error = NSError(domain:"com.companyname.appname.API JSON Essential Value Parsing Error", code:0, userInfo:nil)
        Crashlytics.sharedInstance().recordError(error)
        return error
    }
    
    // 1. parsing된 값이 empty임
    // 2. parsing된 값이 상황상 null이면 안 되는데 null임
    static func apiEmptyValueError() {
        let error = NSError(domain:"com.companyname.appname.API JSON Empty Parsed Value Error", code:0, userInfo:nil)
        Crashlytics.sharedInstance().recordError(error)
    }
    
    // 요구사항에서 받기로 했던 length와 API 결과 length가 다를 때
    static func apiLengthError() {
        let error = NSError(domain:"com.companyname.appname.API JSON Length is Not Expected", code:0, userInfo:nil)
        Crashlytics.sharedInstance().recordError(error)
    }
}

enum ModelError: Error {
    case invalidFormat(data: [String: Any])
}

Model class

extension IngredientPack {
    static func getIngredientPack(_ requestManager: RequestManager, type: PackType, completion: @escaping (RequestResult<IngredientPack>) -> Void) {
        let url = CustomRequest.domainAppend("APIPath")!
        requestManager.request(url: url, parameters: ["type": type.rawValue]) { (result) in
            switch result {
            case .success(let data):
                do {
                    let ingredientPacks = try JSONDecoder().decode([IngredientPack].self, from: data)
                    guard let ingredientPack = ingredientPacks.first else {
                        throw ModelUtil.apiFormatError()
                    }
                    completion(.success(ingredientPack))
                } catch {
                    completion(.failure(.others))
                }
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

2. Throw errors when the status code is not normal

func parseResponse(request: URLRequest, _ error: Error?, _ response: URLResponse?, _ data: Data?, completion: @escaping (RequestResult<Data>) -> Void) {
        // Network Indicator Control.
        self.networkIndicatorCount -= 1
        
        // Handling Error.
        if let error = error {
            ResponseError.cocoaError(request: request, error: error)
            completion(.failure(.others))
            
            // Handling Response Data.
        } else {
            if let httpURLResponse = response as? HTTPURLResponse {
                if httpURLResponse.statusCode != 403
                    && (httpURLResponse.statusCode < 200
                        || httpURLResponse.statusCode >= 400){
                    let statusCodeError = ResponseError.statusCodeError(statusCode: httpURLResponse.statusCode, request: request)
                    Crashlytics.sharedInstance().recordError(statusCodeError)
                    completion(.failure(.others))
                    return
                }
            }
            
            // (1) Data 형식이 JSON형태가 아닐 경우 Error 처리.
            // (2) Data 형식이 JSON형태이고 값이 UserAuthError인 경우 Error 처리.
            // (3) Data 형식이 JSON형태이고 값이 UserAuthError가 아니면 success처리.
            if let data = data, let response = response {
                if (data.count > 0) {
                    // TODO: UserAuthError를 확인하는 방법으로 Result접근이 아니라 StatusCode로 분별하기.
                    do {
                        let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
                        
                        // 인증 에러 인 경우 JSON형식으로 UserAuthError라는 Key에 Message값을 보냄.
                        if let json = jsonObject as? [String: Any],
                            let authErrorMessage = json["UserAuthError"] as? String {
                            ResponseError.userAuthError(request: request, response: response, json: json, authErrorMessage: authErrorMessage)
                            
                            completion(.failure(.authenticationFailed(message: authErrorMessage)))
                            return // .success가 호출되지 않도록 return
                        }
                    } catch {
                        let nsError = error as NSError
                        var userInfo = nsError.userInfo
                        let responseData = String(data: data, encoding: .utf8).or("nil")
                        userInfo.updateValue(responseData, forKey: "responseData")
                        let error = NSError(domain: nsError.domain, code: nsError.code, request: request, userInfo: userInfo)
                        Crashlytics.sharedInstance().recordError(error)
                        completion(.failure(.others))
                        return // .success가 호출되지 않도록 return
                    }
                }
                completion(.success(data))
                // Handling No Data.
            } else {
                ResponseError.noData(request: request)
                completion(.failure(.others))
                
                // TODO: CustomRequest.
                // 정상적인 상황에서도 response가 없는 요청도 있기 때문에 completeHandler가 있는 요청에 대해서만 (response를 바라는데 error 가 발생 한 경우) 에러를 리포팅 한다.
                // Response가 없는 요청 알아둘 것.
            }
        }
    }

3. Third party login error

@objc func login(on viewController: UIViewController) {
        if let currentToken = AccessToken.current {
            if let tracker = loginEventTracker {
                let loginResult = LoginManagerLoginResult(token: currentToken, isCancelled: false, grantedPermissions: [], declinedPermissions: [])
                tracker(loginResult, nil)
            } else {
                fetchUser(from: viewController)
            }
        } else {
            loginManager.logIn(permissions: MyFacebook.permissions, from: viewController) { (result, error) in
                if let error = error {
                    Alert.showConfirmAlert(withTitle: self.errorTitle, message: error.localizedDescription, viewController: viewController)
                    Crashlytics.sharedInstance().recordError(error)
                    self.thirdPartyLoginDelegate?.logFail(registerType: MyFacebook.registerType, message: self.errorMessage)
                } else if result?.isCancelled == false {
                    self.fetchUser(from: viewController)
                }
            }
        }
    }

Do I have to use “weak self” in network block?

  1. A reference cycle happens if the “self” refers the network completion block for example by storing the block as its property. But if not, event if the block refers self, the “self” doesn’t refer the block. So a reference cycle doesn’t happen.
  2. Using weak self in the block, make the “self” deinit even before the network request is processed.  You can determine whether you need to deinit self even before the network request is processed. Even though you do not use weak self, self will be deinitialized after the handler is called or the data task is canceled.
  3. What if the “self” refers to the url session data task?
    1. The task refers the completion block and the block refers to self.
    2. The reference cycle only occurs until the task is completed or canceled. 
    3. The task does not refer the block anymore once the request completed or got canceled. 
    4. There’s no difference between referring the session data task or not in this concern.

 

send nil value to Server

First of all, if you put nil, then parameter value becomes “nil”. But you cannot send nil as value, because nil is not used in some other languages like kotlin and if the server needs to handle other language too.

If you use NSNull() instead -> it becomes “<null>”

So both way doesn’t work.

Normally, just do not send key if it’s nil

https://github.com/Alamofire/Alamofire/issues/2407#issuecomment-355935419

https://stackoverflow.com/a/36717010/7193843