Skip to content

エラータイプ

HTTPC は 2 層のエラーモデルを採用しています:下層は標準 error インターフェース、上層は分類された ClientError 構造体です。すべてのリクエスト失敗(ネットワーク層)は classifyError によってコンテキストを保持する ClientError にマッピングされ、エラータイプ、リトライ可否の判定、構造化フィールドを提供します。HTTP 層エラー(4xx/5xx)は error を返さず、Result.StatusCode() でチェックします。

ClientError

go
type ClientError = engine.ClientError

分類された HTTP クライアントエラーで、errors.As で抽出します。内部 engine.ClientError のタイプエイリアスです。

構造体フィールド

go
type ClientError struct {
    Type       ErrorType  // エラー分類
    Message    string     // エラーの説明
    Cause      error      // 基底エラー
    URL        string     // リクエスト URL(マスク済み)
    Method     string     // HTTP メソッド
    Attempts   int        // 試行済み回数
    StatusCode int        // HTTP ステータスコード(該当する場合)
    Host       string     // ホスト名(サーキットブレーカー用)
}
フィールドタイプ説明典型値
TypeErrorTypeエラー分類、switch 判定に使用ErrorTypeNetworkErrorTypeTimeout
Messagestringエラーの説明情報"network operation failed"
Causeerror基底エラー、Unwrap() で取得可能*net.OpError*net.DNSError
URLstringリクエスト URL(認証情報はマスク済み)"https://example.com/path"
MethodstringHTTP メソッド"GET""POST"
Attemptsint試行済み回数(初回リクエスト含む)1(初回失敗)、4(3 回リトライ後)
StatusCodeintHTTP ステータスコード(HTTP エラー以外では 0)0(ネットワークエラー)、503(サーバーエラー)
Hoststringリクエストホスト名(サーキットブレーカー用)"example.com"

メソッド

メソッド戻り値説明
Error()string"METHOD url: message: cause (attempt N)" 形式にフォーマット
Code()string短いエラーコードを返す(例:"NETWORK_ERROR""TIMEOUT"
IsRetryable()boolこのエラーがリトライに値するかを判定
Unwrap()errorCause を返し、errors.Is/errors.As でエラーチェーンを走査可能
WithType(t ErrorType)*ClientErrorエラータイプを設定したコピーを返す(オリジナルは変更しない)

Error() フォーマット

Error() メソッドはエラーを読み取り可能な文字列にフォーマットします:

  • URL と Method の両方がある:"GET https://example.com: network operation failed: dial tcp ... (attempt 1)"
  • Message のみ:Message を直接出力
  • Cause がある:": " + Cause.Error() を追加
  • Attempts がある(>0):" (attempt N)" を追加

URL は出力前に自動的に SanitizeURL でマスク(認証情報削除)されます。エンジン分類パスで生成されたエラーは既にマスク済み(urlSanitized=true)で、冗長な url.Parse 呼び出しをスキップして割り当てを回避します。

Code() エラーコード

Code() はエラータイプを識別する短い文字列を返し、ログの分類と監視アラートに便利です:

ErrorTypeCode() 戻り値
ErrorTypeNetwork"NETWORK_ERROR"
ErrorTypeTimeout"TIMEOUT"
ErrorTypeContextCanceled"CONTEXT_CANCELED"
ErrorTypeResponseRead"RESPONSE_READ_ERROR"
ErrorTypeTransport"TRANSPORT_ERROR"
ErrorTypeRetryExhausted"RETRY_EXHAUSTED"
ErrorTypeTLS"TLS_ERROR"
ErrorTypeCertificate"CERTIFICATE_ERROR"
ErrorTypeDNS"DNS_ERROR"
ErrorTypeValidation"VALIDATION_ERROR"
ErrorTypeHTTP"HTTP_ERROR"
ErrorTypeUnknown(およびその他)"UNKNOWN_ERROR"
go
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) {
    log.Printf("エラーコード: %s, URL: %s, 試行回数: %d, リトライ可: %v",
        clientErr.Code(), clientErr.URL, clientErr.Attempts, clientErr.IsRetryable())
}

IsRetryable 判定ロジック

IsRetryable() は HTTPC リトライメカニズムの中核となる意思決定メソッドです。判定フロー:

  1. コンテキストエラーを優先チェック:Cause が context.Canceled または context.DeadlineExceeded の場合、即座に false を返す(永遠にリトライしない)
  2. ErrorType でディスパッチ
