Invoke-RestMethod で日本語が文字化けする。送る側を UTF-8 にしても直らない
PowerShell 5.1 で FastAPI の日本語 JSON が文字化けする原因と直し方
Windows PowerShell 5.1 の Invoke-RestMethod で、日本語の JSON が「ã」で始まる文字列に化ける。原因はレスポンスの Content-Type に charset がないこと。FastAPI 側での直し方と、サーバーを触れないときの読み方。
先に結論
- Windows PowerShell 5.1 の
Invoke-RestMethodは、Content-Typeにcharsetがない JSON を UTF-8 として読まない。 - FastAPI(Starlette)の JSON 応答は、既定では
application/jsonだけでcharsetが付かない。 - サーバー側で
application/json; charset=utf-8を返すようにすれば直る。エラー応答の分も忘れずに。 - サーバーを触れないなら、生のバイト列を UTF-8 として読み直すか、PowerShell 7 を使う。
症状:GET でも化ける
自作のローカル API(FastAPI)を Windows PowerShell 5.1 から叩いたら、日本語が ã で始まる文字列になった。
最初は送る側を疑い、リクエストの本文を UTF-8 のバイト列にして送った。それでも直らなかった。決め手は、本文を持たない GET でも同じように化けたこと。送っていないものは化けようがないので、受け取る側の読み方の問題だと分かる。
原因:Content-Type に charset がない
FastAPI が返す JSON の本文は、ちゃんと UTF-8 だった。問題はヘッダーのほうで、Content-Type が application/json だけだった。
FastAPI の JSON 応答は Starlette の JSONResponse で、Starlette は text/ で始まる型にしか charset を足さない。application/json はそのまま出ていく。
PowerShell 5.1 は、charset がないと本文を UTF-8 として読まない。同じ本文を charset あり・なしで返して比べると、こうなった。
| 返したヘッダー | PowerShell 5.1 | PowerShell 7.6 |
|---|---|---|
application/json |
ã で始まる文字列に化けた |
読めた |
application/json; charset=utf-8 |
読めた | 読めた |
化けた文字を一文字ずつ見ると、「こ」の UTF-8 のバイト E3 81 93 が、そのまま U+00E3 U+0081 U+0093 の三文字になっていた。一バイトを一文字として読む ISO-8859-1(Latin-1)で読んだときと同じ形になる。
切り分けの順番
開発記録では、次の順に疑っていた。
- リクエスト本文のエンコード →
GETでも化けるので違う - データベースに保存した文字が壊れている → 保存されていたのは UTF-8 のままで、壊れていなかった
- レスポンスの
Content-Typeにcharsetがない → これだった
データベースは壊れていなかったので、書き換えずに済んだ。同じ症状のときは、ヘッダーを先に見ると早い。
直し方:FastAPI 側で charset を付ける
charset 付きの JSON 応答クラスを一つ作り、アプリ全体の既定にする。ルートごとに直すと漏れるので、まとめて変える。
from fastapi import FastAPI, Request
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
class UTF8JSONResponse(JSONResponse):
media_type = "application/json; charset=utf-8"
app = FastAPI(default_response_class=UTF8JSONResponse)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
return UTF8JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=getattr(exc, "headers", None),
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return UTF8JSONResponse(status_code=422, content={"detail": jsonable_encoder(exc.errors())})
エラー応答も忘れない
default_response_class を変えただけでは、エラー応答は変わらない。FastAPI 0.141.1 で確かめると、普通の応答は charset=utf-8 付きになったが、HTTPException の 404 と、入力の検査で落ちた 422 は application/json のままだった。
エラーの detail に日本語を入れていると、そこだけ化ける。上のコードの二つの例外ハンドラーはそのためのもの。このコードで 200、401、404、422 のすべてが charset=utf-8 付きになることを確かめた。
サーバーを触れないとき
他人の API など、ヘッダーを変えられないときは、受け取る側で UTF-8 として読み直す。
$res = Invoke-WebRequest $url -UseBasicParsing
$json = [Text.Encoding]::UTF8.GetString($res.RawContentStream.ToArray()) | ConvertFrom-Json
PowerShell 5.1 でも、これで正しく読めた。PowerShell 7(7.6 で確認)なら、charset がなくてもそのまま読める。
よく見かける [Console]::OutputEncoding = [Text.Encoding]::UTF8 は、この症状には効かなかった。画面に出すときの設定で、応答の読み方は変わらないからだ。
確かめ方
応答のヘッダーを見る。
(Invoke-WebRequest $url -UseBasicParsing).Headers['Content-Type']
application/json だけなら、PowerShell 5.1 では化ける。application/json; charset=utf-8 になっていれば大丈夫。
似た罠:スクリプトファイルそのもの
この記事の再現用に、日本語のコメントを入れた .ps1 を BOM なしの UTF-8 で保存したら、PowerShell 5.1 では途中でエラーになった。PowerShell 7 では動いた。BOM 付きで保存し直すと、5.1 でも動いた。
応答の文字化けとは別の話だが、5.1 で日本語を扱うなら、スクリプトは BOM 付きで保存しておくと迷わない。
確かめた環境
| 項目 | 版 |
|---|---|
| OS | Windows 11(build 26200、ANSI コードページ 932) |
| Windows PowerShell | 5.1.26100.9444 |
| PowerShell | 7.6.6 |
| FastAPI / Starlette | 0.141.1 / 1.6.0 |
再現に使ったサーバーは、Python 標準ライブラリの http.server で、同じ UTF-8 の本文を charset あり・なしの二通りで返すだけのもの。