落とし穴#
落ちるバグは自分で見つかります。ここに集めたのは、落ちないバグです。
ローカルで Cookie が効かない#
症状: セッションもログインも CSRF も Flash も、エラーなしで効かない。
原因: cookie.secure の既定は true です。ローカルは http なので、
ブラウザが Cookie を送り返しません。
対処: ローカルの application.conf に入れます。本番では消します。
cookie {
secure = false
}
起動時に WARN が出るので、ログを見てください。
ルートが登録されていない#
症状: 404 になる。コードには書いてある。
原因: install(SomeController::new) を書き忘れています。
jimble はクラスパスを走査しないので、書かないと登録されません。
対処: 起動ログのルート一覧を見てください。無ければ登録されていません。
before が効いていない(同じパスなのに)#
before は書いたブロックの中だけに効きます。パスには付きません。
path("/admin", () -> {
before(requireAuth);
get("/users", ...);
});
install(() -> new SpaController("/admin", ...)); // ← 認証されない
同じ /admin でも、別の場所で登録したルートは別のブロックです。
まとめたいなら、同じブロックに入れてください。
逆の症状(効いてほしくないのに効く)は起きません。これが起きていたのが
移行前の形で、/admin の SPA ログイン画面まで認証に捕まっていました。
なお、ルートが確定した後に before を足すと落ちます。
「足したのに効かない」を黙って通さないためです。
ルート定義はコントローラの初期化ブロックの中で完結させてください。
送られてこなかった値が null のまま DB に入る#
症状: Column 'title' cannot be null で落ちる。フォームには項目がある。
原因: 無いキーの getString は null です(例外にはなりません)。
Data input = context.request().bodyAll();
String title = input.getString("title"); // 送られてこなければ null
対処: 読む前に 検証 を通します。required() はキーが無いときも失敗にします。
セッションが保存されていない#
症状: put() したのに次のリクエストで消えている。
原因: save() を呼んでいません。自動保存はしません。
context.session().put("user_id", 42);
// 明示的に保存する(要件 F-S-02)。自動保存はしない
context.session().save();
変えるのは session().put(...) です。session().data() は読み取り専用の写しで、書き換えると例外になります。
select の結果が取れない#
症状: row.getString("title") が空。
原因: 結果はテーブル名でネストしています。
Data row = db.select(
SQL.select()
.from(Post.instance())
.where(Post.id.eq(1L))
).orElseThrow();
// SELECT の結果はテーブル名でネストする(要件 F-D-02)
String title = row.getData("post").getString("title");
// Column で引けば、途中の文字列が出てこない
String same = row.getString(Post.title);
Column で引けば間違えません。
トランザクションの中で DB の失敗を受け止めると、確定できない#
症状: tx.commit() が TransactionException(DB_004)で落ち、何も保存されない。
原因: DB の失敗は SqlExecuteException です。トランザクションの中でそれを catch して続けても、
その Tx は確定できません。commit() が断り、全部巻き戻します。
対処: 分岐したい失敗は一意制約くらいです。トランザクションの外で DuplicateKeyException だけを受け止めます。
ほかの失敗は書かずに上まで飛ばします(500 になり、トランザクションは巻き戻ります)。
try (DB db = BlogExample.db()) {
/*
* 0件は空(空リスト・空の Optional・件数 0)、失敗は SqlExecuteException(2.0)。
* 書かなければ上まで飛んで 500。トランザクションの中なら巻き戻る。
*/
List<Data> rows = db.selectList(SQL.select().from(Post.instance()));
// 分岐したい失敗は一意制約くらい。それだけを受け止める
try {
db.insert(SQL.insert(Post.instance()).value(Post.title, "hello"));
} catch (DuplicateKeyException ex) {
Log.info("もうあります");
}
}
「コミットもロールバックもされていない」が出る#
ERROR コミットもロールバックもされていないトランザクションが残っていました。ロールバックして閉じます: main
原因: try-with-resources で囲まずに db.begin() して、途中で return しています。
対処: 囲んでください。jimble は実行の終わりに拾ってロールバックしますが、 それは事故の後始末であって、正しい書き方ではありません。
入口によって用意されるテーブルが違う#
{"error":"トランザクションのコミットに失敗しました。"}
原因: MqQueue#install() や BatchTables.install() を、一部の入口でしか呼んでいない。
Web・バッチ・スケジューラで入口が分かれていると、それぞれが自分の分だけ用意しがちです。 たとえば MQ のテーブルをバッチの入口でしか作っていないのに、 キューに積むのは Web ということが起こります。
やっかいなのは、いちどでもバッチを動かしたマシンでは動いてしまうことです。 手元では気づけず、まっさらな DB(CI や新しい環境)で初めて出ます。
対処: 起動時に用意するものは1か所にまとめ、どの入口もそれを呼んでください。
public final class Bootstrap {
public static void load () {
Migration.install(); // DBUtil.load より前
// 繋がらなければ SqlExecuteException(DB_007)で起動が止まる
DBUtil.load(Conf.conf().config(), Bootstrap.class);
BatchTables.install(DBUtil.getMainDB());
new MqQueue(NoticeExecutor.QUEUE_NAME).install();
}
}
**テストからも同じものを呼んでください。**テストだけ別に用意すると、 入口が変わったときにテストだけ通る状態になります。
こつ
まっさらな DB で1回流してみるのがいちばん早い確認です。
examples/blog はこれで壊れているのが分かりました。
製品を変えたら生成物の列順が変わる#
症状: 何も直していないのに codegen の差分が出る。列の並びだけが違う。
原因: 生成される列の並びは物理順です。
ALTER TABLE ... ADD COLUMN の位置が MySQL(AFTER body が書ける)と
PostgreSQL(必ず末尾)で違うので、同じスキーマでも並びが変わります。
対処: 生成は主にする製品で行ってください。codegenCheck も同じ製品で回します。
型も NULL 可否もコメントも一致するので、並び以外は変わりません。
SSE が届かない・全部まとめて届く#
原因: 間にいる nginx が溜め込んでいます。
対処: jimble は X-Accel-Buffering: no を付けています。
それでも駄目なら nginx 側で proxy_buffering off; を設定してください。
SSE のループが終わらない#
原因: sse.isOpen() を見ていません。
isOpen() は寿命が残っているかを返します。切断は検出できません
(helidon が教えてくれず、書き込みが永久にブロックすることがあります)。
ループの中で必ず見てください。詳しくは SSE を読んでください。
WebSocket で Cookie が読めない#
原因: 昇格したあとに読もうとしています。
HTTP のヘッダが見えるのは onUpgrade の時点だけです。
対処: 認証は onUpgrade でやります。false を返せば 403 になります。
MQ が2回実行された#
原因: そういうものです。「ちょうど1回」は作れません。
対処: 何度やっても同じになるように書いてください。詳しくは MQ。
HOCON のコメントで落ちる#
Key '/' may not be followed by token: '*'
原因: /* */ を使っています。HOCON のコメントは # か // です。
jimbleRun で「メイン・マニフェスト属性がありません」#
gradle-wrapper.jar に Main-Class がありません。gradlew と gradle-wrapper.jar が
食い違っています。
いまの gradlew はこう起動します。
exec java -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" ...
これには Main-Class を持つ JAR が要ります。古い(-classpath 前提の)JAR が
残っていると、gradlew も JAR も見た目は揃っているのに動きません。
unzip -p gradle/wrapper/gradle-wrapper.jar META-INF/MANIFEST.MF
# Main-Class: org.gradle.wrapper.GradleWrapperMain ← これが無ければアウト
直すには、そのプロジェクトで一度流してください。
gradle wrapper --gradle-version 9.7.1
jimbleRun は作り直しのときに ./gradlew を子プロセスで叩くので、
保存のたびにこれが出ます。 壊れていると分かったときは
理由を出して PATH の gradle に逃がすようにしてあるので、
ホットリロード自体は続きます。
Gradle が起動しない#
* What went wrong: 25.0.4
原因: Gradle 8 は Java 25 の上では動きません。 (ツールチェーンとして Java 25 を使うのは 8 でもできますが、Gradle 自身が動きません)
対処: Gradle 9 以上を使ってください。
Kotlin のコメントが入れ子になる#
build.gradle.kts のブロックコメントに /* が現れると、
そこから入れ子のコメントが始まります(Kotlin のブロックコメントは入れ子にできます)。
外側が閉じないまま、その下のコードが全部消えます。エラーは出ません。
/*
* docs/site/<言語>/*.md を読む ← この /* が入れ子を開く
*/
tasks.register("site") { ... } ← 登録されない。エラーも出ない
パスを書くときは * を避けるか、// を使ってください。
.gitignore の docs/ が全階層に効く#
症状: 公開したリポジトリからテンプレートが消えている。
原因: .gitignore に docs/ と書くと、git はどの階層の docs/ にも効かせます。
jimble-docs/src/main/jte/docs/ まで巻き込みます。
対処: 先頭に / を付けて /docs/ と書きます。
Gradle 9 で落ちるビルドスクリプト#
Gradle 8 では通り、9 で落ちるものがあります。
| 書き方 | 何が起きるか |
|---|---|
val x by tasks.registering { } |
9.6 で非推奨。Kotlin DSL ではコンパイルエラー。tasks.register("x") { } にします |
"...".formatted(...) |
Java 15 のメソッド。9.7 の Kotlin スクリプトからは解決できません。文字列テンプレートにします |
ビルドスクリプトは、実際に使うバージョンで一度は流してください。
2.0 でなくなった落とし穴#
1.x にあった次の落とし穴は、2.0 でコンパイルエラーか例外になりました。詳しくは 2.0 への移行 を読んでください。
context.request().getString("x")が黙ってnullを返す →RequestはDataではなくなり、コンパイルエラーselectのnull・更新系の-1を見落とす → 失敗はSqlExecuteExceptionDBUtil.load(...)の戻り値を捨てて、繋がらないまま進む → 例外で起動が止まる- 移送元の
commit()と jimble のcommit()の意味が違う →tx.commit()は確定して終わる。続けるならtx.checkpoint() session().data().put(...)が保存されない →UnsupportedOperationExceptionrequired()がキーごと送られない項目を通す → 失敗になる