ErrorTypeリトライ可判定ロジック
ErrorTypeNetwork条件によるCause をチェック:ラップされた ClientError → 再帰判定;*net.OpError → タイムアウトまたはリトライ可能な syscall(ECONNREFUSED/ECONNRESET/EPIPE/ETIMEDOUT/ENETUNREACH/EHOSTUNREACH);net.Error → デフォルトでリトライ可;メッセージマッチ("connection reset"/"eof"/"broken pipe" など)
ErrorTypeTimeoutはいすべてのトランスポート層タイムアウトはリトライ可
ErrorTypeTransportはいHTTP トランスポート層エラー
ErrorTypeResponseRead条件による読み取り操作(Op == "read" または "readfrom")のみリトライ可;書き込み操作はリトライしない
ErrorTypeDNS条件によるCause が *net.DNSError の場合、IsTemporary または IsTimeout が true の場合のみリトライ
ErrorTypeHTTP条件によるStatusCode が retryableStatusCodes(408/429/500/502/503/504)にヒットすればリトライ可
ErrorTypeContextCanceledいいえユーザーの能動的キャンセル
ErrorTypeValidationいいえリクエスト自体が不正、リトライは無意味
ErrorTypeTLSいいえTLS プロトコルエラー、通常は自己修復しない
ErrorTypeCertificateいいえ証明書検証失敗、リトライは無意味
ErrorTypeRetryExhaustedいいえリトライ回数が枯渇済み
ErrorTypeUnknownいいえ不明なエラー、保守的にリトライしない

retryableStatusCodes

go
var retryableStatusCodes = map[int]bool{
    408: true, // Request Timeout
    429: true, // Too Many Requests
    500: true, // Internal Server Error
    502: true, // Bad Gateway
    503: true, // Service Unavailable
    504: true, // Gateway Timeout
}

これは HTTP ステータスコードがリトライをトリガーする唯一の真実のソースで、リトライロジックと IsRetryable() の両方で使用されます。

タイムアウトタイプの微妙な違い

ErrorTypeTimeout はリトライ可能ですが、コンテキストデッドラインでトリガーされたタイムアウトはリトライできません——context.DeadlineExceeded は第 1 ステップでインターセプトされる(false を返す)ためです。トランスポート層タイムアウト(net.OpError.Timeout() など)のみが第 2 ステップに到達してリトライ可能と判定されます。これによりユーザーが設定した WithTimeout がリトライで突破されるのを防ぎます。

ErrorType

go
type ErrorType = engine.ErrorType

エラー分類列挙(int 型)。

定数意味典型的トリガーシナリオリトライ可
ErrorTypeUnknown0不明/未分類いかなる既知のパターンにもマッチしないいいえ
ErrorTypeNetwork1ネットワーク層エラー接続拒否、接続リセット、ネットワーク到達不可条件による
ErrorTypeTimeout2タイムアウトnet.OpError タイムアウト、コンテキストデッドライン¹条件による²
ErrorTypeContextCanceled3コンテキストキャンセルcontext.Cancel がトリガーいいえ
ErrorTypeResponseRead4レスポンスボディ読み取りエラーレスポンスボディ読み取り時の EOF/接続中断条件による
ErrorTypeTransport5トランスポート層エラーHTTP プロトコルエラー、トランスポート失敗はい
ErrorTypeRetryExhausted6リトライ枯渇MaxRetries に達しても成功せずいいえ
ErrorTypeTLS7TLS エラーTLS ハンドシェイク失敗、プロトコル不一致いいえ
ErrorTypeCertificate8証明書検証エラーx509 証明書の期限切れ/非信頼いいえ
ErrorTypeDNS9DNS 解決エラードメインが存在しない、DNS タイムアウト条件による
ErrorTypeValidation10リクエスト検証エラーURL 形式エラー、リダイレクト超過、CRLF インジェクションいいえ
ErrorTypeHTTP11HTTP 層エラー4xx/5xx レスポンス(リトライシナリオでのみ発生)条件による

¹ コンテキストデッドライン(WithTimeoutTimeoutConfig.Request)でトリガーされたタイムアウトはリトライされません;トランスポート層タイムアウト(net.OpError タイムアウトなど)のみがリトライされます。 ² 詳細は上記 IsRetryable 判定ロジック を参照してください。

タイプ判定

go
result, err := client.Get(url)
if err != nil {
    var clientErr *httpc.ClientError
    if errors.As(err, &clientErr) {
        switch clientErr.Type {
        case httpc.ErrorTypeTimeout:
            log.Println("リクエストタイムアウト")
        case httpc.ErrorTypeNetwork:
            log.Println("ネットワークエラー:", clientErr.Message)
        case httpc.ErrorTypeTLS:
            log.Println("TLS エラー")
        case httpc.ErrorTypeCertificate:
            log.Println("証明書検証失敗")
        case httpc.ErrorTypeDNS:
            log.Println("DNS 解決失敗")
        case httpc.ErrorTypeRetryExhausted:
            log.Println("リトライ枯渇、合計", clientErr.Attempts, "回試行")
        case httpc.ErrorTypeContextCanceled:
            log.Println("リクエストがキャンセルされました")
        case httpc.ErrorTypeValidation:
            log.Println("リクエスト検証失敗")
        }
    }
}

URL マスクメカニズム

ClientError.Error() はフォーマット時に自動的に validation.SanitizeURL を呼び出し、URL 内の認証情報(user:pass@host***:***@host)を削除し、機密情報のログやエラーメッセージへの漏洩を防止します。

go
// 元の URL: https://admin:[email protected]/data
// Error() 出力: GET https://***:***@api.example.com/data: ...

エンジン分類パス(classifyErrorWithSanitizedURL)は初回分類時にマスクを完了し urlSanitized=true を設定します。以降の Error() 呼び出しは冗長な url.Parse をスキップし、毎回のログ出力で割り当てが発生するのを回避します。

