v3.1 (4/N): GraalVM native-image hints + typed payload record#9
Merged
Conversation
…Registrar)
Two changes that together make the library play nicely with Spring Boot
3's AOT processor and `./gradlew nativeBuild`:
1) Replace Map.of(...) error payload with the typed SsrfBlockPayload record
The previous form serialised an immutable Map.of(...) — internally one
of ImmutableCollections.MapN, whose private layout AOT couldn't
introspect. Reflection on those classes would either fail at native-
image build time or strip the fields silently, producing empty
{} bodies in production where the LLM expected its structured error.
The record fixes both: AOT sees the components at build time and
registers reflection automatically, AND the JSON wire shape gains a
stable declaration order (Map iteration order was officially
undefined). The wire keys, values, and substring assertions remain
unchanged — every existing JsonToolInputGuard test passes verbatim.
2) Add SsrfGuardLlmRuntimeHints + META-INF/spring/aot.factories
- SsrfBlockPayload record reflection (INVOKE_PUBLIC_METHODS,
constructors, DECLARED_FIELDS — covers everything Jackson touches)
- BlockReason enum reflection (PUBLIC_FIELDS + methods — Jackson
reads constants and calls label() via reflection)
Spring Boot's AotServices loader discovers the registrar via
`META-INF/spring/aot.factories`, so it activates whenever
`ssrf-guard-llm` is on the classpath — no consumer code change.
What does NOT need its own hints
- All -springai / -langchain4j autoconfigs — Spring Boot 3 AOT
handles @autoConfiguration / @bean / @conditional* / @ConfigurationProperties
out of the box.
- SsrfGuardReactorAddressResolverGroup — extends Netty's
AbstractAddressResolver, which is a regular subclass relationship;
no reflection.
- All -httpclient5 / -jdkhttp / -okhttp types — direct Java code,
no runtime reflection.
Tests
- Existing 9 JsonToolInputGuard tests pass after the record refactor
(proves wire-compat).
- 2 new SsrfGuardLlmRuntimeHintsTest tests assert the right hints
are registered via Spring's RuntimeHintsPredicates.
- Full build: 212 tests across 11 modules.
Note: actual GraalVM native-image build verification isn't in CI
yet — adding it requires the GraalVM toolchain on the runner and
~10 min build per smoke. The declarations here follow Spring Boot's
documented patterns; if a consumer hits a native build issue, an
additional registerType(...) call here is the fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the LLM-adapter modules native-image friendly. Two changes:
1. Replace
Map.of(...)error payload withSsrfBlockPayloadrecordThe previous form was an immutable
Map.of(...)— internally one ofImmutableCollections.MapN, whose private layout Spring Boot's AOT processor couldn't introspect. On a./gradlew nativeBuild, reflection on those classes would either fail or silently strip the field names, leaving the LLM with an empty{}body where it expected{"error":"ssrf_blocked","reason":"..."}.The new record:
Mapiteration order was officially undefined)2.
SsrfGuardLlmRuntimeHints+META-INF/spring/aot.factoriesRegisters what the AOT processor wouldn't trace on its own:
SsrfBlockPayloadreflection (belt-and-braces — AOT usually catches records, but explicit removes guessing)BlockReasonenum reflection — Jackson reads enum constants + invokeslabel()via reflectionLoaded through
META-INF/spring/aot.factoriesso Spring Boot'sAotServicespicks it up automatically wheneverssrf-guard-llmis on the classpath.What doesn't need hints
-springai/-langchain4jautoconfigs → Spring Boot 3 AOT handles@AutoConfiguration/@Bean/@Conditional*/@ConfigurationPropertiesout of the boxSsrfGuardReactorAddressResolverGroup→ direct subclass of Netty'sAbstractAddressResolver, no reflection-httpclient5/-jdkhttp/-okhttp→ pure Java, no runtime reflectionTest plan
JsonToolInputGuardtests pass after record refactor (proves wire-compat)SsrfGuardLlmRuntimeHintsTesttests assert the right hints register viaRuntimeHintsPredicatesNot in CI
Actual native-image smoke build (
./gradlew nativeBuildon a demo) requires the GraalVM toolchain — adds ~10 min per run. The declarations here follow Spring Boot's documented patterns; if a consumer hits a native build issue, a one-lineregisterType(...)in the hints class is usually the fix. Tracking a CI-level native verification as a v3.2 candidate.What's left in v3.1
v3.1.0, Maven Central publish via release workflow, demo bumps in devslab-examples (including the newssrf-guard-langchain4j-demoI'll write right after the tag goes out).