リクエストとレスポンス#
入力を読む#
context.request() から取れます。どこから来た値かで分かれています。
| メソッド | 中身 |
|---|---|
bodyPath() |
パスパラメータ(/users/{id}) |
bodyQuery() |
クエリ文字列 |
bodyForm() |
application/x-www-form-urlencoded / multipart |
bodyJson() |
JSON の本文 |
bodyFile() |
アップロードされたファイル(ファイルアップロード) |
bodyAll() |
上を全部重ねたもの |
bodyAll() の重ね順は path → query → form → json です。
あとのものが前を上書きします。
戻り値はどれも Data(LinkedHashMap<String,Object> の派生)です。
getString getInt getLong getBoolean getData getDataList などで取り出します。
無いキーは getString なら null、getInt なら 0、getBoolean なら false です
(「無い」と「0」は区別できません。区別したいときは getIntObject などの Object 版か isNull(key)。
詳しくは ユーティリティ)。
あるのに読めない値("abc" を getInt で読むなど)は DataConversionException です(黙って 0 になりません)。
**利用者の入力は、先に 検証 を通してください。**通さずに読んで例外になると 500 です。
既定値を渡す形(getInt("page", 1))は、無い・空欄のときだけ既定値を返します。
Request は Data ではありません。context.request().getString("title") のようには読めません(コンパイルエラーです)。
上の表のどれか(ふつうは bodyAll())を通してください。
Data input = context.request().bodyAll();
String title = input.getString("title");
Content-Type が application/json なのに本文が JSON として読めないと、body() / bodyJson() / bodyAll() が 400 の HttpException を投げます(何度読んでも同じです)。
途中まで読んだ本文や空の Data で先へ進むことはありません。
落とし穴
getStringOptional は「無ければ空文字」ですが、その空文字を Data に入れます。
読んだだけでキーが増えるので、JSON にして返す直前やループの中では使わないでください
(ユーティリティ)。
ヘッダと Cookie(同じ名前が2つ来たとき)#
context.request().source() から読めます。
| メソッド | 中身 |
|---|---|
headers() |
ヘッダ。キーは小文字。同じ名前が2行で来たら ", " で繋ぎます(Cookie だけ "; ") |
cookies() |
Cookie。同じ名前が2つ来たら先頭だけです |
headerValues() |
ヘッダ。繋ぐ前(Map<String, List<String>>) |
cookieValues() |
Cookie。捨てる前(Map<String, List<String>>) |
ふつうは headers() / cookies() で足ります。
2行で来たことそのものを知りたいときだけ ...Values() です。
落とし穴
同じ名前の Cookie は2つ来ることがあります。 別のパスやドメインに同じ名前で置かれた場合です。 どちらが先頭になるかは Cookie の仕様では決まっていません。
セッション ID でこれが起きると、ログインが不定期に外れます——
例外は出ませんし、次のリクエストでは直っていることがあります。
jimble は2本以上来たら警告を出します(値は出しません)。
ログに出たら、Path / Domain を揃えて置き直してください。
入れ子のパラメータ#
. と [ ] のどちらでも入れ子にできます。
user.name=taro
user[name]=taro 同じ
items[0].price=100
items[].price=100 末尾に足す
[ ] の中が数字なら添字、空なら追加、それ以外なら名前です。
JSON の本文と混ざったときは、フォームの値が JSON を上書きします。
検証する#
ValidationRules rules = new ValidationRules()
.put(Item.name, new ValidationRule().empty())
.put(Item.age, new ValidationRule().integer(1, 120));
Data request = new Data();
request.putData(Item.name, "");
request.putData(Item.age, "999");
// エラーは最初の1件で止めず、全部集める(要件 F-V-03)
Data errors = rules.errors(null, request);
Data messages = ValidationMessages.toMessages(errors);
エラーは最初の1件で止めません。 全部集めてから返します。 入力し直す人にとって、1件ずつ言われるのがいちばん困るからです。
ValidationMessages.toMessages(errors) で、項目名 → メッセージの Data になります。
そのまま JSON で返すか、テンプレートに渡します。
止めてよいなら rules.validate(db, input); と文で書きます。通らなければ ValidationException で、枠組みが 422 を返します。
ルールの一覧、ルート単位でかける ValidationExecutor、ページングは
検証とページング にまとめてあります。
ページングの件数は最初の1回で渡します(paging() のあとに paging(50) と違う件数を渡すと IllegalStateException です)。
返す#
context.response().send("text"); // text/plain
context.response().json("posts", list); // JSON
context.response().view("blog/posts.jte"); // テンプレート
context.response().redirect("/"); // 302
context.response().download(file, "報告書.xlsx"); // ダウンロード
context.response().code(201).send(); // 本文なし
json() は組み立てるだけです。複数回呼ぶと、1つの JSON に足されていきます。
返し方は1つだけです。json / jsonl / text / cache / view / redirect / download のうち、
違う種類を2つ積んだ時点で IllegalStateException になります(json(...) のあとに redirect(...) など)。
同じ種類を重ねるのは構いません。
json() のあとに send() を書く必要はありません。
組み立てておけば、ディスパッチャが実行の終わりに送ります。
send() を明示するのは、本文なしで終わらせたいとき(code(204).send())や、
テキストをそのまま返すときです。
get("/posts", context -> context.response().json("posts", BlogApp.listPosts()));
204 / 205 / 304 は本文を持てません(HTTP の決まりです)。
json(...) などを積んだまま code(204).send() すると、本文を捨てて、ステータスとヘッダだけを返します
(Set-Cookie は届くので、ログアウトを 204 で返す形も使えます)。
捨てたときは WARN が出ます——積んだ本文が誰にも届かないのは、たいてい書き間違いだからです。
本文の中身はログに出しません。
redirect() の飛び先が、いま処理しているリクエストと同じ URL なら RedirectLoopException が投げられます(500)。
そのまま返すとブラウザが同じ URL を取りに来て、同じ処理が同じ飛び先を返す——止まりません。
例外は他と同じく error(...) に渡るので、画面へ飛ばすか JSON を返すかはそちらで決めます。
見るのは GET / HEAD のときだけで、POST /login から /login へ戻す(PRG)は止めません。
見つけられるのは自分自身へ戻る1段だけで、/a → /b → /a のように別のリクエストをまたぐものは分かりません。
大きいものを流す#
全部をメモリに載せたくないときは outputStream() を直接使います。
context.response().setResponseHeader("Content-Type", "text/csv; charset=UTF-8");
try (OutputStream out = context.response().outputStream()) {
// 1行ずつ書く
}
outputStream() を呼んだ時点でステータスとヘッダが決まります。
code(...)・cookies() に積んだ Cookie・既定の Cache-Control: no-store(自分で決めていれば上書きしません)は、
その前に済ませてください。送ったあとにヘッダや Cookie を書くと IllegalStateException です(もう届きません)。
204 / 205 / 304 のときは、書いたものは捨てられて WARN が出ます。
**書いたものは flush() するまで溜まります。**CSV のように最後まで書き切るものはそのままで構いませんが、
届いたそばから相手に見せたいとき(途中経過・ほかのサーバーの応答の中継)は、書くたびに flush() してください。
InputStream を渡すときは send(in, "型")(または stream(...))です。
続きがまだ届いていなければ(available() が 0 なら)、そこまでを送り出します。
ファイルのように手元にそろっているものはまとめて書き、別のサーバーのストリーミング応答のように少しずつ届くものは、届いたそばから流れます。
HttpResponse<InputStream> upstream = http.send(request, HttpResponse.BodyHandlers.ofInputStream());
context.response().code(upstream.statusCode()).send(upstream.body(), "text/event-stream");
進捗を送りたいだけなら SSE のほうが簡単です。
二重送信#
send() を2回呼ぶとエラーになります。
isSent() で送信済みかどうかを見られます。
after フィルタや error ハンドラの中では、これを見てから触ってください。