プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

FastAPI でセッションを扱う方法

FastAPI でセッションを扱う方法を調べてみました。 SessionMiddleware というのを使えば、簡単に使うことができるようです。

FastAPIでのHTTPセッション管理について:(Google OAuth 2.0実装例付き) | 知と富の交差点

この記事にも書かれていますが、 SessionMiddleware は署名付きクッキーを使ってセッションを管理してくれるようです。

準備

まずは必要なパッケージをインストールしておきます。

pip install fastapi uvicorn python-dotenv itsdangerous

なお、itsdangerous はサンプルファイルから直接呼んでいないのですが、 SessionMiddleware を使うと内部で使っているようで、必要になります。

設定方法

SessionMiddleware を使うには、

# FastAPI アプリケーションの作成
app = FastAPI()

app.add_middleware(
    SessionMiddleware,
    secret_key=_FASTAPI_SESSION_SECRET_KEY,
    max_age=(int)(datetime.timedelta(minutes=_SESSION_TIMEOUT_MIN).total_seconds()),
)

のようにすれば OK です。ここで、 _FASTAPI_SESSION_SECRET_KEY はセッション情報の署名のための秘密鍵になります。今回の場合は、 .env ファイルから読み込むようにしました。

# 環境変数の読み込み
#   秘密鍵 (セッション管理用)が必要
#   .env ファイルから取得する

load_dotenv()
_FASTAPI_SESSION_SECRET_KEY = os.environ.get("FASTAPI_SESSION_SECRET_KEY")
if not _FASTAPI_SESSION_SECRET_KEY:
    raise ValueError("FASTAPI_SESSION_SECRET_KEY is not set in environment variables.")

_SESSION_TIMEOUT_MIN = 10  # セッション有効期限, 分単位

.env ファイルは

FASTAPI_SESSION_SECRET_KEY = "session_secret_key"

こんな感じです。

セッションの使い方

ネットのサンプルだと認証に絡んだものが多い印象があるので、ここでは何かの API にアクセスした際にセッション情報があれば、それを返し、なければ新規にセッション情報を作成するようなケースを考えます。

まず、この API は POST でアクセスすると想定して、POSTのペイロードを定義しておきます。

# POST のペイロードを定義
class PostPayload(BaseModel):
    question: str = Query(
        min_length=1,
        max_length=1000,
        description="The question to query the FAQ system.",
    )

このあたりは詳しくは FastAPI のドキュメントなどを見てください。

次に API を定義します。

# Depends を使った API
@app.post("/query/")
async def query(payload: PostPayload, session: dict = Depends(_get_session)):
    print(f"session is: {session}")

    # レスポンスの整形
    result = {
        "session_id": session["session_id"],
        "count": session["count"],
        "query": str(payload.question),
    }
    return result

ここで、 _get_session という関数を依存性注入という仕組みを使って渡しています。この関数は、

def _get_session(request: Request):
    print(f"get_session, target is: {request.session}")
    if not request.session:
        request.session["session_id"] = str(uuid.uuid4())
        request.session["count"] = 0
    request.session["count"] += 1
    return request.session

という感じに定義してあり、セッションがなければ session_id を新規に作成し、既存のセッションがあればそれを利用するという処理を行います。また、わかりやすいように、 session_id ごとの呼び出し回数をカウントするようにしています。

テスト

これでテストしてみます。一連の処理を書いたファイルの最後に、

if __name__ == "__main__":
    import uvicorn

    # Start the FastAPI application
    uvicorn.run(app, host="127.0.0.1", port=8000)

と追記して、 VSCode から実行します。

こちらの記事でも触れたように、 FastAPI で定義した API を呼び出してテストするには VSCode の拡張機能である REST Client を使います。test.http というファイルを作成し、

POST http://localhost:8000/query/
Content-Type: application/json

{
    "question": "question 1"
}

のように記述しておきます。この時、『Send Request』と表示されているので、クリックするとコンソールに、

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
get_session, target is: {}
session is: {'session_id': '505185b0-3b07-487f-9591-ff0d56fc3390', 'count': 1}
INFO:     127.0.0.1:49634 - "POST /query/ HTTP/1.1" 200 OK

