【Swift】HTTPステータスコードに対するエラーハンドリング-async awaitを用いて-

ポケモンのAPIを用いて、実装しているサンプルコードがあり、そちらを参考に記事を作成する。

https://github.com/kinkofer/PokemonAPI


まず最初に HTTPステータスコードについて、

https://ja.wikipedia.org/wiki/HTTP%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89

簡単に説明すると、APIで通信処理をした際に、返ってくる番号がHTTPステータスコードです。そのステータスコードには、それぞれ意味があり、詳しくはURLのWikipediaをご覧ください。

私は、このwikipediaを見て、思いました。

「初学者泣かせだなー」「これ全部エラーを返さないとダメなのか。。」　　と、、

で、未経験エンジニアの私は思いました。

「HTTPステータスコードは別に、他のプロジェクトにおいてもエラーを投げる場所はそんなに変わらないんじゃないか？」　　と

そしたら、ちょうど参考にしていたポケモンAPIのサンプルコードで、HTTPErrorに関する実装があったので、参考にする。

HTTPError,HTTPStatus,Categoryを定義する
-
```swift: HTTPError.swift
import Foundation


enum HTTPError: Error, LocalizedError {
    // Request Error cases
    /// The request could not be constructed
    case invalidRequest
    /// The body of the request could not be created
    case unexpectedBody

    // Response Error cases
    /// The web service returned a unknown error, like a 500
    case httpError
    /// The response data did not have the expected format, value, or type
    case unexpectedResponse
    /// The web service did not return valid JSON
    case jsonParsingError
    /// The web service did not return valid String data
    case stringParsingError
    /// HTTP error 401
    case unauthorized
    /// HTTP error 403
    case forbidden
    /// HTTP error -1001
    case timeout

    /// A network connection could not be made
    case noNetwork

    /// General HTTP Error, including the response and response data, if anything was returned
    case serverResponse(HTTPStatus, Data?)
    /// General Error
    case other(Error)


    /// The error message returned from localizedDescription
    public var errorDescription: String? {
        #if DEBUG
        return debugDescription
        #else
        return description
        #endif
    }


    /// A user friendly error message
    var description: String {
        switch self {
        case .invalidRequest:
            return NSLocalizedString("The request could not be made. Please change and try again.", comment: "Invalid Request")
        case .unexpectedBody:
            return NSLocalizedString("There was a problem with the input. Please change and try again.", comment: "Unexpected Body")
        case .httpError:
            return NSLocalizedString("The web service returned an error.", comment: "HTTP Error")
        case .unexpectedResponse:
            return NSLocalizedString("The data returned was an unexpected response.", comment: "Unexpected Response")
        case .jsonParsingError:
            return NSLocalizedString("The json could not be parsed.", comment: "JSON Parsing Error")
        case .stringParsingError:
            return NSLocalizedString("The string could not be parsed.", comment: "String Parsing Error")
        case .unauthorized:
            return NSLocalizedString("Unauthorized, please sign in again.", comment: "Unauthorized")
        case .forbidden:
            return NSLocalizedString("You have not granted this app permission to access this data.", comment: "Forbidden")
        case .timeout:
            return NSLocalizedString("The request timed out.", comment: "Timeout")
        case .noNetwork:
            return NSLocalizedString("A network connection could not be established.", comment: "No Network")
        case .serverResponse(let status, _):
            return NSLocalizedString("The web service returned status code \(status.rawValue)", comment: "Server Response Error")
        case .other(let error):
            return NSLocalizedString("An error occured: \(error.localizedDescription)", comment: "Other Error")
        }
    }


    /// A developer friendly error message
    var debugDescription: String {
        switch self {
        case .invalidRequest:
            return NSLocalizedString("DEBUG (invalidRequest): The request could not be made. Please change and try again.", comment: "DEBUG Invalid Request")
        case .unexpectedBody:
            return NSLocalizedString("DEBUG (unexpectedBody): There was a problem with the input. Please change and try again.", comment: "DEBUG Unexpected Body")
        case .httpError:
            return NSLocalizedString("DEBUG (httpError): The web service returned an error.", comment: "DEBUG HTTP Error")
        case .unexpectedResponse:
            return NSLocalizedString("DEBUG (unexpectedResponse): The data returned was an unexpected response.", comment: "DEBUG Unexpected Response")
        case .jsonParsingError:
            return NSLocalizedString("DEBUG (jsonParsingError): The json could not be parsed.", comment: "DEBUG JSON Parsing Error")
        case .stringParsingError:
            return NSLocalizedString("DEBUG (stringParsingError): The string could not be parsed.", comment: "DEBUG String Parsing Error")
        case .unauthorized:
            return NSLocalizedString("DEBUG (unauthorized): Unauthorized, please sign in again.", comment: "DEBUG Unauthorized")
        case .forbidden:
            return NSLocalizedString("DEBUG (forbidden): You have not granted this app permission to access this data.", comment: "DEBUG Forbidden")
        case .timeout:
            return NSLocalizedString("DEBUG (timeout): The request timed out.", comment: "DEBUG Timeout")
        case .noNetwork:
            return NSLocalizedString("DEBUG (noNetwork): A network connection could not be established.", comment: "DEBUG No Network")
        case .serverResponse(let status, _):
            return NSLocalizedString("DEBUG (serverResponse): The web service returned status code \(status.rawValue)", comment: "DEBUG Server Response Error")
        case .other(let error):
            return NSLocalizedString("DEBUG (other): An error occured: \(error.localizedDescription)", comment: "DEBUG Other Error")
        }
    }


    /// Code from the custom error type
    var code: Int {
        switch self {
        case .unauthorized:
            return 401
        case .forbidden:
            return 403
        case .httpError:
            return 599
        case .timeout:
            return -1001
        case .noNetwork:
            return -1009
        case .serverResponse(let status, _):
            return status.rawValue
        case .other(let error as NSError):
            return error.code
        default:
            return 499
        }
    }
}



/**
 Valid HTTP response status codes that can be returned from a web server
 - seealso: [List of HTTP status codes - Wikipedia](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)
 */
enum HTTPStatus: Int {
    case continueCode = 100
    case switchingProtocols = 101
    case processing = 102

    case ok = 200
    case created = 201
    case accepted = 202
    case nonAuthoritativeInformation = 203
    case noContent = 204
    case resetContent = 205
    case partialContent = 206
    case multiStatus = 207
    case alreadyReported = 208
    case imUsed = 226

    case multipleChoices = 300
    case movedPermanently = 301
    case found = 302
    case seeOther = 303
    case notModified = 304
    case useProxy = 305
    case switchProxy = 306
    case temporaryRedirect = 307
    case permanentRedirect = 308

    case badRequest = 400
    case unauthorized = 401
    case paymentRequired = 402
    case forbidden = 403
    case notFound = 404
    case methodNotAllowed = 405
    case notAcceptable = 406
    case proxyAuthenticationRequired = 407
    case requestTimeOut = 408
    case conflict = 409
    case gone = 410
    case lengthRequired = 411
    case preconditionFailed = 412
    case payloadTooLarge = 413
    case uriTooLong = 414
    case unsupportedMediaType = 415
    case rangeNotSatisfiable = 416
    case expectationFailed = 417
    case teapod = 418
    case misdirectedRequest = 421
    case unprocessableEntity = 422
    case locked = 423
    case failedDependency = 424
    case upgradeRequired = 426
    case preconditionRequired = 428
    case tooManyRequests = 429
    case requestHeaderFieldsTooLarge = 431
    case unavailableForLegalReasons = 451

    case internalServerError = 500
    case notImplemented = 501
    case badGateway = 502
    case serviceUnavailable = 503
    case gatewayTimeOut = 504
    case httpVersionNotSupported = 505
    case variantAlsoNegotiates = 506
    case insufficientStorage = 507
    case loopDetected = 508
    case notExtended = 510
    case networkAuthenticationRequired = 511

    case unknown = 999


    /// Categories of HTTP status codes
    enum Category {
        /// 100-199
        case informational
        /// 200-299
        case success
        /// 300-399
        case redirection
        /// 400-499
        case clientError
        /// 500-599
        case serverError
        /// Not between 100-599
        case unknown
    }
}



extension HTTPStatus {
    /// Initializes an HTTPStatus enum with a given numeric status code
    /// - parameter code: The HTTP response code
    init(code: Int) {
        if let validStatus = HTTPStatus(rawValue: code) {
            self = validStatus
        }
        else {
            self = .unknown
        }
    }


    /// The category the status code belongs to
    var category: Category {
        let code = self.rawValue
        if code >= 100 && code < 200 {
            return .informational
        }
        else if code >= 200 && code < 300 {
            return .success
        }
        else if code >= 300 && code < 400 {
            return .redirection
        }
        else if code >= 400 && code < 500 {
            return .clientError
        }
        else if code >= 500 && code < 600 {
            return .serverError
        }
        else {
            return .unknown
        }
    }
}
```

