Flaskの例外処理の方針を考える

## エラーハンドリングのベストプラクティスを考える
天下のGoogle様の[記事](https://developers.google.com/tech-writing/error-messages/error-handling)を参考にしてエラーハンドリングのベストプラクティスを考えます。

### 1. Don't fail silently

アプリが何のエラーも報告せずに突然終了してしまうのは、ユーザーにとっても開発側にとっても大きなストレスとなるでしょう。エラーが発生した場合は適切に報告し、何が起きたのか、原因が何なのかを明確に伝えることが重要です。
エラーは完全に防ぐことはできないものと認識し、エラーメッセージをソフトウェア設計時に計画的に組み込むことが求められます。

---
### 2. プログラミング言語のガイドラインに従う
Googleが[スタイルガイド(python)](https://google.github.io/styleguide/pyguide.html)を公開しているため、そちらを参照しましょう。flaskに関して言えば[公式ドキュメント](https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/index.html)も参照します。
エラーをハンドリングするだけではなく、そもそもエラーを引き起こさない設計やコーディングを優先するも重要ですね。

Googleは他の言語についてもスタイルガイドを公開しています。ご参考までに；
- [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html)
- [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
- [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html)
- [Google Go Style Guide](https://google.github.io/styleguide/go/index.html)
---
### 3. 完全なエラーモデルを実装する
エラーモデルについては、[GoogleのAIP（API Improvement Proposals)](https://cloud.google.com/apis/design/errors)で定義されている方法で実装することが推奨されています。
まとめると以下のような観点があります(適切なものだけを抜粋)

- [ ] **3-1.統一されたエラーモデル**:
今回で言うと、アプリ内で統一されたモデルに従うこと
- [ ] **3-2.エラー構造**:
  - **`code`**: クライアントが処理可能なエラーコード
  - **`message`**: 開発者向けの英語のエラーメッセージ。エラー説明と解決策を含む
  - **`details`**: クライアントがエラーを処理するための追加情報
- [ ] **3-3.標準エラーコードの使用**:
既存の例外を使用し、新たなエラーコードの追加は極力避ける。他の記事でもみましたが、「運用に則って不便があれば」適宜実装する程度で良いかもしれません。
- [ ] **3-4.エラーメッセージの設計**:
    - 簡潔で、非エキスパートユーザーにも理解可能
    - 必要に応じてリンクや `details` フィールドを使用して追加情報を提供
    - 再試行のガイドラインを書く
- [ ] **3-5エラーの伝搬**:
   - 実装詳細や機密情報は非公開にする
---
### 4. 根本原因を隠さない
「サーバーエラー」のような曖昧なメッセージではなく、具体的な原因を特定するためのコンテキストを提供する。当たり前ですが大事ですね。
例: サービスの失敗、ネットワーク切断、ステータスの不一致、権限の問題など。

---
### 5. エラーコードを記録する
カスタマーサポートやエンジニアがエラーを監視・診断するのに役立つため、ログで記録を残します。エラーコードのリストを文書化しておくのも良いでしょう。

---
### 6. エラーは即座に報告する
デバッグコストを軽減できるため、エラーは早期に報告するようにしましょう。また報告するだけではなく、それに気づく必要もあるので外部サービスに通知することも必要でしょう。

---
念の為、他のサイトでもベストプラクティスを調べましたが、上記の項目でカバーできていました。
調べたサイトについては参考文献で書いています。

## Flaskでどのように実装するか
さて、ベストプラクティスに基づいてどのような実装にするか考えてみます。

### エラーモデルの定義
```python
# カスタムエラーの定義

class PermissionError(Exception):
    status_code = 400
    message = "十分な権限がありません。"
    
    def __init__(self, payload=None):
        super().__init__()
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['message'] = self.message
        return rv

# 使用
def update(self, updatee: User, updater: User) -> None:
        if not updater.is_editable(updatee):
            raise PermissionError({
                "detail": f"あなたは{updatee.name}への編集権限がありません。", 
                "suggestion": "適切な権限に切り替えてください。"
            })
...
```
ポイント：
- 標準的なエラーは`Werkzeug`でカバーされているため、そちらを使うかオーバーライドしても良いかもしれません
- `detail`詳細や`suggestion`次のアクションに言及

### エラーの登録
```python
@app.errorhandler(PermissionError)
def invalid_api_usage(e):
    return jsonify(e.to_dict()), e.status_code
```
ポイント：
- flaskの`errorhandler`デコレーターを使います
- エラー後の処理はこちらに書くこと。細かいエラーはドメイン層に書かない
- 返り値はカスタムテンプレートなども返せます

### メールでの通知
```python
from flask import has_request_context, request
from flask.logging import default_handler
import logging
from logging.handlers import SMTPHandler

# メールハンドラの設定
mail_handler = SMTPHandler(
    mailhost='127.0.0.1',
    fromaddr='server-error@example.com',
    toaddrs=['mochi@sakura.com'],
    subject='アプケーションエラー'
)
mail_handler.setLevel(logging.ERROR)

class RequestFormatter(logging.Formatter):
    def format(self, record):
        if has_request_context():
            record.url = request.url
            record.remote_addr = request.remote_addr
        else:
            record.url = None
            record.remote_addr = None

        return super().format(record)

formatter = RequestFormatter(
    '[%(asctime)s] %(remote_addr)s requested %(url)s\n'
    '%(levelname)s in %(module)s: %(message)s'
)

default_handler.setFormatter(formatter)
mail_handler.setFormatter(formatter)


if not app.debug:
    app.logger.addHandler(mail_handler)
```
- ローカルにメールサーバーがある想定です
- `RequestFormatter`をオーバーライドして、詳細な情報を提示します
## 参考文献
- [General error handling rules  |  Technical Writing  |  Google for Developers](https://developers.google.com/tech-writing/error-messages/error-handling)
- [アプリケーションのエラーの処理 — Flask Documentation (2.2.x)](https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/errorhandling.html)
- [Error Handling Best Practices](https://auth0.com/docs/troubleshoot/error-handling-best-practices)
- [Best Practices for API Error Handling | Postman Blog](https://blog.postman.com/best-practices-for-api-error-handling/)
- [凄腕エンジニアさんから学んだ例外の話 #error - Qiita](https://qiita.com/TairaNozawa/items/ab878eaa0b9f64b9854f)