のように表示されます。初回のリクエストのため、セッションがないことがわかります。また、VSCode 上でウィンドウが開いて、

のように、応答が表示されます。ここで、 set-cookie ヘッダが送られていることも確認できます。

同じAPIを2回目呼び出すと、

get_session, target is: {'session_id': '505185b0-3b07-487f-9591-ff0d56fc3390', 'count': 1}
session is: {'session_id': '505185b0-3b07-487f-9591-ff0d56fc3390', 'count': 2}
INFO:     127.0.0.1:42480 - "POST /query/ HTTP/1.1" 200 OK

のようになり、 session_id は同じだけど、 count が加算されていることがわかります。

なお、REST Client の cookie はどうなっているかも見てみます。REST Client の cookie などの情報は、 ~/.rest-client にあります。

mor@DESKTOP-DE7IL4F:~/.rest-client$ ls -l
合計 20
-rw-r--r-- 1 mor mor  370  92 10:37 cookie.json
-rw-r--r-- 1 mor mor    0  63 12:51 environment.json
-rw-r--r-- 1 mor mor 9243  92 10:37 history.json
drwxr-xr-x 4 mor mor 4096  63 12:51 responses
mor@DESKTOP-DE7IL4F:~/.rest-client$

さきほどの1回目のリクエスト後に cookie を表示してみると

mor@DESKTOP-DE7IL4F:~/.rest-client$ cat cookie.json
{"localhost":{"/":{"session":{"key":"session","value":"eyJzZXNzaW9uX2lkIjogIjUwNTE4NWIwLTNiMDctNDg3Zi05NTkxLWZmMGQ1NmZjMzM5MCIsICJjb3VudCI6IDF9.aLZKUg.YpJ3eJiukxLAcyG3i1XIw24VNAM","maxAge":600,"domain":"localhost","path":"/","httpOnly":true,"extensions":["samesite=lax"],"hostOnly":true,"creation":"2025-09-02T01:37:22.546Z","lastAccessed":"2025-09-02T01:37:22.546Z"}}}}mor@DESKTOP-DE7IL4F:~/.rest-client$
mor@DESKTOP-DE7IL4F:~/.rest-client$

のようになっています。署名付き cookie なので当然内容はわかりませんが。

2回目のリクエストの後だと、

mor@DESKTOP-DE7IL4F:~/.rest-client$ cat cookie.json
{"localhost":{"/":{"session":{"key":"session","value":"eyJzZXNzaW9uX2lkIjogIjUwNTE4NWIwLTNiMDctNDg3Zi05NTkxLWZmMGQ1NmZjMzM5MCIsICJjb3VudCI6IDJ9.aLZLVg.Hvv8fAkOWQWDsyO2eZrLEiSJNoU","maxAge":600,"domain":"localhost","path":"/","httpOnly":true,"extensions":["samesite=lax"],"hostOnly":true,"creation":"2025-09-02T01:37:22.546Z","lastAccessed":"2025-09-02T01:41:42.730Z"}}}}mor@DESKTOP-DE7IL4F:~/.rest-client$
mor@DESKTOP-DE7IL4F:~/.rest-client$

のようになっています。value の値をよく見ると、1回目とは値が異なっています。

これは、多分、セッション情報を使って署名(ハッシュ操作?)を作っているため、 count の値が変わったことで、 cookie の設定値も変化したのだと思われます。

別の API の定義方法

さて、上記の方法は依存性注入というのを使ったのですが、別に無理にこれを使わなくてもセッション情報を扱うことができます。また、 SessionMiddleware を使わず、直接 cookie を操作することでセッション情報を扱うこともできます。

とうことで、これらも簡単に書いておきます。

方法2

SesionMiddleware を使うけど、 依存性注入は使わない方法です。

下記のドキュメントにあるように、POSTのペイロードと一緒に request オブジェクトを受け取ることができるので、これを元にセッションを操作できます。

Using the Request Directly - FastAPI

たとえば、こんな感じに定義できます。