上のコードは、

- `HTTPError`という　Errorに準拠した型
- `HTTPStatus`という列挙型
- `HTTPStatue`の中に`Category`　という列挙型

を定義する。詳細を説明すると、

- `HTTPError`は、エラーを投げる際に用いる。
- `HTTPStatus`は、インスタンス生成時にステータスコードを入れると、数字ではなく、言葉の列挙型でHTTPステータスコードの詳細を表示。
- `HTTPStatus`の中の`Category`は、`HTTPStatus`の列挙型の値が、下画像のどの分類に当てはまるかを、列挙型で定義している。
![スクリーンショット 2023-05-05 21.41.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799713/8a4800b5-446e-2aee-a514-4c4c4ddc352f.png)

URLSessionを拡張して、関数を定義し、その中でErrorを投げる
-

```swift: URLSession+Request.swift
extension URLSession {
        /**
     Starts the URLRequest and returns the response from the server or an error.
     - parameter request: The URLRequest
     - returns: Returns the web service response or throws an HTTPError
     */
    @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
    func startData(_ url: URL) async throws -> Data {
        do {
            let (data, response) = try await self.data(from: url)

            if let response = response as? HTTPURLResponse {
                let status = HTTPStatus(code: response.statusCode)

                if status == .unauthorized {
                    throw HTTPError.unauthorized
                }
                else if status.category == .success {
                    return data
                }
                else {
                    throw HTTPError.serverResponse(status, data)
                }
            }
            else {
                throw HTTPError.httpError
            }
        }
        catch {
            if let nsError = error as NSError?,
                nsError.code == HTTPError.noNetwork.code {
                throw HTTPError.noNetwork
            }
            else if let nsError = error as NSError?,
                nsError.code == HTTPError.timeout.code {
                throw HTTPError.timeout
            }
            else if let httpError = error as? HTTPError {
                throw httpError
            }
            else {
                throw HTTPError.other(error)
            }
        }
    }
}
```
先程の定義した`HTTPError`・`HTTPStatus`・`Category`を用いて、HTTPステータスコードから、エラーを投げています。また、`URLSession`の中で、`HTTPError`を投げる事によって、この後に作成するAPIの構造体の中で、HTTPステータスコードに関するエラーを投げる必要がなくなります。この`URLSession`のメソッドの中で、投げているからです。

