はじめに
- 個人の学習時の備忘録目的のため、シンプルな記事です
- コード例は実際のコードから必要と思われる部分を抜粋しています
- もし同じエラーで困っている人がいれば、何かの参考になれば幸いです
背景
- フロントにReact、バックエンドにFastAPIを用いたアプリを作成中
- 認証機能については、FastAPI側は公式チュートリアルに沿った形で
OAuth2PasswordBearer
(OAuth2)を使用して作成し、SwaggerUI上にてusernameとpasswordで認証できる事を確認済み
問題
React側からFastAPIのエンドポイントに対してusernameとpasswordで認証できるか試したところ、HTTPステータスコード 422 Unprocessable Entityというエラーになった。
原因
FastAPI側の認証のエンドポイントではOAuth2PasswordRequestForm
(OAuth2)を使用しており、リクエストのデータ形式がフォームデータ(application/x-www-form-urlencoded
)である必要がある。
しかし、React側からdataをJSON形式(application/json
)で送信していたため、エラーになっていた。
例えば、OAuth2仕様が使用できる方法の1つ(「パスワードフロー」と呼ばれる)では、フォームフィールドとしてusernameとpasswordを送信する必要があります。
仕様では、フィールドの名前がusernameとpasswordであることと、JSONではなくフォームフィールドとして送信されることを要求しています。
解決方法
-
React側で、認証リクエスト時のみContent-Typeに
application/json
ではなく、application/x-www-form-urlencoded
を指定するように変更(条件分岐)した。 -
データ形式もJSONからURLエンコードへ変更する必要がある。
→通常はURLSearchParams
等を使用する。
→今回はHTTPリクエストにAxios
を使用しており、Axios
ではContent-Typeにapplication/x-www-form-urlencoded
を指定すればデータが自動的にapplication/x-www-form-urlencoded
の形にシリアライズされる。そのため、コード自体はJSON形式のままでOK
🆕 Automatic serialization
Axios will automatically serialize the data object to urlencoded format if the content-type header is set to application/x-www-form-urlencoded.
変更後のコード(React)
import axios from "axios";
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const apiPathUrl = {
signup: "/auth/signup",
signin: "/auth/signin",
};
// APIリクエストのベース
const apiRequest = async({ method, apiEndpoint, data={}, token, isFormUrlEncoded = false }) => {
try {
const headers = {
...(isFormUrlEncoded
? { "Content-Type": "application/x-www-form-urlencoded" } // Formで送信したい時(signup)に使用
: { "Content-Type": "application/json" }),
...(token && { Authorization: `Bearer ${token}` }), // トークンが存在する場合のみ設定
};
const response = await axios({
method: method,
url: apiBaseUrl + apiEndpoint,
data: data,
headers: headers,
});
return response;
} catch (error) {
console.log('apiRequest failed', error);
throw error;
}
};
// サインアップ
export const signupRequest = async(name, email, password) => {
const response = await apiRequest({
method: "POST",
apiEndpoint: apiPathUrl.signup,
data: {
name: name,
email: email,
password: password,
},
});
return response;
};
// サインイン
// メールアドレスでログインさせたいが、バックエンド側のOAuth2PasswordBearerが
// usernameを受け取る必要があるため、メールアドレスをusernameとして送信する
// OAuth2PasswordRequestForm は Formデータを受け取る仕様
// →application/x-www-form-urlencodedを指定すれば、dataはaxiosで自動的に対応形式にシリアライズされる
export const signinRequest = async(email, password) => {
const response = await apiRequest({
method: "POST",
apiEndpoint: apiPathUrl.signin,
data: {
username: email,
password: password,
},
isFormUrlEncoded: true,
});
return response;
};
補足メモ
application/x-www-form-urlencoded
とapplication/json
の違い
項目 | application/x-www-form-urlencoded |
application/json |
---|---|---|
データ形式 | key1=value1&key2=value2 | {"key1": "value1", "key2": "value2"} |
エンコード | URLエンコード | JSONエンコード |
用途 | フォームデータ | APIのリクエスト/レスポンス |
おわりに
- FastAPIはSwaggerUIで動作確認ができる点が便利だが、その分フレームワーク側でうまく処理されて見えにくくなる部分があることを再認識した。
- APIリクエストのデータ形式について、あまり深く考えずに
application/json
としていたので、フォームデータが必要になる場面がある事、正しく送信しないとエラーになるという事がわかった。
参考