# request で受け取る API
@app.post("/query2/")
async def query2(payload: PostPayload, request: Request):

    session: dict = _get_session(request)

    print(f"query2, session is: {session}")

    # レスポンスの整形
    result = {
        "session_id": session["session_id"],
        "count": session["count"],
        "query2": str(payload.question),
    }
    return result

このサンプルだと、 request を _get_session 関数に渡して処理していますが、 request.session を直接いろいろと操作することができます。

方法3

もっと簡単にやるには、 SessionMiddleware を使わず、直接 Cookie を操作することで実現できます。

今回、自分では試していませんが、例えば、下記の記事のようなやり方です。

🚀 FastAPI 実践入門:第三歩目で学ぶテンプレートとセッション管理

この場合、 cookie の中身を見ることができてしまうので、機密の高いものには使えないですかね。

セッションのクリア

あと、セッションのクリアをするには、最初の方法と方法2であれば、

# セッションのクリア
@app.get("/clear")
async def clear_session(request: Request):
    request.session.clear()
    return {"message": "Session cleared"}

のようにします。

方法3の cookie を直接使う場合は

@app.get("/delete")
async def delete_session(request: Request):
    response = JSONResponse(content={"message": "Session cookie deleted"})
    response.delete_cookie("session")
    return response

のように、cookieを削除する方法で実現できます。ただし、 SessionMiddleware を使う場合はこれだとうまくいきません(request オブジェクトに session が残っていると再度 set-cookie ヘッダが送られているみたいです)のでご注意を。

おまけ

今回のサンプルで使った python ファイルの全体を載せておきます。ご参考までに。

import datetime
import os
import uuid

from dotenv import load_dotenv
from fastapi import Depends, FastAPI, Query, Request
from pydantic import BaseModel
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import JSONResponse

"""
FastAPI でセッションを使うためのサンプル

SessionMiddleware を使って、セッションを扱う。
セッションは署名付きクッキーを使う。
署名は秘密鍵方式となる(このため、秘密鍵を指定する必要がある)。
"""

# 環境変数の読み込み
#   秘密鍵 (セッション管理用)が必要
#   .env ファイルから取得する

load_dotenv()
_FASTAPI_SESSION_SECRET_KEY = os.environ.get("FASTAPI_SESSION_SECRET_KEY")
if not _FASTAPI_SESSION_SECRET_KEY:
    raise ValueError("FASTAPI_SESSION_SECRET_KEY is not set in environment variables.")

_SESSION_TIMEOUT_MIN = 10  # セッション有効期限, 分単位


# POST のペイロードを定義
class PostPayload(BaseModel):
    question: str = Query(
        min_length=1,
        max_length=1000,
        description="The question to query the FAQ system.",
    )


# FastAPI アプリケーションの作成
app = FastAPI()

app.add_middleware(
    SessionMiddleware,
    secret_key=_FASTAPI_SESSION_SECRET_KEY,
    max_age=(int)(datetime.timedelta(minutes=_SESSION_TIMEOUT_MIN).total_seconds()),
)


def _get_session(request: Request):
    print(f"get_session, target is: {request.session}")
    if not request.session:
        request.session["session_id"] = str(uuid.uuid4())
        request.session["count"] = 0
    request.session["count"] += 1
    return request.session


# 問い合わせ エンドポイント


# Depends を使った API
@app.post("/query/")
async def query(payload: PostPayload, session: dict = Depends(_get_session)):
    print(f"session is: {session}")

    # レスポンスの整形
    result = {
        "session_id": session["session_id"],
        "count": session["count"],
        "query": str(payload.question),
    }
    return result


# request で受け取る API
@app.post("/query2/")
async def query2(payload: PostPayload, request: Request):

    session: dict = _get_session(request)

    print(f"query2, session is: {session}")

    # レスポンスの整形
    result = {
        "session_id": session["session_id"],
        "count": session["count"],
        "query2": str(payload.question),
    }
    return result


# セッションのクリア
@app.get("/clear")
async def clear_session(request: Request):
    request.session.clear()
    return {"message": "Session cleared"}


@app.get("/delete")
async def delete_session(request: Request):
    # これだとうまくセッションが削除できない
    #
    # たぶん、 delete_cookie でクッキーを削除するが、
    # request オブジェクトに残っているセッション情報があるため、
    # 自動的にセッションがもう一度設定されるためと思われる
    response = JSONResponse(content={"message": "Session cookie deleted"})
    response.delete_cookie("session")
    return response


