学び
それはそうだろという話ではあるけど、routeデコレーター引数のresponse_classをHTMLレスポンスで宣言しているにもかかわらず、実際はそのルータメソッドでFileResponseを返していると、HTTPレスポンスを上手くフォーマットできない場合がある。
今回Cloud Runでデプロイした場合は「Service Unavailable」が起きた。FastAPIもCloud RunもHTTP通信を明示的に扱うため、APIが宣言するものと実際に送信するものに不一致がある場合、予期せぬエラーにつながる。
この実装をした背景
- FastAPIでフォーム送信すると、その内容に基づいて画像ファイルを生成して、ファイルの中身を画面で表示させるWebアプリを開発した(画面にはファイルの中身を表示させたいが、ファイルダウンロードさせたいわけではない。)
- ローカルでの動作確認やGAEやGCEでデプロイしたときは、response_classとレスポンスが異なっていても、Webアプリはいい感じにファイルの中身を表示してくれていたので、この雑なやりかたでも問題なかった。
- 例えばルータメソッドの中身はこんな感じ(エラーが起きる例):
@router.post(
f"***",
response_class=HTMLResponse,
response_description="***",
)
@handle_file_data_fetching_failed
async def show_***_file(
document: DocumentSchema = Depends(DocumentSchemaAsForm),
text: ***TextSchema = Depends(***TextSchemaAsForm),
template_type: TemplateTypeSchema = Depends(TemplateTypeSchemaAsForm),
) -> FileResponse:
return Single***Service(document, text, option, template_type).make()
改善後
ファイルの中身を読み込んでから、HTMLのコンテンツとして返す。
# application service
import base64
import aiofiles
from fastapi.responses import HTMLResponse
async def file_to_html_response(file_path: str) -> HTMLResponse:
async with aiofiles.open(file_path, "r") as f:
content = await f.read()
encoded_content = base64.b64encode(content.encode()).decode()
svg_data_url = f"data:image/svg+xml;base64,{encoded_content}"
html_content = f"<img src='{svg_data_url}' alt='SVG Image' />"
return HTMLResponse(content=html_content)
# routers
@router.post(
f"***",
response_class=HTMLResponse,
response_description="***",
)
@handle_file_data_fetching_failed
async def show_***_file(
document: DocumentSchema = Depends(DocumentSchemaAsForm),
text: ***TextSchema = Depends(***TextSchemaAsForm),
template_type: TemplateTypeSchema = Depends(TemplateTypeSchemaAsForm),
) -> HTMLResponse:
file = return Single***Service(document, text, option, template_type).make()
return await file_to_html_response(file.path)