概要
SpringBootで、REST-APIの使用方法について少々ハマったので、その対処法について解説します。
Stack Overflowでもちゃんとした解決策が示されていなかったので時間が掛かってしまいました。時間のない人は、結論だけ読んでくれても良いです。
処理内容
例えば、ホテル登録のボタンを押すとフォームに書いた内容が、サーバーに送られてデータベースに登録される処理の例です。
画面のHTML、JavaScriptと、SpringBootでREST-APIのコントローラーで受けるメソッドは以下のようなものです。順に書きます。
HTML
<h1>ホテル登録</h1>
<h3>情報を入力してください</h3>
<form class="add_form" action="/hotel-entory" method="POST">
<input type="hidden" name="id">
<table border="1">
<tr>
<th>ホテル名称</th>
<td>
<input type="text" name="name">
</td>
</tr>
<tr>
<th>カテゴリー</th>
<td>
<select name="categoryId">
</select>
</td>
<tr>
<th>住所</th>
<td>
<input type="text" name="address">
</td>
</tr>
<tr>
<th>電話番号</th>
<td>
<input type="text" name="phone">
</td>
</tr>
</table>
<button class="btn">登録</button>
</form>
JavaScript
const fetchForm = document.querySelector('.add_form');
const btnForm = fetchForm.querySelector('.btn');
async function addHotel() {
const status = await postFetch();
if (status == 200 || status== 201) {
(成功処理)
}
}
btnForm.addEventListener('click', addHotel, false);
async function postFetch() {
const url = fetchForm.getAttribute('action');
const method = fetchForm.getAttribute('method');
const formData = new FormData(fetchForm);
const data = Object.fromEntries(formData.entries());
console.log(data);
const json = JSON.stringify(data);
try {
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
body: json,
});
if (response.status !== 200 && response.status !== 201) {
// 200,201以外ならばエラーメッセージを投げる
throw `response.status = ${response.status}, response.statusText = ${response.statusText}`;
}
return await response.status;
}
catch (err) {
console.log("err: " + err);
}
return null;
}
Spring側コントローラー
@RequestMapping(value = "/hotel-entory", method = RequestMethod.POST)
public boolean post(@RequestBody Hotel hotel) {
hotelRepository.save(hotel);
return true;
}
処理結果(エラー時)
ボタンを押すと、下記のようなエラーが返却されます。
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Mon Jul 03 18:01:09 JST 2023
There was an unexpected error (type=Unsupported Media Type, status=415).
Content-Type 'application/x-www-form-urlencoded;charset=UTF-8' is not supported.
org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/x-www-form-urlencoded;charset=UTF-8' is not supported
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:209)
at
・・・・・
対策の検討
これに対応するには、Stack Overflowなどでは、以下のようなコードに変えるようにとの解決策しか示されてません。
@RequestMapping(value = "/hotel-entory", method = RequestMethod.POST)
public boolean post(@RequestParam Map<String, String> map) {
ObjectMapper mapper = new ObjectMapper();
Hotel hotel = mapper.convertValue(map, Hotel.class);
hotelRepository.save(hotel);
return true;
}
確かに動くは動くのですが、でもこれではRequestBodyも使えないですしね。
そもそもRESTインタフェースが使えないなんて変ですよね。
curlでサーバー側の振る舞いをチェックしてみます。
% curl -H "Content-Type:application/json" -X POST -d "{\"name\":\"aaa\", \"categoryId\":1, \"address\":\"aaaa\", \"phone\":\"123\"}" http://localhost:8080/hotel-entory -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /hotel-entory HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type:application/json
> Content-Length: 63
>
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Mon, 03 Jul 2023 08:32:31 GMT
<
何の問題もなく終了し、データベースに書き込まれました。
そもそも、JavaScriptには、JSON指定しているのに、エラー時のコンテンツタイプがContent-Type 'application/x-www-form-urlencoded;charset=UTF-8'
になっているのはなぜなのか? 調べてみました。
結論
Formタグ中の、buttonタグに下記のように指定なき場合
<button class="btn">登録</button>
はデフォルトでtype="submit"になってしまっていたようです。
<button class="btn" type="submit">登録</button>
なので、敢えて下記のようにtype="button"を追加しましょう。
<button class="btn" type="button">登録</button>
問題なく、コンテンツタイプが、JSONで認識されるようになりました。
たったこれだけです。
SpringBootはこの辺りのポーティングが、厳格というか癖があるようですね。
世界中でこれにハマってしまった人がいるようなので、公開しておきます。