if __name__ == "__main__":
    import uvicorn

    # Start the FastAPI application
    uvicorn.run(app, host="127.0.0.1", port=8000)

ファミリーリンク:アプリのアクティビティが正しく反映されない

子どもが夏休み中、毎日のようにゲームにいそしんでいます。

そんなある日の午前中、子どもから「ゲームやってないのに、利用時間が上限になってる。おかしい。ゲームできない。なんとかして」と訴えがありました。

その時、子どもが送ってきたファミリーリンクのスクリーンショットが、

でした。

最初の画像にある、個別のアプリのアクティビティ(使用時間)は確かにタイムアップとなってます。一方、後者の画像を見ると、端末自体の使用時間はほぼゼロです。この日に端末を使ってないことは間違いなさそうです。

親側のファミリーリンクも見てみます。

親側の端末の使用時間も1分となっており、大きくずれてはいません。一方、アプリの使用時間はタイムアップとなっていません。この連絡は LINE で来ていたので、 LINE の使用時間が1分未満となっているのも正しい数字でしょう。

子ども曰く、この使用時間は前の日に使っていたものがそのまま表示されいる、とのことです。なんか変だぞ。

ということで、このトラブルを調べてみたので、メモっておきます。

応急処置

さて、とりあえずスマホを使いたい子どものリクエストに急ぎこたえるために、正しく使用状況を反映できないかいくつか試してみました。

まずは、子どものスマホを再起動させてみます。状況は変わらずです。

次に、タイムアップとなっているアプリについて、親側の設定で一時的に使用時間制限を解除します。子供の端末のファミリーリンクを見ると、時間制限がなくなった旨表示されています。

その後、再度使用時間制限を有効にして、子どもの端末側の使用時間が正しく戻っているか確認すると、残念、さきほどと同じくタイムアップとなってます。なんでだ?

ということで、どうもすぐには改善しなさそうだったので、個別の使用時間を延ばすということで、この日はしのぎました。あー、めんど。

Google に問い合わせ

さて、最初にとのトラブルがあってから、数日後、また同じトラブルに見舞われました。これはやってられないということで、 Google に問い合わせします。

ちなみに、最近 Google の問い合わせを見つけるのがなんとなく難しくなった気がしていたのですが、 Google アカウントでログインしてヘルプを検索するとちゃんと

と問い合わせへのボタンが表示されていました。

早速聞いてみたところ、

お子様のデバイスからアプリのアクティビティが報告されていないようです。

と回答がありました。一緒に注意点として

お子様がお使いのデバイスからアプリのアクティビティを報告するには、インターネットに接続されている必要があります。

とあります。そういえば、このトラブルが起きたときの状況の子どもに聞いていたら、前の日の夜に電池がなくなったといってました。

どうもこれと関係がありそうですね。これは完全なる私の推測ですが、日をまたぐあたりでアプリのアクティビティの送信ができなかったことで、前日の使用時間がそのまま翌日にも反映されているっぽいですね。

このあたりは、サーバー側での、アプリのアクティビティの更新をどう扱っているかという処理に絡むところなので、実情は不明ですが、挙動だけからするとこういう動きっぽいです。

修正方法

再度アプリのアクティビティを正しく送るために、 Google からアドバイスされた方法としては、

  • 親の端末側で、アプリのアクティビティが有効になっていることを確認する
  • もし、既に有効であれば、一旦無効にする
  • アプリのアクティビティが正しく反映されていることを確認する(反映まで1~2時間かかる場合があるみたいです)
  • 親端末、子端末の両方で、 設定アプリからGoogle アカウントを選択して、『今すぐ同期』を実行する

というものでした。

一通り、これも試しておきます。これのおかげかどうかはわかりませんが、とりあえず今のところ正しくアプリのアクティビティが送られ、問題なく動いているようです。

まとめ

ファミリーリンク自体はそんなに悪い印象はないのですが、ときどき思いもよらぬ動作をするときがあり、困ることがあります。まあ、このあたりはうまく付き合っていくしかないんでしょうね。 この記事が何かの参考になれば幸いです。