コールバック内のエラーマスク

MetricsMiddlewareLoggingMiddleware などのミドルウェアのコールバックでは、HTTPC は非 ClientError タイプのエラーメッセージを追加でチェックし、元の URL をマスク版に置き換えて、コールバックが認証情報を漏洩しないようにします。

エラー分類フロー

classifyError は基底の error*ClientError にマッピングする中核関数で、以下の順序で階層的に判定します:

  1. context エラーcontext.CanceledErrorTypeContextCanceledcontext.DeadlineExceededErrorTypeTimeout
  2. コネクションプール枯渇connection.ErrPoolExhaustedErrorTypeNetwork
  3. *url.Error のアンラップ:HTTP/2 無効ヘッダー、URL 解析失敗 → ErrorTypeValidation;それ以外は内側のエラーをアンラップして判定継続
  4. *net.DNSErrorErrorTypeDNS、タイムアウトと失敗を区別
  5. *net.OpErrorErrorTypeNetwork、タイムアウトと操作失敗を区別
  6. net.Error:タイムアウト → ErrorTypeTimeout;その他 → ErrorTypeNetwork
  7. メッセージパターンマッチ(フォールバック):エラーメッセージのキーワードで TLS/certificate/timeout/connection refused などの 20+ パターンをマッチ
  8. フォールバックurl.Error の内側がいかなるパターンにもマッチしない場合 → ErrorTypeNetwork;それ以外 → ErrorTypeUnknown

エラー変数

設定エラー

変数エラーメッセージトリガー条件
ErrNilConfig"config cannot be nil"nil Config を New/ValidateConfig に渡した
ErrInvalidTimeout"invalid timeout"タイムアウト値が負または 30 分の上限を超過
ErrInvalidRetry"invalid retry configuration"MaxRetries が 0–10 の範囲外、BackoffFactor が 1.0–10.0 の範囲外
ErrInvalidConnection"invalid connection configuration"MaxIdleConns/MaxConnsPerHost が範囲外、ProxyURL 形式エラー
ErrInvalidSecurity"invalid security configuration"MaxResponseBodySize が 0–1GB の範囲外
ErrInvalidMiddleware"invalid middleware configuration"MaxRedirects が 0–50 の範囲外、UserAgent が長すぎるか制御文字を含む
ErrInvalidHeader"invalid header"リクエストヘッダーのキー値に制御文字を含むかサイズ制限を超過

リクエストとレスポンスのエラー

変数エラーメッセージトリガー条件
ErrEmptyFilePath"file path cannot be empty"DownloadConfig.FilePath が空
ErrFileExists"file already exists"ファイルが既存で Overwrite=false かつ ResumeDownload=false
ErrResponseBodyEmpty"response body is empty"空のレスポンスボディに対して Unmarshal などの解析メソッドを呼び出した
ErrResponseBodyTooLarge"response body too large"レスポンスボディが MaxResponseBodySize を超過

クライアントエラー

変数エラーメッセージトリガー条件
ErrClientClosed"client is closed"Close() 後にクライアントを使用した

実用的なマッチングパターン

errors.As で ClientError を抽出

go
result, err := client.Get(url)
if err != nil {
    var clientErr *httpc.ClientError
    if errors.As(err, &clientErr) {
        // 構造化フィールドにアクセス
        fmt.Printf("エラーコード: %s\n", clientErr.Code())
        fmt.Printf("エラータイプ: %d\n", clientErr.Type)
        fmt.Printf("リクエスト: %s %s\n", clientErr.Method, clientErr.URL)
        fmt.Printf("試行回数: %d\n", clientErr.Attempts)
        if clientErr.StatusCode != 0 {
            fmt.Printf("ステータスコード: %d\n", clientErr.StatusCode)
        }
    }
}

errors.Is でセンチネルエラーをマッチ

go
if errors.Is(err, httpc.ErrClientClosed) {
    // クライアントがクローズ済み、再作成が必要
}
if errors.Is(err, httpc.ErrResponseBodyEmpty) {
    // レスポンスボディが空、解析をスキップ
}
if errors.Is(err, httpc.ErrFileExists) {
    // ファイルが既存、ユーザーに通知するか Overwrite=true を設定
}

errors.Unwrap でエラーチェーンを走査

go
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) {
    // Cause は基底エラー(例:*net.OpError)
    cause := clientErr.Unwrap()
    if cause != nil {
        var opErr *net.OpError
        if errors.As(cause, &opErr) {
            fmt.Println("操作:", opErr.Op)
            fmt.Println("ネットワーク:", opErr.Net)
            fmt.Println("アドレス:", opErr.Addr)
        }
    }
}

3 種類のマッチング方式の選択

  • errors.As:ClientError の構造化フィールド(Type/Code/URL/Attempts など)にアクセスする必要がある場合
  • errors.Is:センチネルエラー(ErrClientClosed などの設定/ファイルエラー)をマッチする場合
  • errors.Unwrap:最下層の net/error に到達してシステムレベルの診断情報を取得する必要がある場合

関連項目