Repository navigation
fix(graalvm): pre-generate WebSessionAvailable mixin to fix native image crash - #154
Conversation
… support Add SessionMiddleware to NativeApplicationFactory.buildApp() so that WebSessionAvailable is included in the $Mixin subclass generated at build time. Without this, SessionMiddleware.handle() falls through to Proxy.newProxyInstance() at runtime, which is forbidden in native image. - Merge buildApp()/create() into a single buildApp(ComponentInjector) to eliminate duplicate middleware stack definitions (closes #148) - Add GET /session endpoint with SessionController (visit counter demo) - Add KotowariFeatureTest to verify WebSessionAvailable is pre-generated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses a GraalVM native-image runtime crash caused by SessionMiddleware invoking MixinUtils.mixin(...) (which uses Proxy.newProxyInstance()) by ensuring the required WebSessionAvailable mixin class is pre-generated at build time.
Changes:
- Add
SessionMiddlewareto thekotowari-graalvm-examplemiddleware stack soWebApplication.createRequest()pre-generates theWebSessionAvailable$Mixinclass. - Refactor
NativeApplicationFactoryto centralize stack construction inbuildApp(ComponentInjector)and have bothbuildApp()andcreate()delegate to it. - Add a
/sessiondemo endpoint and a unit test assertingWebSessionAvailablemixin pre-generation.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| kotowari-graalvm/src/test/java/kotowari/graalvm/KotowariFeatureTest.java | Adds a test verifying mixin pre-generation for WebSessionAvailable when SessionMiddleware is used. |
| kotowari-graalvm-example/src/main/java/kotowari/example/graalvm/NativeApplicationFactory.java | Ensures SessionMiddleware is included in the build-time middleware stack and consolidates app creation logic. |
| kotowari-graalvm-example/src/main/java/kotowari/example/graalvm/controller/SessionController.java | Adds a simple per-session visit counter endpoint for native-image validation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| app.createRequest(); | ||
|
|
||
| // One of the generated classes must implement WebSessionAvailable | ||
| boolean found = false; | ||
| for (String className : MixinUtils.generatedClassBytes.keySet()) { | ||
| Class<?> cls = Class.forName(className); | ||
| if (WebSessionAvailable.class.isAssignableFrom(cls)) { | ||
| found = true; | ||
| break; | ||
| } | ||
| } | ||
| assertThat(found) | ||
| .as("Expected a $Mixin class implementing WebSessionAvailable to be pre-generated") | ||
| .isTrue(); |
There was a problem hiding this comment.
The test scans the global MixinUtils.generatedClassBytes map and calls Class.forName on every entry. This can become brittle if other tests populate the map (false positives) or if any generated entry isn’t loadable in this test’s class loader. Consider asserting against the specific request returned by app.createRequest(): verify it is instanceof WebSessionAvailable, and then assert generatedClassBytes containsKey(request.getClass().getName()) (or only scan newly-added keys).
| app.createRequest(); | |
| // One of the generated classes must implement WebSessionAvailable | |
| boolean found = false; | |
| for (String className : MixinUtils.generatedClassBytes.keySet()) { | |
| Class<?> cls = Class.forName(className); | |
| if (WebSessionAvailable.class.isAssignableFrom(cls)) { | |
| found = true; | |
| break; | |
| } | |
| } | |
| assertThat(found) | |
| .as("Expected a $Mixin class implementing WebSessionAvailable to be pre-generated") | |
| .isTrue(); | |
| HttpRequest request = app.createRequest(); | |
| assertThat(request) | |
| .as("Expected app.createRequest() to return a request implementing WebSessionAvailable") | |
| .isInstanceOf(WebSessionAvailable.class); | |
| assertThat(MixinUtils.generatedClassBytes) | |
| .as("Expected the generated request mixin class to be pre-generated") | |
| .containsKey(request.getClass().getName()); |
| Routes routes = routes(); | ||
|
|
||
| if (injector != null) { | ||
| // Register routes for KotowariFeature build-time discovery (runtime only) |
There was a problem hiding this comment.
This comment is contradictory: the code runs only when injector != null (runtime), but it says it’s for “KotowariFeature build-time discovery”. Either update the wording to reflect the actual behavior, or (if the intent really is build-time discovery via RouteRegistry.get()) move RouteRegistry.register(routes) out of the injector != null conditional.
| // Register routes for KotowariFeature build-time discovery (runtime only) | |
| // Register routes in the runtime registry when a real injector is available. |
- KotowariFeatureTest: assert createRequest() result is instanceof WebSessionAvailable directly, instead of scanning generatedClassBytes (more focused, less fragile against static map state) - NativeApplicationFactory: fix misleading comment on RouteRegistry.register() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop the MixinUtils.generatedClassBytes.containsKey() assertion from KotowariFeatureTest — the instanceof check on the return value of createRequest() is sufficient and does not depend on internal state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
SessionMiddlewarecallsMixinUtils.mixin(request, WebSessionAvailable.class)at runtime, which internally callsProxy.newProxyInstance()— forbidden in GraalVM native imageNativeApplicationFactory.buildApp()did not includeSessionMiddleware, soWebSessionAvailablewas never pre-generated as a$Mixinsubclass at build timeSessionMiddlewareto the build-time middleware stack soWebApplication.buildRequestFactory()includesWebSessionAvailableinMixinUtils.createFactory(), making themixin()call a noop at runtimeChanges
NativeApplicationFactory: merged duplicatebuildApp()/create()into a singlebuildApp(ComponentInjector)to eliminate double maintenance;buildApp()(no-arg) andcreate()both delegate to itSessionController: new demo endpointGET /session— per-session visit counter, demonstrates session usage in native imageKotowariFeatureTest: new test verifying thatWebSessionAvailableis included in the pre-generated$Mixinclass whenSessionMiddlewareis in the stackTest plan
mvn test -pl kotowari-graalvm -am -DexcludedGroups=integrationpasses (11 tests including newbuildRequestFactory_pregeneratesMixinForSessionMiddleware)mvn -pl kotowari-graalvm-example -Pnative packagebuilds without errorcurl -c /tmp/s.txt http://localhost:3000/session→visits=1curl -b /tmp/s.txt -c /tmp/s.txt http://localhost:3000/session→visits=2Closes #148
🤖 Generated with Claude Code