aws でクレジットカードの支払ができなくて焦った話

2025 年 8 月のことです。それまで aws を使って料金を普通に支払っていたのですが、突然

というようなメールが届くようになりました。

これはなんだ?と思ってルートユーザーでログインして『請求とコスト管理』を表示して、『支払設定』を確認すると

となっています。どうも、クレジットカードが未検証となっているためのメールのようです。

このトラブルへ対応しようと思ったら、意外と手間取ったんで、せっかくなのでメモっておきます。

まずやったこと

まずは、先ほど表示されていたコンソール上のメッセージを見てみると『未検証』とでており、近くに『検証』ボタンもあります。

ということで、ボタンを押して検証してみると、さて何も起こりません。ただ、画面上のステータスから『未検証」がなくなっています。

なので、これでいいのかな?と思ってログアウトしていたら、後日、また同じ内容のメールが届いてました(コンソールを確認すると『未検証』も復活していました)。同じことを2回ほど繰り返したのですが、結果は変わりません。

さて、困りましたね。

サポートへ連絡

仕方ないので、サポートへ連絡してみました(サポートのレスポンスがめっちゃいいですね)。

で、状況を伝えて聞いてみると、

お客様のアカウントを確認いたしましたところ、8月1日 に 7月分ご利用料金 xxxxxx円 のクレジットカード決済が失敗しておりました。 ご登録のクレジットカードが何らかの問題でお支払いできなかったことにより、『支払方法が無効です』と表示されております。

とのことでした。カード決済に失敗したので支払ができていない、とのことです。このため、メールが送られてきたんですね、きっと。

ただ、先月まで問題なかったのが、なぜ決済に失敗したんですかね?これに関しては、サポートからの説明は、

なお、『支払方法が無効です』を解消するための決済失敗の理由はカード保有者の個人情報として、クレジットカード会社から当窓口へ共有されず調査を行うことができません。 お手数ですが、ご利用のクレジットカード会社へお問い合わせいただきますようお願いいたします。

とのことで、 aws 側ではわからないので、カード会社に聞いてくれとのことです。

カード会社へ連絡

気を取り直して、カード会社に聞いてみました。最近の会社さんだと、問い合わせ先がなかなかみつからない時があり、困りますよね。今回はカード会社のサービスにログインしてなんとか問い合わせ窓口を見つけました。

で、問い合わせした結果は、

国際ブランド側のセキュリティの観点よりご利用をお断りしております。 当社では制限解除ができませんため、他の国際ブランドのカードでご利用をお試しいただきますようお願いいたします。

とのことでした。国際ブランド(このカードは VISA との提携カードなんで、 VISA のことでしょうね)のセキュリティのため、決済に失敗したとのことだったのですが、具体的な理由はわからずじまいでした。カード利用の限度額も問題ないし、結局、原因はわからずじまいです。

クレジットカードの再登録

最初に aws のサポートに尋ねた際に、

ご登録クレジットカードの確認、またクレジットカード会社への決済失敗理由の確認をされても問題が解決されない場合、以下の対応もご検討ください。

・該当クレジットカードを再登録 ・別クレジットカードを登録

と連絡をもらってました。

ということで、この当該クレジットカードの再登録をおこなおうと思ったら、

となっており、支払方法の削除が選択できません。これについてよくよく調べてみると、

となっており、デフォルトの支払方法は削除できなくて、一度別の支払方法に切り替えてから削除する必要があるとのことです(サポートにも確認しました)。

まじか。。。

振込による支払

この時点でクレジットカードの再登録ができないとなったので、いよいよ料金の支払いが怪しくなってきました。

こんなときは再度サポートさんへ連絡です。カードの再登録ができないので、取り急ぎ今回の請求分はどう支払えばよいか尋ねてみました。すると、

ご登録のお支払い方法にて決済が失敗している請求書につきましては、以下いずれかのお支払いをご検討くださいませ。

  1. 別のクレジットカードでの一括決済
  2. 送金でのお支払い

という回答が返ってきました。送金(銀行振り込み)で一時的に対応してもらえるとのことです。ただ、下記のような注意もありました。

