【React + FastAPI】OAuth2を利用した認証リクエスト時に出た442エラーの原因と解決方法

## はじめに
- 個人の学習時の備忘録目的のため、シンプルな記事です
- コード例は実際のコードから必要と思われる部分を抜粋しています
- もし同じエラーで困っている人がいれば、何かの参考になれば幸いです

## 背景
- フロントに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仕様が使用できる方法の１つ（「パスワードフロー」と呼ばれる）では、フォームフィールドとしてusernameとpasswordを送信する必要があります。
> 仕様では、フィールドの名前がusernameとpasswordであることと、JSONではなくフォームフィールドとして送信されることを要求しています。

https://fastapi.tiangolo.com/ja/tutorial/request-forms/#form_1

## 解決方法
- 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.

https://axios-http.com/docs/urlencoded

### 変更後のコード（React）
```JavaScript:Requests.jsx
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`としていたので、フォームデータが必要になる場面がある事、正しく送信しないとエラーになるという事がわかった。

## 参考

https://fastapi.tiangolo.com/ja/tutorial/security/first-steps/

https://qiita.com/sand/items/990afc5d49a37b026acc

https://note.shiftinc.jp/n/n7b4f14e4bbcd