PokemonAPI　を実装
-
```swift:PokemonAPI.swift
struct PokemonAPI {
    let session = URLSession.shared

    private let baseUrl = "https://pokeapi.co/api/v2"

   func fetchPokemonDetail(pokemon: Pokemon) async throws -> PokemonDetail {
        guard let url = URL(string: pokemon.url) else {
            throw PokemonAPIError.invalidURL
        }

        do {
            let data = try await session.startData(url)
            let decoder = JSONDecoder()
            let pokemon = try decoder.decode(PokemonDetail.self, from: data)
            return pokemon
        } catch  _ as DecodingError {
            throw PokemonAPIError.decodingFailed
        } 
    }
}
```

PokemonAPIの中で、`let data = try await session.startData(url)`があります。
このコードにより、
- 失敗すれば、HTTPErrorが投げられる。
- 成功すれば、`Data`型が返ってきます。

PokemonAPIのメソッド内をスッキリ書くことができました。


Viewで実装
-
```swift
.task {
            do {
                pokemonDetail = try await pokemonAPI.fetchPokemonDetail(pokemon: pokemon)
            } catch {
                print(error.localizedDescription)
            }
        }
```

`error.localizedDescription`で、エラーの詳細を受け取ることができます。


以上になります。

（余談）
学習始めた初期に、個人開発で都道府県の列挙型を定義する必要がありました。その際に記事を探していると、都道府県の列挙型をコピペして使えるようにしてくださっている記事があり、とても感動しました。できるだけプロジェクト間で、共通化できる部分は、手を抜いて、共通化が難しい実装ではしっかり実装する必要があるな。。と初学者ながら、感じました。


他にも良い方法があれば、コメントいただけると大変うれしいです。
良かったと思ったら、いいねやTwitterのフォローよろしくお願いいたします！

https://sites.google.com/view/muranakar
個人でアプリを作成しているので、良かったら覗いてみてください！

参考URL
-

https://www.seohacks.net/blog/1147/