<ご留意点> ・次回以降のお支払いは引き続きご登録のクレジットカードへ決済がされます  次回お支払い時は、登録クレジットカードで決済が通るように調整いただくか、決済可能な他カードをご登録ください

基本はカードで払ってほしいけど、一時的な緊急措置ということで銀行振り込みで対応してもらえることのようです。

ということで、追加の支払方法を準備するまで日数がかかるので、取り急ぎ、銀行振り込みで対応することにしました。なお、振込方法についても丁寧に教えていただいたので、そちらは問題なく手続き完了となりました。

追加の支払方法について

取り急ぎ、支払は完了したので、次の請求日まで若干猶予ができました。このあいだに、

  • もう一枚クレジットカードを作るか?
  • それとも、妻のカードを一時的に借りて、今のカードを再登録してみるか?

などいろいろと考えていたのですが、ふと、 aws の支払いにデビットカードが使えることに気がつきました。

そうか!デビットなら必要以上に使ってしまう恐れもない(口座引き落としなので、口座の金額が上限になる)し、クレカの2枚持ちも避けられるので、これを使うことにします。

ということで、いま仕事の決済に使っているメインの銀行を調べてみると、口座さえあれば面倒な審査もなく、デビッドカードをすぐに発行することも可能とのことです。

これですね。

ということで、デビットカードを早速申し込みます。

追加の支払方法の登録

デビットカードの発行が完了したら、早速、支払方法として登録します。『請求とコスト管理』から『支払方法』を開きます。『支払方法を追加』を押すと、

こんな感じの登録画面が表示されるので、必要事項を記入していきます。この時点では『デフォルトの支払方法として設定』にチェックはつけません。登録が完了すると、

こんな感じに支払方法が2つ表示されます。

次に、デフォルトの支払方法を切り替えます。やり方は簡単で、新しく追加した2つ目の支払方法を選択して、『デフォルトとして設定』を押すだけです。

こんな感じの画面になり、無事にデフォルトの支払方法が切り替わりました。

(おまけ)未検証のクレジットカードの再登録

さて、このままでも来月以降の支払いはできるはずなんで、一応今回のトラブルは解決したということになります。

ですが、既存のカードが『未検証』の状態だと気持ち悪いので、クレジットカードを再登録してみます。まずは、既存のクレカ(支払方法)を選択して、『削除』ボタンを押して、削除します。

クレジットカード情報が削除できたら、再度『支払い方法を追加』ボタンから同じカード情報を入力します。入力後、最下段にある『支払い方法を追加』を押すと、

と表示されます。また『検証』ですね。同じことの繰り返しにならないか、ちょっと不安ですが、『検証』ボタンをクリックします。すると、

という画面が表示され、SMS に送った認証番号を入力するように求められました。

最初の検証では、この認証コードの入力画面が出てこなかったので、 aws 側かカード会社側かわかりませんがこのあたりに問題があったんでしょうね。

とうことで、送られてきた認証コードを入れると、無事に登録できました!最終的には、

こんな感じに2つの支払方法が登録された形になりました。

バックアップの支払方法の設定

さて、支払方法が2つ設定された状態になっています。このままでもいいんですが、aws からの情報としてコンソール画面に

と表示されています。これは裏を返せば、支払方法が複数あっても、デフォルトの支払方法が失敗したときに自動的に支払い方法が切り替わらない、ということなんでしょうね。

せっかく支払方法を2つ登録してあるので、バックアップの支払方法も登録しておきます。『デフォルトの支払いの詳細設定』で『編集』ボタンを押します。画面の少し下側のほうに、

とあるので、『バックアップの支払方法を有効にする』にチェックをつけて、『変更を保存』をクリックします。これで、OKです。

『デフォルトの支払いの詳細設定』を見てみると、

のように『バックアップの支払方法』が『有効』になっているのがわかります。これで一安心ですね。

まとめ

同じ支払アカウントでお客さんのサービスも動かしていたので、 aws の支払いができないとなるとなかなか焦りますね。 仕事で使っている方は、せめて支払方法を2つ用意しておくほうが無難そうです。