- AWS
boto3のエラーハンドリング|ClientErrorのコード取得とリトライ設定
boto3 のエラーは、AWS が返したエラー(botocore.exceptions.ClientError)と、リクエストを送る前後に手元で起きるエラー(BotoCoreError の仲間)の 2 系統に分かれます。ClientError は e.response["Error"]["Code"] でエラーコードを取り出して分岐するのが基本形です。
この記事では、e.response の中身を実際の辞書で確かめ、client.exceptions.XXX との使い分け、サービス別のエラーコード、botocore.exceptions の発生条件を表にします。後半では Config(retries=...) の 3 つのモードで「何回送られるか」を数え、リトライされるエラーとされないエラー、自前のバックオフと重ねたときの回数までコードで確かめます。
確認環境:Ubuntu 24.04、Python 3.11.15、boto3 1.43.103(botocore 1.43.103)。AWS の API をローカルで再現するツール(moto 5.2.3)で実行しました。S3 などを呼ぶコードは、実際の AWS でそのまま使える形(接続先の指定なし)で載せています。リトライの回数を数える節だけは、決まった応答を返す検証用サーバーに接続します。
boto3エラーハンドリングの早見表
| 知りたいこと | 書き方 |
|---|---|
| AWS が返したエラーを捕まえる | except botocore.exceptions.ClientError as e: |
| エラーコード | e.response["Error"]["Code"] |
| エラーメッセージ | e.response["Error"]["Message"] |
| HTTP ステータス | e.response["ResponseMetadata"]["HTTPStatusCode"] |
| リクエスト ID(問い合わせ用) | e.response["ResponseMetadata"]["RequestId"] |
| SDK が再送した回数 | e.response["ResponseMetadata"]["RetryAttempts"] |
| どの API で起きたか | e.operation_name |
| 決まった 1 種類だけ捕まえる | except client.exceptions.NoSuchKey: |
| resource から同じことをする | except resource.meta.client.exceptions.NoSuchKey: |
| 手元で起きるエラーをまとめて捕まえる | except botocore.exceptions.BotoCoreError: |
| リトライの回数とモードを決める | Config(retries={"mode": "standard", "total_max_attempts": 5}) |
| タイムアウトを決める | Config(connect_timeout=5, read_timeout=10) |
| 系統 | 親クラス | 起きる場面 | 例 |
|---|---|---|---|
| AWS が返したエラー | ClientError | リクエストは届き、サービスがエラー応答を返した | NoSuchKey、AccessDenied、ThrottlingException |
| 手元で起きるエラー | BotoCoreError | 引数の検証、認証情報・リージョンの解決、接続、待機 | ParamValidationError、NoCredentialsError、EndpointConnectionError |
ClientError と BotoCoreError は親子関係にありません。 両方を受けたいときは、except を 2 つ書きます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「boto3のエラーハンドリング|ClientErrorのコード取得とリトライ設定」
ClientErrorのe.responseの中身を見る
以降のコードは、rjc-demo-bucket というバケット(中にオブジェクトが 1 つある)を用意した状態で動かしています。存在しないキーを get_object して、例外の中身を表示します。
# err_response.py
import pprint
import boto3
from botocore.exceptions import ClientError
s3 = boto3.client("s3")
try:
s3.get_object(Bucket="rjc-demo-bucket", Key="report.csv")
except ClientError as e:
print("例外クラス :", type(e).__name__)
print("str(e) :", e)
print("操作名 :", e.operation_name)
print("Code :", e.response["Error"]["Code"])
print("Message :", e.response["Error"]["Message"])
meta = e.response["ResponseMetadata"]
print("HTTPStatus :", meta["HTTPStatusCode"])
print("RequestId :", meta["RequestId"])
print("RetryAttempts:", meta["RetryAttempts"])
print("--- e.response 全体(HTTPHeaders は長いので省略して表示)")
shown = dict(e.response)
shown["ResponseMetadata"] = {**meta, "HTTPHeaders": "…(省略)…"}
pprint.pprint(shown)
例外クラス : NoSuchKey
str(e) : An error occurred (NoSuchKey) when calling the GetObject operation: The specified key does not exist.
操作名 : GetObject
Code : NoSuchKey
Message : The specified key does not exist.
HTTPStatus : 404
RequestId : pEPoQ6Y09IvPuoKHiFJEu6OhHgbEtbaHbRqy9OM9zm4yj0AinJSf
RetryAttempts: 0
--- e.response 全体(HTTPHeaders は長いので省略して表示)
{'Error': {'Code': 'NoSuchKey',
'Key': 'report.csv',
'Message': 'The specified key does not exist.'},
'ResponseMetadata': {'HTTPHeaders': '…(省略)…',
'HTTPStatusCode': 404,
'RequestId': 'pEPoQ6Y09IvPuoKHiFJEu6OhHgbEtbaHbRqy9OM9zm4yj0AinJSf',
'RetryAttempts': 0}}
| キー | 中身 | 使いどころ |
|---|---|---|
| Error.Code | エラーコード(文字列) | 分岐の条件にする |
| Error.Message | 人が読むための説明 | ログに残す。文言は変わりうるので分岐には使わない |
| Error のその他のキー | サービスごとの追加情報(上の例では Key) | 対象の特定 |
| ResponseMetadata.HTTPStatusCode | HTTP ステータス | 4xx(呼び出し側の原因)と 5xx(サービス側の原因)の大まかな区別 |
| ResponseMetadata.RequestId | リクエスト ID | AWS サポートへの問い合わせ、ログの突き合わせ |
| ResponseMetadata.RetryAttempts | SDK が自動で再送した回数 | リトライが効いたかの確認 |
| ResponseMetadata.HTTPHeaders | 応答ヘッダーの辞書 | 詳しい調査 |
公式ドキュメントの例では、ResponseMetadata に HostId というキーも載っています。どのサービスでも必ずあるのは Error.Code と ResponseMetadataと考え、追加のキーは .get() で読むと安心です。
client.exceptionsとClientError+コード判定の使い分け
捕まえ方は 2 通りあります。
# two_ways.py
import boto3
from botocore.exceptions import ClientError
s3 = boto3.client("s3")
# 方法1: client.exceptions.XXX で、決まった 1 種類だけを捕まえる
try:
s3.get_object(Bucket="rjc-demo-bucket", Key="report.csv")
except s3.exceptions.NoSuchKey as e:
print("方法1 NoSuchKey を捕まえた:", e.response["Error"]["Message"])
# 方法2: ClientError で受けて、Error.Code で分岐する
def read_or_none(bucket, key):
try:
return s3.get_object(Bucket=bucket, Key=key)["Body"].read()
except ClientError as e:
code = e.response["Error"]["Code"]
if code in ("NoSuchKey", "404"):
return None # 無いのは想定内
if code == "NoSuchBucket":
raise RuntimeError(f"バケット {bucket} がありません") from e
raise # それ以外は上に伝える
print("方法2 無いキー :", read_or_none("rjc-demo-bucket", "report.csv"))
try:
read_or_none("rjc-none-bucket", "report.csv")
except RuntimeError as e:
print("方法2 無いバケット :", e)
# client.exceptions に無いコードは、ClientError でしか受けられない
for label, call in [
("head_object(無いキー)", lambda: s3.head_object(Bucket="rjc-demo-bucket", Key="report.csv")),
("delete_bucket(空でない)", lambda: s3.delete_bucket(Bucket="rjc-demo-bucket")),
]:
try:
call()
except s3.exceptions.NoSuchKey:
print(label, "-> NoSuchKey クラス")
except ClientError as e:
print(label, "->", type(e).__name__, "/ Code =", e.response["Error"]["Code"])
# client.exceptions に用意されているクラスの数
for name in ["s3", "dynamodb", "sqs", "iam", "sts"]:
c = boto3.client(name)
names = [n for n in dir(c.exceptions) if n[0].isupper() and n != "ClientError"]
print(f"{name:9} {len(names):3} クラス 例: {', '.join(names[:3])}")
方法1 NoSuchKey を捕まえた: The specified key does not exist.
方法2 無いキー : None
方法2 無いバケット : バケット rjc-none-bucket がありません
head_object(無いキー) -> ClientError / Code = 404
delete_bucket(空でない) -> ClientError / Code = BucketNotEmpty
s3 20 クラス 例: AccessDenied, AnnotationLimitExceeded, AnnotationNameTooLong
dynamodb 34 クラス 例: BackupInUseException, BackupNotFoundException, ConditionalCheckFailedException
sqs 28 クラス 例: BatchEntryIdsNotDistinct, BatchRequestTooLong, EmptyBatchRequest
iam 38 クラス 例: AccountNotManagementOrDelegatedAdministratorException, CallerIsNotManagementAccountException, ConcurrentModificationException
sts 12 クラス 例: ExpiredTokenException, ExpiredTradeInTokenException, IDPCommunicationErrorException
出力の 4〜5 行目がポイントです。head_object で無いキーを指定したときのコードは "404"、空でないバケットを消そうとしたときは BucketNotEmpty で、どちらも client.exceptions にクラスが無く、素の ClientError として届きます。 client.exceptions に用意されているのは、API 定義に載っているエラーだけです。
| 比べる点 | client.exceptions.XXX | ClientError + Error.Code |
|---|---|---|
| 書き方 | except s3.exceptions.NoSuchKey: |
except ClientError as e: のあと e.response["Error"]["Code"] で分岐 |
| 捕まえられる範囲 | API 定義にあるエラーだけ | サービスが返すすべてのエラー |
| 複数のコードをまとめる | except を並べる | if code in (...) で 1 か所にまとめる |
| クライアントが必要か | 必要(そのクライアントの属性) | 不要(import だけで書ける) |
| 向いている場面 | 「このエラーだけは想定内」と 1 種類を読みやすく書きたい | AccessDenied・スロットリングなど横断的なコードも扱う、共通関数にする |
公式ドキュメントにも、ClientError で受けてエラーコードを調べる方法が、サービス側のすべてのエラーを捕まえる最も確実な方法だと書かれています。迷ったら右側の書き方を基本にして、読みやすさが欲しい箇所だけ左側を使うとよいでしょう。
なお、client.exceptions のクラスは ClientError の子クラスです。except ClientError を先に書くと、後ろの except s3.exceptions.NoSuchKey には届かないので、細かいほうを先に書きます。
サービス別:よく出るエラーコード一覧
次のスクリプトで、5 つのサービスのエラーを実際に起こし、例外クラス名・Error.Code・HTTP ステータスを並べました。
# codes.py
import boto3
from botocore.exceptions import ClientError
s3, ddb, sqs, iam = (boto3.client(n) for n in ["s3", "dynamodb", "sqs", "iam"])
key = [{"AttributeName": "id", "KeyType": "HASH"}]
attr = [{"AttributeName": "id", "AttributeType": "S"}]
ddb.create_table(TableName="Tasks", KeySchema=key, AttributeDefinitions=attr, BillingMode="PAY_PER_REQUEST")
ddb.put_item(TableName="Tasks", Item={"id": {"S": "1"}})
queue_url = sqs.create_queue(QueueName="tasks")["QueueUrl"]
iam.create_user(UserName="taro")
cases = [
("s3.get_object 無いキー", lambda: s3.get_object(Bucket="rjc-demo-bucket", Key="x")),
("s3.head_object 無いキー", lambda: s3.head_object(Bucket="rjc-demo-bucket", Key="x")),
("s3.get_object 無いバケット", lambda: s3.get_object(Bucket="rjc-none-bucket", Key="x")),
("s3.create_bucket 作成済み", lambda: s3.create_bucket(Bucket="rjc-demo-bucket", CreateBucketConfiguration={"LocationConstraint": "ap-northeast-1"})),
("s3.delete_bucket 空でない", lambda: s3.delete_bucket(Bucket="rjc-demo-bucket")),
("ddb.get_item 無いテーブル", lambda: ddb.get_item(TableName="Nothing", Key={"id": {"S": "1"}})),
("ddb.create_table 作成済み", lambda: ddb.create_table(TableName="Tasks", KeySchema=key, AttributeDefinitions=attr, BillingMode="PAY_PER_REQUEST")),
("ddb.put_item 条件に合わない", lambda: ddb.put_item(TableName="Tasks", Item={"id": {"S": "1"}}, ConditionExpression="attribute_not_exists(id)")),
("ddb.get_item キー名が違う", lambda: ddb.get_item(TableName="Tasks", Key={"idx": {"S": "1"}})),
("sqs.get_queue_url 無いキュー", lambda: sqs.get_queue_url(QueueName="nothing")),
("sqs.create_queue 属性違いで再作成", lambda: sqs.create_queue(QueueName="tasks", Attributes={"VisibilityTimeout": "99"})),
("sqs.delete_message 受信ハンドルが不正", lambda: sqs.delete_message(QueueUrl=queue_url, ReceiptHandle="bogus")),
("iam.get_role 無いロール", lambda: iam.get_role(RoleName="nothing")),
("iam.create_user 作成済み", lambda: iam.create_user(UserName="taro")),
("iam.create_policy JSON が不正", lambda: iam.create_policy(PolicyName="p", PolicyDocument="{bad")),
]
for label, call in cases:
try:
call()
except ClientError as e:
err, status = e.response["Error"], e.response["ResponseMetadata"]["HTTPStatusCode"]
print(f"{label}\n class={type(e).__name__} Code={err['Code']} HTTP={status}")
s3.get_object 無いキー
class=NoSuchKey Code=NoSuchKey HTTP=404
s3.head_object 無いキー
class=ClientError Code=404 HTTP=404
s3.get_object 無いバケット
class=NoSuchBucket Code=NoSuchBucket HTTP=404
s3.create_bucket 作成済み
class=BucketAlreadyOwnedByYou Code=BucketAlreadyOwnedByYou HTTP=409
s3.delete_bucket 空でない
class=ClientError Code=BucketNotEmpty HTTP=409
ddb.get_item 無いテーブル
class=ResourceNotFoundException Code=ResourceNotFoundException HTTP=400
ddb.create_table 作成済み
class=ResourceInUseException Code=ResourceInUseException HTTP=400
ddb.put_item 条件に合わない
class=ConditionalCheckFailedException Code=ConditionalCheckFailedException HTTP=400
ddb.get_item キー名が違う
class=ClientError Code=ValidationException HTTP=400
sqs.get_queue_url 無いキュー
class=QueueDoesNotExist Code=AWS.SimpleQueueService.NonExistentQueue HTTP=400
sqs.create_queue 属性違いで再作成
class=QueueNameExists Code=QueueAlreadyExists HTTP=400
sqs.delete_message 受信ハンドルが不正
class=ReceiptHandleIsInvalid Code=ReceiptHandleIsInvalid HTTP=400
iam.get_role 無いロール
class=NoSuchEntityException Code=NoSuchEntity HTTP=404
iam.create_user 作成済み
class=EntityAlreadyExistsException Code=EntityAlreadyExists HTTP=409
iam.create_policy JSON が不正
class=MalformedPolicyDocumentException Code=MalformedPolicyDocument HTTP=400
クラス名と Error.Code が一致しないサービスがあります。 IAM はクラス名が NoSuchEntityException、コードが NoSuchEntity です。SQS はクラス名が QueueDoesNotExist、コードが AWS.SimpleQueueService.NonExistentQueue です。コードで分岐するときは、クラス名から推測せず、一度出力して確かめるのが確実です。
実行して確かめたものに、公式ドキュメントと botocore 同梱の API 定義から読み取ったものを足した一覧です(「確認」の列が「実行」のものは上の出力のとおり)。
S3
| Error.Code | HTTP | 出る場面 | client.exceptions のクラス | 確認 |
|---|---|---|---|---|
| NoSuchKey | 404 | get_object で無いキー | NoSuchKey | 実行 |
| 404 | 404 | head_object・head_bucket で対象が無い | なし | 実行 |
| NoSuchBucket | 404 | 無いバケットを指定 | NoSuchBucket | 実行 |
| BucketAlreadyOwnedByYou | 409 | 自分が持っている名前で create_bucket | BucketAlreadyOwnedByYou | 実行 |
| BucketAlreadyExists | 409 | ほかの利用者が使っている名前で create_bucket | BucketAlreadyExists | 公式 |
| BucketNotEmpty | 409 | 空でないバケットを delete_bucket | なし | 実行 |
| AccessDenied | 403 | 権限が足りない | AccessDenied | 公式・API 定義 |
| InvalidAccessKeyId | 403 | アクセスキー ID が登録されていない | なし | 公式 |
| SignatureDoesNotMatch | 403 | 署名が合わない(シークレットキーの誤りなど) | なし | 公式 |
| ExpiredToken | 400 | 一時的な認証情報の期限切れ | なし | 公式 |
| RequestTimeTooSkewed | 403 | 端末の時刻とサーバーの時刻の差が大きい | なし | 公式 |
| PreconditionFailed | 412 | 指定した前提条件に合わない | なし | 公式 |
| SlowDown | 503 | リクエストの頻度が高い | なし | 公式 |
| InternalError | 500 | サービス内部のエラー | なし | 公式 |
DynamoDB
「再試行」の列は、公式ドキュメントのエラー表にある「OK to retry?」の値です。
| Error.Code | HTTP | 出る場面 | 再試行 | client.exceptions のクラス | 確認 |
|---|---|---|---|---|---|
| ResourceNotFoundException | 400 | 無いテーブル・索引を指定 | しない | あり | 実行 |
| ResourceInUseException | 400 | 作成済みのテーブルを作る、変更中のテーブルを操作 | しない | あり | 実行 |
| ConditionalCheckFailedException | 400 | ConditionExpression に合わない | しない | あり | 実行 |
| ValidationException | 400 | キーや式の書き方が合わない | しない | なし | 実行 |
| AccessDeniedException | 400 | 権限が足りない | しない | なし | 公式 |
| ProvisionedThroughputExceededException | 400 | 設定したスループットを超えた | する | あり | 公式 |
| ThrottlingException | 400 | リクエストの頻度が許容量を超えた | する | あり | 公式 |
| RequestLimitExceeded | 400 | アカウントのスループット上限を超えた | する | あり | 公式 |
| LimitExceededException | 400 | 同時に行える操作の数を超えた | する | あり | 公式 |
| ItemCollectionSizeLimitExceededException | 400 | アイテムコレクションのサイズ上限を超えた | する | あり | 公式 |
| InternalServerError | 500 | サービス内部のエラー | する | あり | 公式 |
| ServiceUnavailable | 503 | 一時的に利用できない | する | なし | 公式 |
SQS
| Error.Code | HTTP | 出る場面 | client.exceptions のクラス | 確認 |
|---|---|---|---|---|
| AWS.SimpleQueueService.NonExistentQueue | 400 | 無いキューを指定 | QueueDoesNotExist | 実行 |
| QueueAlreadyExists | 400 | 同じ名前・違う属性で create_queue | QueueNameExists | 実行 |
| ReceiptHandleIsInvalid | 400 | 受信ハンドルが正しくない | ReceiptHandleIsInvalid | 実行 |
API 定義には、このほか次のクラスがあります(説明は API 定義の文面から)。
| client.exceptions のクラス | 出る場面 |
|---|---|
| QueueDeletedRecently | キューを消してから 60 秒以内に同じ名前で作成 |
| PurgeQueueInProgress | 直近 60 秒以内に PurgeQueue を受け付けたキューに再度 PurgeQueue |
| TooManyEntriesInBatchRequest | 一括操作のエントリが上限(10 件)を超えた |
| BatchRequestTooLong | 一括送信のメッセージ合計が上限を超えた |
| OverLimit | 処理中メッセージ数などの上限に達した |
| RequestThrottled | リクエストの頻度が許容量を超えた |
IAM
| Error.Code | HTTP | 出る場面 | client.exceptions のクラス | 確認 |
|---|---|---|---|---|
| NoSuchEntity | 404 | 無いユーザー・ロール・ポリシーを指定 | NoSuchEntityException | 実行 |
| EntityAlreadyExists | 409 | 同じ名前で作成 | EntityAlreadyExistsException | 実行 |
| MalformedPolicyDocument | 400 | ポリシーの JSON が正しくない | MalformedPolicyDocumentException | 実行 |
| DeleteConflict | 409 | 付随するものが残っているリソースを削除 | DeleteConflictException | API 定義 |
| LimitExceeded | 409 | アカウントの上限を超えて作成 | LimitExceededException | API 定義 |
| ConcurrentModification | 409 | 同じ対象への変更が同時に送られた | ConcurrentModificationException | API 定義 |
STS
| Error.Code | HTTP | 出る場面 | client.exceptions のクラス | 確認 |
|---|---|---|---|---|
| ExpiredTokenException | 400 | 渡した Web ID トークンの期限切れ | ExpiredTokenException | API 定義 |
| InvalidIdentityToken | 400 | Web ID トークンを検証できない | InvalidIdentityTokenException | API 定義 |
| MalformedPolicyDocument | 400 | セッションポリシーの JSON が正しくない | MalformedPolicyDocumentException | API 定義 |
| PackedPolicyTooLarge | 400 | セッションポリシーとタグの合計が大きい | PackedPolicyTooLargeException | API 定義 |
| RegionDisabledException | 403 | そのリージョンで STS が有効になっていない | RegionDisabledException | API 定義 |
| ThrottlingException | 400 | リクエストの頻度が高い | なし | 公式 |
| ServiceUnavailable | 503 | 一時的に利用できない | なし | 公式 |
各サービスの全コードは、公式ドキュメントが案内しているとおり、サービスごとの API リファレンスにあるエラーの節で確認できます。
botocore.exceptionsの発生条件一覧
ここからは、AWS に届く前後に手元で起きるエラーです。認証情報もリージョンも設定していない状態を作り、順に起こしました。
# botocore_errors.py
import boto3
import botocore.exceptions as be
from botocore.config import Config
def show(label, func):
try:
func()
except be.ClientError as e:
print(f"[{label}] ClientError: {e}")
except be.BotoCoreError as e:
parents = [c.__name__ for c in type(e).__mro__[1:] if c.__module__ == "botocore.exceptions"]
print(f"[{label}] {type(e).__name__}(親: {' > '.join(parents)})\n {str(e).strip()[:110]}")
fast = Config(connect_timeout=1, read_timeout=1, retries={"total_max_attempts": 1})
show("引数の型が違う", lambda: boto3.client("s3", region_name="ap-northeast-1").list_objects_v2(Bucket="rjc-demo-bucket", MaxKeys="ten"))
show("必須の引数が無い", lambda: boto3.client("s3", region_name="ap-northeast-1").get_object(Bucket="rjc-demo-bucket"))
show("認証情報が無い", lambda: boto3.client("s3", region_name="ap-northeast-1").list_buckets())
show("認証情報が片方だけ", lambda: boto3.client("s3", region_name="ap-northeast-1", aws_access_key_id="AKIAEXAMPLE").list_buckets())
show("リージョンが無い", lambda: boto3.client("dynamodb"))
show("profile が無い", lambda: boto3.Session(profile_name="nothing"))
show("サービス名が違う", lambda: boto3.client("s33", region_name="ap-northeast-1"))
show("接続先に届かない", lambda: boto3.client(
"s3", region_name="ap-northeast-1", endpoint_url="http://localhost:9219",
aws_access_key_id="x", aws_secret_access_key="x", config=fast).list_buckets())
show("ページ分割に非対応の操作", lambda: boto3.client("s3", region_name="ap-northeast-1").get_paginator("get_object"))
show("retries の指定が違う", lambda: boto3.client("s3", region_name="ap-northeast-1", config=Config(retries={"mode": "fast"})))
設定を空にするため、次のように実行しています。
env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_DEFAULT_REGION \
AWS_CONFIG_FILE=./empty AWS_SHARED_CREDENTIALS_FILE=./empty \
AWS_EC2_METADATA_DISABLED=true python3 botocore_errors.py
[引数の型が違う] ParamValidationError(親: BotoCoreError)
Parameter validation failed:
Invalid type for parameter MaxKeys, value: ten, type: <class 'str'>, valid types:
[必須の引数が無い] ParamValidationError(親: BotoCoreError)
Parameter validation failed:
Missing required parameter in input: "Key"
[認証情報が無い] NoCredentialsError(親: BotoCoreError)
Unable to locate credentials
[認証情報が片方だけ] PartialCredentialsError(親: BotoCoreError)
Partial credentials found in explicit, missing: aws_secret_access_key
[リージョンが無い] NoRegionError(親: BaseEndpointResolverError > BotoCoreError)
You must specify a region.
[profile が無い] ProfileNotFound(親: BotoCoreError)
The config profile (nothing) could not be found
[サービス名が違う] UnknownServiceError(親: DataNotFoundError > BotoCoreError)
Unknown service: 's33'. Valid service names are: accessanalyzer, account, account-access, acm, acm-pca, agent-
[接続先に届かない] EndpointConnectionError(親: ConnectionError > BotoCoreError)
Could not connect to the endpoint URL: "http://localhost:9219/"
[ページ分割に非対応の操作] OperationNotPageableError(親: BotoCoreError)
Operation cannot be paginated: get_object
[retries の指定が違う] InvalidRetryModeError(親: InvalidRetryConfigurationError > BotoCoreError)
Invalid value provided to "mode": "fast" must be one of: ('legacy', 'standard', 'adaptive')
待機(waiter)が時間内に終わらないときは WaiterError です。
# waiter_error.py
import boto3
from botocore.exceptions import WaiterError
s3 = boto3.client("s3")
try:
s3.get_waiter("bucket_exists").wait(
Bucket="rjc-none-bucket", WaiterConfig={"Delay": 1, "MaxAttempts": 3})
except WaiterError as e:
print(type(e).__name__, ":", e)
print("最後の応答の Code:", e.last_response["Error"]["Code"])
WaiterError : Waiter BucketExists failed: Max attempts exceeded. Previously accepted state: Matched expected HTTP status code: 404
最後の応答の Code: 404
| 例外 | 起きるタイミング | 発生条件 | 対処の方向 | 確認 |
|---|---|---|---|---|
| ParamValidationError | 送信前 | 引数の型が違う、必須の引数が無い、長さが範囲外 | コードを直す(再試行しても同じ) | 実行 |
| NoCredentialsError | 送信前 | どの探索先にも認証情報が無い | 環境変数・profile・ロールを設定 | 実行 |
| PartialCredentialsError | 送信前 | アクセスキーとシークレットキーの片方だけ | 両方をそろえる | 実行 |
| NoRegionError | クライアント作成時 | リージョンがどこにも指定されていない | region_name か AWS_DEFAULT_REGION | 実行 |
| ProfileNotFound | Session 作成時 | 指定した profile が設定ファイルに無い | profile 名と設定ファイルの場所を確認 | 実行 |
| UnknownServiceError | クライアント作成時 | サービス名の綴りが違う | メッセージ内の一覧から正しい名前を選ぶ | 実行 |
| EndpointConnectionError | 送信時 | 接続先に届かない(名前解決・ネットワーク・接続先の指定) | ネットワークとリージョン名を確認 | 実行 |
| ReadTimeoutError | 応答待ち | read_timeout の時間内に応答が無い | タイムアウトと処理量を見直す | 実行(後述) |
| WaiterError | 待機中 | MaxAttempts 回確認しても目的の状態にならない | Delay・MaxAttempts を調整 | 実行 |
| OperationNotPageableError | paginator 取得時 | ページ分割に対応していない操作を指定 | 対応する操作を使う | 実行 |
| InvalidRetryModeError | Config 作成時 | retries の mode が 3 種類以外 | legacy・standard・adaptive から選ぶ | 実行 |
botocore.exceptions に定義されているクラスは、公式ドキュメントにある次のコードで一覧できます(この環境では 101 個でした)。
import botocore.exceptions
for key, value in sorted(botocore.exceptions.__dict__.items()):
if isinstance(value, type):
print(key)
まとめて受ける定型
import boto3
from botocore.exceptions import BotoCoreError, ClientError, ParamValidationError
s3 = boto3.client("s3")
def fetch(bucket, key):
try:
return s3.get_object(Bucket=bucket, Key=key)["Body"].read()
except s3.exceptions.NoSuchKey:
return None # 想定内:無ければ None
except ClientError as e: # AWS が返したエラー
err, meta = e.response["Error"], e.response["ResponseMetadata"]
print(f"AWS error op={e.operation_name} code={err['Code']} "
f"http={meta['HTTPStatusCode']} request_id={meta['RequestId']}")
raise
except ParamValidationError as e: # 引数の誤り
raise ValueError(f"引数を確認してください: {e}") from e
except BotoCoreError as e: # 認証情報・接続など
print(f"client-side error {type(e).__name__}: {e}")
raise
print(fetch("rjc-demo-bucket", "report.csv"))
None
リトライ設定:legacy・standard・adaptiveの比較
boto3 は、一時的なエラーを自動で再送します。挙動は Config の retries で決めます。
import boto3
from botocore.config import Config
config = Config(retries={"mode": "standard", "total_max_attempts": 5})
sqs = boto3.client("sqs", config=config)
公式ドキュメント(Retries)の内容を表にします。
| 比べる点 | legacy | standard | adaptive |
|---|---|---|---|
| 位置づけ | 指定しないときの既定 | 各言語の SDK で共通化されたルール | standard + クライアント側の流量調整(公式では試験的な扱い) |
| 既定の最大試行回数(初回を含む) | 5 | 3 | 3 |
| 再送する HTTP ステータス | 429、500、502、503、504、509 | 500、502、503、504 | standard と同じ |
| 再送するエラーコード | Throttling、ThrottlingException、ThrottledException、RequestThrottledException、ProvisionedThroughputExceededException | 左に加えて RequestTimeout、PriorRequestNotComplete、TooManyRequestsException、TransactionInProgressException、RequestLimitExceeded、LimitExceededException、SlowDown など | standard と同じ |
| 再送する接続系のエラー | ConnectionError、ConnectionClosedError、ReadTimeoutError、EndpointConnectionError | ConnectionError、HTTPClientError | standard と同じ |
| 待ち時間 | 2 を底にした指数バックオフ | 同じ(最大 20 秒) | 同じ + 送信ペースの調整 |
| そのほか | ― | 障害が続くときに再送を控える仕組み | エラーの応答に応じて送信レートを更新 |
設定できる場所は 3 つです。
| 場所 | 書き方 | max_attempts の意味 |
|---|---|---|
| 設定ファイル(~/.aws/config) | retry_mode = standard と max_attempts = 5 |
初回を含む総回数 |
| 環境変数 | AWS_RETRY_MODE と AWS_MAX_ATTEMPTS | 初回を含む総回数 |
| Config オブジェクト | retries={"mode": ..., "max_attempts": N} |
再送の回数(初回を含まない) |
| Config オブジェクト | retries={"mode": ..., "total_max_attempts": N} |
初回を含む総回数 |
Config の max_attempts だけ意味が違うので、コードでは total_max_attempts を使うと、設定ファイルや環境変数と数え方がそろいます。
何回送られるかを数える
実際の AWS で 503 やスロットリングを狙って起こすことは難しいので、決まったエラー応答を返す小さな HTTP サーバーを立て、そこへ SQS の ListQueues を送って回数を数えます。次の 2 ファイルを同じフォルダーに置きます(このサーバーは回数を数えるためのもので、接続先を endpoint_url で切り替えています)。
# stub_server.py
"""決まった応答を返す検証用の HTTP サーバー(SQS の ListQueues 向け)"""
import json
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
RESPONSES = {
"503": (503, {"__type": "ServiceUnavailable", "message": "Service Unavailable"}),
"throttle": (400, {"__type": "ThrottlingException", "message": "Rate exceeded"}),
"denied": (403, {"__type": "AccessDeniedException", "message": "not authorized"}),
"invalid": (400, {"__type": "InvalidParameterValue", "message": "invalid value"}),
}
state = {"mode": "503", "fail_times": 999, "requests": 0}
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def do_POST(self):
self.rfile.read(int(self.headers.get("Content-Length", 0)))
state["requests"] += 1
if state["requests"] <= state["fail_times"]:
if state["mode"] == "slow":
time.sleep(3)
status, body = 200, {"QueueUrls": []}
else:
status, body = RESPONSES[state["mode"]]
else:
status, body = 200, {"QueueUrls": []}
data = json.dumps(body).encode()
try:
self.send_response(status)
self.send_header("Content-Type", "application/x-amz-json-1.0")
self.send_header("x-amzn-RequestId", f"req-{state['requests']:04d}")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
except OSError:
pass # クライアントが先に切った場合
def start(port=9211):
server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
return f"http://127.0.0.1:{port}"
def reset(mode, fail_times=999):
state.update(mode=mode, fail_times=fail_times, requests=0)
# retry_lab.py
import time
import boto3
from botocore.config import Config
from botocore.exceptions import BotoCoreError, ClientError
import stub_server
URL = stub_server.start()
def run(label, retries, mode, fail_times=999):
stub_server.reset(mode, fail_times)
sqs = boto3.client(
"sqs", endpoint_url=URL, region_name="ap-northeast-1",
aws_access_key_id="x", aws_secret_access_key="x",
config=Config(retries=retries) if retries else None)
started = time.time()
try:
meta = sqs.list_queues()["ResponseMetadata"]
result = f"成功 RetryAttempts={meta['RetryAttempts']}"
except ClientError as e:
meta = e.response["ResponseMetadata"]
result = f"{e.response['Error']['Code']} RetryAttempts={meta['RetryAttempts']}"
except BotoCoreError as e:
result = type(e).__name__
print(f"{label:32} 送信 {stub_server.state['requests']} 回 {time.time() - started:4.1f} 秒 {result}")
print("== 503 を返し続ける")
run("指定なし", None, "503")
for mode in ["legacy", "standard", "adaptive"]:
run(f"mode={mode}", {"mode": mode}, "503")
print("== ThrottlingException(HTTP 400)を返し続ける")
for mode in ["legacy", "standard", "adaptive"]:
run(f"mode={mode}", {"mode": mode}, "throttle")
print("== リトライされないエラー")
for mode in ["legacy", "standard"]:
run(f"mode={mode} AccessDenied", {"mode": mode}, "denied")
run(f"mode={mode} InvalidParameter", {"mode": mode}, "invalid")
print("== max_attempts と total_max_attempts")
run("standard max_attempts=3", {"mode": "standard", "max_attempts": 3}, "503")
run("standard total_max_attempts=3", {"mode": "standard", "total_max_attempts": 3}, "503")
run("standard total_max_attempts=1", {"mode": "standard", "total_max_attempts": 1}, "503")
print("== 2 回エラーのあと成功する")
run("standard total_max_attempts=5", {"mode": "standard", "total_max_attempts": 5}, "503", fail_times=2)
== 503 を返し続ける
指定なし 送信 5 回 8.7 秒 ServiceUnavailable RetryAttempts=4
mode=legacy 送信 5 回 8.1 秒 ServiceUnavailable RetryAttempts=4
mode=standard 送信 3 回 1.4 秒 ServiceUnavailable RetryAttempts=2
mode=adaptive 送信 3 回 1.8 秒 ServiceUnavailable RetryAttempts=2
== ThrottlingException(HTTP 400)を返し続ける
mode=legacy 送信 5 回 3.8 秒 ThrottlingException RetryAttempts=4
mode=standard 送信 3 回 0.9 秒 ThrottlingException RetryAttempts=2
mode=adaptive 送信 3 回 4.0 秒 ThrottlingException RetryAttempts=2
== リトライされないエラー
mode=legacy AccessDenied 送信 1 回 0.0 秒 AccessDeniedException RetryAttempts=0
mode=legacy InvalidParameter 送信 1 回 0.0 秒 InvalidParameterValue RetryAttempts=0
mode=standard AccessDenied 送信 1 回 0.0 秒 AccessDeniedException RetryAttempts=0
mode=standard InvalidParameter 送信 1 回 0.0 秒 InvalidParameterValue RetryAttempts=0
== max_attempts と total_max_attempts
standard max_attempts=3 送信 4 回 0.7 秒 ServiceUnavailable RetryAttempts=3
standard total_max_attempts=3 送信 3 回 1.6 秒 ServiceUnavailable RetryAttempts=2
standard total_max_attempts=1 送信 1 回 0.0 秒 ServiceUnavailable RetryAttempts=0
== 2 回エラーのあと成功する
standard total_max_attempts=5 送信 3 回 1.6 秒 成功 RetryAttempts=2
読み取れることを整理します。
| 確かめたこと | 結果 |
|---|---|
| 指定なしの挙動 | legacy と同じ 5 回(初回 1 + 再送 4) |
| standard・adaptive の既定 | 3 回(初回 1 + 再送 2) |
| HTTP 400 でも ThrottlingException は再送される | 5 回/3 回 |
| AccessDeniedException・InvalidParameterValue | 1 回だけ(再送しても結果が変わらないエラーは再送されない) |
| Config の max_attempts=3 | 4 回送られる(再送が 3 回) |
| Config の total_max_attempts=3 | 3 回 |
| total_max_attempts=1 | 自動の再送なし |
| 途中で成功した場合 | 成功した応答の RetryAttempts に、それまでの再送回数(2)が入る |
かかった秒数は、待ち時間に乱数が入るため実行のたびに変わります。
リトライされるエラー・されないエラー
| 種類 | 例 | SDK の自動リトライ | 自分で書く対処 |
|---|---|---|---|
| スロットリング | ThrottlingException、ProvisionedThroughputExceededException、SlowDown | される | 回数を増やす、呼び出しの頻度を下げる |
| サービス側の一時的なエラー | HTTP 500・502・503・504 | される | 総回数とタイムアウトを決めておく |
| 接続・タイムアウト | ConnectionError、ReadTimeoutError | される | connect_timeout・read_timeout を明示 |
| 権限・認証 | AccessDenied、ExpiredToken | されない | 権限や認証情報を直す |
| 入力の誤り | ValidationException、ParamValidationError | されない | コードを直す |
| 対象が無い・状態が合わない | NoSuchKey、ResourceNotFoundException、ConditionalCheckFailedException | されない | 業務上の分岐として扱う |
タイムアウトとリトライは掛け算になる
応答に 3 秒かかるサーバーへ、read_timeout=1 で送ります。
# timeout_demo.py
import time
import boto3
from botocore.config import Config
from botocore.exceptions import ReadTimeoutError
import stub_server
URL = stub_server.start()
for label, retries in [("total_max_attempts=3", 3), ("total_max_attempts=1", 1)]:
stub_server.reset("slow") # 応答まで 3 秒かかる
sqs = boto3.client(
"sqs", endpoint_url=URL, region_name="ap-northeast-1",
aws_access_key_id="x", aws_secret_access_key="x",
config=Config(connect_timeout=2, read_timeout=1,
retries={"mode": "standard", "total_max_attempts": retries}))
started = time.time()
try:
sqs.list_queues()
except ReadTimeoutError as e:
print(f"{label}: 送信 {stub_server.state['requests']} 回 {time.time() - started:.1f} 秒")
print(" ", type(e).__name__, ":", e)
total_max_attempts=3: 送信 3 回 4.0 秒
ReadTimeoutError : Read timeout on endpoint URL: "http://127.0.0.1:9211/"
total_max_attempts=1: 送信 1 回 1.0 秒
ReadTimeoutError : Read timeout on endpoint URL: "http://127.0.0.1:9211/"
1 回の呼び出しが返るまでの最長時間は、おおよそ「タイムアウト × 総回数 + 待ち時間の合計」になります。read_timeout の既定は 60 秒なので、画面の応答や Lambda の実行時間に上限がある処理では、Config でタイムアウトと総回数の両方を決めておくと見積もりやすくなります。
自前バックオフとの二重リトライに注意
SDK の自動リトライの外側に自前のリトライを書くと、回数は掛け算になります。
# double_retry.py
import time
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
import stub_server
URL = stub_server.start()
RETRYABLE = {"ThrottlingException", "ServiceUnavailable"}
def make_client(**retries):
return boto3.client(
"sqs", endpoint_url=URL, region_name="ap-northeast-1",
aws_access_key_id="x", aws_secret_access_key="x",
config=Config(retries=retries))
def call_with_backoff(client, tries=3, base=0.2):
"""自前の指数バックオフ(リトライしてよいコードだけ)"""
for attempt in range(1, tries + 1):
try:
return client.list_queues()
except ClientError as e:
if e.response["Error"]["Code"] not in RETRYABLE or attempt == tries:
raise
time.sleep(base * 2 ** (attempt - 1))
for label, client in [
("SDK 3 回 × 自前 3 回", make_client(mode="standard", total_max_attempts=3)),
("SDK 1 回 × 自前 3 回", make_client(mode="standard", total_max_attempts=1)),
]:
stub_server.reset("throttle")
started = time.time()
try:
call_with_backoff(client)
except ClientError as e:
print(f"{label}: 送信 {stub_server.state['requests']} 回 "
f"{time.time() - started:.1f} 秒 -> {e.response['Error']['Code']}")
SDK 3 回 × 自前 3 回: 送信 9 回 4.1 秒 -> ThrottlingException
SDK 1 回 × 自前 3 回: 送信 3 回 0.6 秒 -> ThrottlingException
SDK が 3 回送るクライアントを自前のループで 3 回呼ぶと、合計 9 回送られました。スロットリング中のサービスに送る回数が増えると、回復までの時間も延びやすくなります。
| 方針 | 設定 | 向いている場面 |
|---|---|---|
| SDK に任せる(基本) | retries={"mode": "standard", "total_max_attempts": 5} のように回数だけ調整し、自前のループは書かない |
ほとんどの API 呼び出し |
| 自前で制御する | SDK 側を total_max_attempts=1 にして、自前のループで回数と待ち時間を決める | 待ち時間の上限や、再試行のたびのログ出力を細かく決めたい |
| 両方を使う | 自前のループは「SDK が再送しないが、業務上は待てば解決するもの」だけを対象にする | 作成直後のリソースが見えるまで待つ、など |
DynamoDB の batch_write_item が返す UnprocessedItems のように、エラーにならずに「残り」が返る API は SDK の自動リトライの対象外です。公式ドキュメントは、残りを送り直すときに指数バックオフを使うことを強く勧めています。
よくある質問
Q. except Exception でまとめて受けてもよいですか? A. 動きますが、原因の切り分けがしにくくなります。ClientError(AWS が返したエラー)と BotoCoreError(手元のエラー)を分けて受け、ClientError は Error.Code をログに残す形がおすすめです。
Q. エラーコードの文字列はどこで調べられますか?
A. 各サービスの API リファレンスのエラーの節です。手元では、一度エラーを起こして e.response["Error"]["Code"] を表示するのが確実です。クラス名とコードが違うサービス(IAM・SQS など)があります。
Q. リトライが本当に行われたかを確かめるには?
A. 応答(成功時も例外時も)の ResponseMetadata["RetryAttempts"] を見ます。公式ドキュメントによると、DEBUG ログにも再送の記録が出ます(legacy は botocore.retryhandler、standard・adaptive は botocore.retries.standard)。
Q. resource を使っているときは、どう捕まえますか?
A. resource.meta.client.exceptions.XXX か、client のときと同じ ClientError + コード判定で捕まえます。
まとめ
- boto3 のエラーは ClientError(AWS が返した)と BotoCoreError(手元で起きた)の 2 系統で、親子関係はありません。
- ClientError は e.response["Error"]["Code"] で分岐し、RequestId・HTTPStatusCode・RetryAttempts をログに残します。
- client.exceptions.XXX は API 定義にあるエラーだけです。"404" や BucketNotEmpty のように、ClientError でしか受けられないコードがあります。
- クラス名とコードが違うサービスがあるので、コードは実際に出力して確かめます。
- リトライは指定なしだと legacy の 5 回、standard・adaptive は 3 回です。回数は total_max_attempts で指定すると数え方がそろいます。
- スロットリング・5xx・接続系は自動で再送され、権限・入力の誤り・対象なしは再送されません。
- 自前のバックオフを重ねると回数は掛け算になります。SDK に任せるか、SDK 側を 1 回にして自前で制御するかを決めます。
参考・出典
- Boto3 ドキュメント:Error handling
- Boto3 ドキュメント:Retries
- Amazon S3 API リファレンス:Error responses
- Amazon DynamoDB 開発者ガイド:Error handling with DynamoDB
- AWS STS API リファレンス:Common Errors
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「boto3のエラーハンドリング|ClientErrorのコード取得とリトライ設定」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。