Repository navigation
Releases: SoftInstigate/restheart
Release list
9.10.0
Release 9.10.0
Overview
A minor release about what happens before authentication. The brute force guard now counts every failed attempt and refuses a blocked source whatever credentials it sends; no request reads the database before it is authenticated. Upgrading from 9.9.2 needs no data migration and no configuration change. It is a minor release, not a patch, because the order of some interceptors changed: if you write plugins, read For plugin developers below.
Improvements
Brute force guard
- A blocked source is refused before authentication. It gets
429 Too Many Requestswhatever credentials it sends. Until 9.9.2 the answer of a blocked source depended on the credentials:401for wrong ones,429for the right ones, so the block could be used to test passwords. - The block holds while the attack goes on. A blocked request counts as a failed attempt too: the source is released only after it stays quiet for a whole 10 seconds window. Until 9.9.2 the window drained during the block, and
max-failed-attemptsrequests went through every 10 seconds. - Every request without an authenticated account counts, whatever its status. Until 9.9.2 only a
401did, so wrong credentials on a collection that does not exist, answered404, were never counted. - At DEBUG, each counted failure is logged with its source and the count in the window:
Failed auth counted: histogram=… count=….
Docs: Brute Force Attack Guard.
Nothing reads the database before authentication
- The properties of the db and of the collection are now read after authentication. Whether a db or a collection exists is information, and the query that answers it no longer runs for a request without valid credentials.
- An error set by an interceptor at
REQUEST_BEFORE_AUTHis sent at once. Until 9.9.2 it was held back until after authentication.
Bug Fixes
- The brute force guard lost failed attempts under concurrency: requests arriving together were counted as one. Each failure is now one entry of the sliding window, and none is lost.
- The brute force guard forgot every source each 100 failed requests overall, so a
max-failed-attemptsof 100 or more was never reached, and under a distributed attack a lower one could be missed too. Only the sources with an empty window are forgotten now.
Behavior Changes
- Brute force guard. A blocked source gets
429also with the right credentials, until it stays quiet for 10 seconds. Requests with wrong or missing credentials are counted on any path, existing or not. 404and401. A request without valid credentials for a db or a collection that does not exist gets401, as before; the difference is that the database is not queried for it.
For plugin developers
Check your interceptors against these changes.
dbPropsInjectorandcollectionPropsInjectormoved fromREQUEST_BEFORE_AUTHtoREQUEST_AFTER_AUTH, with priorityInteger.MIN_VALUEandInteger.MIN_VALUE + 1. An interceptor that readsMongoRequest.getDbProps()orgetCollectionProps()must run atREQUEST_AFTER_AUTHwith a priority value higher than those. AtREQUEST_BEFORE_AUTHthe properties are no longer there.- Each interceptor is resolved right before it is handled, in priority order, so
resolve()sees what the interceptors before it attached to the request. Until 9.9.2 everyresolve()of an intercept point ran first, then everyhandle(). An interceptor whoseresolve()relied on the request as it was before the others handled it should be checked. vectorScanInterceptormoved from priorityInteger.MIN_VALUEtoInteger.MIN_VALUE + 10, still atREQUEST_AFTER_AUTH. The priorities fromInteger.MIN_VALUE + 2to+ 9are free for interceptors that declare an aggregation for the request:$vectorScanis recognized in the stages they add.- An error set at
REQUEST_BEFORE_AUTHends the request there: authentication does not run, and the interceptors atREQUEST_AFTER_FAILED_AUTHare not called for it.
9.9.2
Release 9.9.2
Overview
A patch release for RESTHeart behind proxies: the brute force guard finds the client's address as its documentation says, also on a server with more than one entry, and tells you when it blocks someone. Upgrading from 9.9.1 needs no data migration; two behaviors change, see below.
Improvements
Brute force guard
- More than one entry. A server reached both through a CDN and straight from a load balancer finds the client at a different position of
X-Forwarded-Foron each.x-forwarded-for-overridessets the position per request, by predicate: the first that matches decides, the others usex-forwarded-for-value-from-last-element. The predicate must tell the entries apart by something a client cannot forge, like a host only one entry forwards.
bruteForceAttackGuard:
enabled: true
trust-x-forwarded-for: true
x-forwarded-for-value-from-last-element: 0 # straight to the load balancer
x-forwarded-for-overrides:
- predicate: regex[pattern='.*\.example\.app', value='%{i,Host}', full-match=true]
value-from-last-element: 1 # through a CDN, then the load balancer- One log line. A blocked source is logged once when it crosses the threshold and once when it is released, each on one line that starts with
Brute force guard:, withkey=valuefields. The requests in between go to DEBUG. It replaces a multi-line box written for every blocked request. - Alarm by mail. With the
emailsprovider configured,notify-emailmails the alarm, at most once per source everynotify-cooldown-minutes(60 by default).
Docs: Brute Force Attack Guard.
Change streams
- A change stream WebSocket answers the client's pings with a pong, so a client can tell a live connection from one dropped in silence, and reconnect. Anything else the client sends is ignored, within 4 KiB. Docs: Keeping a WebSocket alive.
Bug Fixes
- The brute force guard ignored
x-forwarded-for-value-from-last-element, the documented key: it readx-forwarded-for-value-from-last, and with the documented one fell back to the remote ip, which behind a proxy is the proxy's for every client. Both keys are read now.
Behavior Changes
- Brute force guard behind a proxy. A configuration with
trust-x-forwarded-for: trueandx-forwarded-for-value-from-last-elementnow counts failures perX-Forwarded-Forvalue, as documented, and no longer per remote ip. Check that the element you configured is one your proxy sets, not one a client can send. - Voyage reranking.
voyageRerankProviderdefaults torerank-3instead ofrerank-2.5, at the same price. Setmodel: rerank-2.5to keep the previous model.
9.9.1
Release 9.9.1
Overview
A patch release for long-lived connections behind a proxy and for restheart-accounts on servers that do not authenticate with a cookie. Upgrading from 9.9.0 needs no data migration; one behavior changes for restheart-accounts, see below.
Improvements
Idle streams stay open behind a proxy
A stream with nothing to report sends nothing, and a load balancer or proxy in front of RESTHeart drops a connection idle for its own timeout without closing it. The client then waits on a socket that will never deliver another event.
- Change stream WebSockets now get a ping at the
change-streams-keep-alive-msinterval, as SSE change streams already got a comment. Default 20 seconds,0sends none. Browsers answer pings on their own. A ping that cannot be written closes the session. - MCP streams get a comment line at the new
stream-keep-alive-secondsinterval, inmcpService. Default 20 seconds,0sends none. A comment that cannot be written ends the stream, and the client reconnects.
mongo:
change-streams-keep-alive-ms: 20_000
mcpService:
stream-keep-alive-seconds: 20restheart-accounts: token delivery follows the server's authentication
- New
cookie-deliverysetting inaccountsConfig: whether a token can be handed over as the auth cookie. Unset, it follows whetherauthCookieHandleris enabled. When it is false,delivery=cookieis refused with400, and the endpoints that default to the cookie hand the token over in the body or in the URL fragment instead. A server that does not read the cookie no longer sets one. cookie-namedefaults to the nameauthCookieSetteruses (/authCookieSetter/name), so the two can no longer disagree. Set it only to use a different name.
Docs: Auth flows.
Bug Fixes
- The MCP listening stream (
GET /mcp) sent its status and headers only with the first message: a client, or a proxy, could wait for them indefinitely.
Behavior Changes
- With
authCookieHandlerdisabled andcookie-deliveryunset,delivery=cookieon therestheart-accountsendpoints now answers400instead of setting a cookie the server does not read. Setcookie-delivery: trueto keep the old behavior.
9.9.0
Release 9.9.0
Overview
RESTHeart 9.9 brings the Model Context Protocol into the framework.
- MCP in the framework. Any plugin becomes an MCP tool or resource by implementing
McpAware. It describes its resources and actions, and RESTHeart serves them to AI agents at/mcp, through the full handler chain and your ACL. The MongoDB and GraphQL plugins use it to expose collections, aggregations, change streams and GraphQL apps, opt-in per resource. - Vector search on any MongoDB. New
restheart-aimodule: vector indexes, document chunking, automatic embeddings, reranking, and$vectorScan, a brute-force vector search that needs neither Atlas nor an index. - Data constraints. A collection declares rules that span documents, such as "no balance goes negative", and the server enforces them on every write.
- Transactional validation. On a replica set, a write that fails its JSON Schema or a constraint is rolled back inside a transaction. Bulk
PATCHis now validated.
Upgrading from 9.8 needs no data migration. A few behaviors change, and custom plugins must be rebuilt: read the upgrade guide.
New Features
MCP in the framework: McpAware (#615, #617, #715)
McpAware is a new interface in restheart-commons. Any plugin implements it to publish Model Context Protocol tools and resources, with no transport code and no hand-written tool definitions.
- A plugin describes its resources, their actions and resource templates. It can watch a resource so subscribers are notified when it changes.
- Actions run in-process through the full handler chain, as the MCP session's own account: authentication, authorizers, interceptors, then the plugin's
handle(). A plugin can overrideexecute()(#741). ctx.scope()partitions the catalogue, so one process can serve isolated callers (#733, #734).Service.operationsToAuthorize()gives single-endpoint services, such as MCP and GraphQL, per-resource authorization (#722).
Docs: McpAware.
The MCP server
The server at /mcp discovers every McpAware plugin. It is enabled by default and exposes nothing until a resource is published.
- Three tools:
list_apisto discover,how_to_callto learn the HTTP request,call_apito run it.call_apireturnsstructuredContent(#744). Theresourcesprimitive supports read and subscribe. - Streamable HTTP transport (#598).
- The catalogue is composed per caller, by reading the caller's ACL permissions (#743, #745, #746).
- Idle sessions end after
session-idle-timeout-seconds, 30 minutes by default. - Works in the GraalVM native image (#714).
Ready-made MCP for MongoDB and GraphQL (#616)
MongoService and GraphQLService implement McpAware. Collections, aggregations, change streams and GraphQL apps are published with an mcp block in their metadata. The same block can narrow visibility with hide_from_roles or show_if.
Docs: MCP for MongoDB and GraphQL and tutorial.
restheart-ai: vector search (#590, #591)
- Vector search index management via
/_indexes. - Pluggable embedding providers (OpenAI-compatible, Voyage, Voyage contextual, Ollama) and rerank providers (Cohere, Voyage).
- Automatic embeddings on write, driven by the
vectorSearchcollection metadata. A collection can declare several embedding rules (#752). $vectorizecomputes the query vector inside an aggregation, resolving the rule from its stage path (#753).$vectorScan: brute-force vector search with no mongot and no index (#712), with a hard cap onmaxCandidates(#755).- Document chunking for RAG, with per-bucket rules and file filters (#754). Chunking can run in the background: the upload answers once the file is stored, and the file carries a status (#758).
Docs: restheart-ai and vector search.
Data constraints (#730)
Collection metadata can declare rules that span documents, as aggregation stages. They are enforced on every write, whatever the caller's role. Requires a replica set. Docs: Data Constraints.
OAuth
grant_type=refresh_tokenon/token, with a configurable grace window (#729).- A deployment can choose which token the Authorization Code flow issues, with a choice step on the sign-in page (#740). RESTHeart serves the sign-in page at
/oauth/login.
Other features
- GraphQL:
@visible(roles: [...])hides a field from callers without those roles (#478). - API keys can be scoped per tenant with
override-keys-db(#738).
Improvements
- Post-write checks run inside a transaction on a replica set, so a refused write leaves no document and no change stream event. Bulk
PATCHis validated againstjsonSchema(#731). - JSON Schema: draft-04, draft-06 and draft-07 are honored as declared; draft-07 is the default (#750).
- A
403from a vetoer carries the reason in its body, for instance the rejectedOrigin(#713). - GraphQL mappings resolve every registered
@variable, not only@user(#727). - Faster request pipeline: cached plugin reflection and service lookup, fewer allocations in interceptor resolution, timing and debug logging only when needed (#704–#710).
Bug Fixes
- The declared JSON Schema draft was overwritten with draft-04, and later keywords were silently ignored (#750).
- The
Locationheader saidhttp://behind a TLS-terminating proxy (#749). - A permission using
@qparamsin its predicate was discarded at load (#742). - A refused write answered with the
LocationandETagof the document it did not create. - A write that had already failed committed its transaction instead of aborting it.
- The Javadoc documented a
plugins-args:configuration key that does not exist (#723).
Breaking Changes
- Four static methods of
VarsInterpolatorinrestheart-commonsnow takeRequest<?>. Source compiles unchanged, but plugins built against 9.8 must be rebuilt. - Behavior changes for JSON Schema, bulk
PATCH,@qparamspermissions and theLocationheader: see the upgrade guide.
Dependencies
- MCP Java SDK: 2.0.1
- GraalVM: 25.1.3
9.8.2
Release 9.8.2
Security patch on top of 9.8.1, and nothing else. Every change is about the same thing: an
ACL variable that cannot be resolved must never widen what a caller can see or write.
Upgrade if you use readFilter, writeFilter or mergeRequest with @ variables, or if you
expose change streams. The documented per-user pattern
readFilter: {"owner": "@user._id"} did the opposite of what it promises for JWT-authenticated
callers — see the first item.
Security
Unresolved ACL variables no longer widen access
A variable the server could not resolve became MongoDB null. In a query {"owner": null} matches
every document that has no owner, so a filter meant to narrow a read widened it instead.
It happened whenever a variable named a property the account does not carry, and a JWT account
carries only the token's claims — never _id. So with the documented pattern
readFilter: {"owner": "@user._id"}, every JWT caller read every unowned document; with
writeFilter, modified or deleted them; and mergeRequest stamped owner: null on everything
they wrote, creating more of them. A file realm account behaves the same way, as does a missing
@qparams entry or any custom VarResolver that returns null.
Now an unresolved variable in readFilter/writeFilter is replaced with a random value that
matches no document — the same answer unbound variables have always had in ACL predicates — and a
warning naming the variable is logged. The rest of the filter is unaffected, so
{"$or": [{"owner": "@user._id"}, {"public": true}]} still returns the public documents.
mergeRequest refuses the write with 403 when an @user variable is unbound, rather than
writing a document nobody owns.
Negations are the exception to keep in mind: {"owner": {"$ne": "@user._id"}} still matches
when the variable is unbound. Do not negate @ variables in filters.
Change streams apply readFilter and projectResponse
A change stream applied neither. A caller whose REST reads were limited to their own documents
received every event on the collection over WebSocket or SSE, fullDocument included, along with
the properties projectResponse hides.
Both are now applied, as they are to the equivalent GET:
- the
readFilteris rewritten ontofullDocument, so events that carry none — deletes — are not
delivered when the filter is about the document's own fields; - an exclusion
projectResponseremoves the properties fromfullDocument,
fullDocumentBeforeChangeandupdateDescription.updatedFields; - a filter that cannot be rewritten safely (
$expr,$where,$text) and an inclusion
projection, which would strip the event's resume token, refuse the stream with403.
Aggregation variables named @... are bound by the server alone
?avars={"@user._id": "someone-else"} — and its flat form ?@user._id=... — supplied a value for
a variable the server is supposed to resolve. It took effect wherever the account had no such
property, which for a JWT account is every dotted name, so an aggregation or change stream written
as {"$var": "@user._id"} to scope itself to the caller could be pointed at another user's data.
A supplied @ name is now ignored: the server's own value is used, and a request carrying one
still runs.
Upgrading
Drop-in: no configuration change, no change to ACLs, aggregations or stream definitions.
What changes is what those ACLs now do, and the expected symptom is fewer documents and fewer
events, not more:
| Configuration | Before | Now |
|---|---|---|
readFilter/writeFilter with a variable the account lacks |
matched every document missing that field | matches nothing |
mergeRequest with an unbound @user variable |
wrote null |
403 |
a change stream, for a caller with a readFilter |
every event | only the caller's own |
a change stream, with $expr/$where/$text in the filter or an inclusion projectResponse |
opened | 403 |
?avars={"@user…": …} |
bound the supplied value | ignored |
If your ACLs identify users by @user._id and your clients authenticate with a JWT, those
filters now match nothing — which is the point, but it is also a behaviour change to plan for. A
JWT account is identified by @user.sub; to keep using _id, add it to
jwtTokenManager/account-properties-claims so it travels in the token, string ids only.
Watch the log for ACL filter variable … is not bound: each line is a filter that was widening
access before this release.
Dependencies
- GraalVM: 25.1.3
- Java: 25+
restheart-mqtt (snapshot)
Latest build of restheart-mqtt from master, published only after its integration tests passed against the core built alongside it.
- commit:
bdd095f59aad86bd9e03b12ec5b5a08b4e6300c1 - built: 2026-09-21 16:19 UTC
The mqtt-snapshot git tag is a fixed anchor for a stable download URL and does not point at the commit above - the commit is recorded here instead.
Install into a RESTHeart instance:
curl -LO https://github.com/SoftInstigate/restheart/releases/download/mqtt-snapshot/restheart-mqtt-10.0.0-SNAPSHOT.zip
unzip restheart-mqtt-10.0.0-SNAPSHOT.zip -d /opt/restheart/plugins/
Then enable at least mqtt-client and mqtt-router - see the module README.
9.8.1
Release 9.8.1
Patch release on top of 9.8.0. Both changes are about aggregation variables, and
together they make notify_when deliver what it promises: one MongoDB cursor for
all the clients of a change stream.
Bug Fixes
Change streams: notify_when no longer fragments the cursor
A stream that filters per client with notify_when is served by a single shared cursor,
with the predicate evaluated per session at dispatch time. The variable each client binds,
however, was still part of the ChangeStreamWorkerKey — (url without query string, avars, jsonMode) — so every distinct value produced its own worker, its own MongoDB cursor and its
own connection from the driver's pool: precisely what the feature exists to avoid. With
enough long-lived or abandoned clients the pool runs out, and every request that touches
MongoDB then blocks until its checkout timeout.
The key is now built from the avars stripped of that variable. Stage interpolation keeps
seeing the full set, so an $ifvar optional stage behaves as before.
Change streams: the notify_when variable is resolved as an ordinary aggregation variable
Both binding forms now work: ?tid=acme and ?avars={"tid":"acme"}. Only the flat query
parameter used to be read, so a client binding through avars fell through to pass-through
mode and received every event on the stream. The raw query parameter still wins when both
are present, being the verbatim value the client sent.
Improvements
Aggregations: non-reserved query parameters are bound as variables
GET /coll/_aggrs/myaggr?status=A now satisfies a pipeline's {"$var": "status"}, with no
need to wrap a single variable in an avars object. A value is read as JSON when it parses
as one (?tags=[1,2,3], ?opts={"a":1}) and taken as a string otherwise; an explicit
avars entry wins on a name clash; reserved parameters (page, pagesize, filter,
sort, keys, hint, rep, …) are never bound. Change stream variables go through the
same mechanism, so this applies to them too.
Upgrading
Drop-in: no configuration change, and no change to existing stream or aggregation
definitions. A notify_when stream that worked before keeps working, and now actually
shares one cursor across its clients.
Dependencies
- GraalVM: 25.1.3
- Java: 25+
9.8.0
Release 9.8.0
New Features
restheart-stripe: Stripe billing and e-commerce, out of the box
A new bundled module adding two independent, opt-in modes on top of Stripe — enable one, both, or neither.
Subscriptions mode — SaaS billing for any entity (team, account, tenant):
- Stripe Checkout and Customer Portal integration, with a Customer lifecycle tied to your existing entities
- Plan catalog (
GET /stripe/plans) sourced from Stripe Products/Prices, with a local config layer for what RESTHeart must enforce (default plan, trial days, seat limits) - Seat licensing with three modes (
capped,per-seat,unlimited), enforced directly in ACL predicates - New
@subscriptionACL variable for writing plan-aware access rules - Webhook-driven subscription state sync (created/updated/deleted/trial-will-end/invoice payment succeeded or failed), with a staleness guard so out-of-order webhook delivery can't roll back a newer state
- Billing notification emails (trial ending, payment failed, subscription canceled, over-limit) with overridable templates
Products mode — cart-to-order e-commerce:
- Checkout session creation from a cart, validated against a catalog read from Stripe Products/Prices
- Order lifecycle as an explicit state machine (
pending_payment→paid/failed/expired), enforced with monotonic transitions so a delayed or replayed webhook can never move an order backwards - A money ledger tracking payments, refunds, and disputes per order, idempotent against redelivered Stripe events
- Order and transaction JSON Schema validation, order confirmation/refund notification emails, and optional stock checks against an inventory collection
Multi-tenancy: on-demand per-database initialization, request-level and tenant-level kill switches, so a single deployment can serve tenants with different Stripe configurations or none at all.
GraalVM native image support (#679): restheart-stripe now builds and runs correctly as a native executable. stripe-java deserializes Stripe API objects reflectively via Gson, which native-image doesn't support without explicit configuration — this is now in place, and the full webhook-driven test suite (subscriptions and products, every handled event type) passes against the native binary.
API key authentication (#699, #700)
New apiKeyAuthMechanism and mongoApiKeyAuthenticator, for authenticating requests with a long-lived API key instead of username/password or a JWT.
Optional plugin dependencies (#698)
@Inject now accepts required=false, so a plugin can declare a dependency on a provider that may not be present (e.g. an optional module) without failing to load when it isn't.
New ACL predicate variables
@roles— lets a predicate distinguish an anonymous caller from an authenticated one by role@authenticated— a boolean shorthand for "is this request authenticated at all"
Improvements
- Native image build workflow: builds trigger asynchronously in CI, with improved artifact handling
- Plugin initialization failures are now reported for what they are ("plugin failed to initialize"), rather than the previous message implying a compilation problem
- A refused deployment (e.g. an email that fails validation) now says why, instead of failing silently further down the pipeline
Bug Fixes
- Restricted temp directory permissions and removed a pointless temporary file
- Fixed a request being re-validated against its JSON Schema after it had already been refused
- Fixed numeric comparison predicates after a refactor to array-based parameters
Known Limitations
- restheart-stripe's native-image verification is webhook-driven end to end, but does not exercise the code paths that make live Stripe API calls (Customer creation, Checkout Session creation, Billing Portal Session creation) — those require a real Stripe test-mode key and aren't covered by the current test suite. Tracked as a follow-up; the reflection config for these paths is in place but unverified under native-image.
Dependencies
- GraalVM: 25.1.3
- Java: 25+
- stripe-java: 33.3.0
9.7.2
Release 9.7.2
Bug Fixes
JS plugins now work in Docker with GraalVM 25.x (#663)
JavaScript plugins were completely broken in the Docker image since GraalVM 25.x. Four separate bugs were identified and fixed:
-
Classloader issue (
Source.findLanguage): JS language discovery failed becauseplugins/lib/js-language-*.jarwas invisible to the system classloader. Fixed by routing throughPluginsClassloader. -
Removed GraalVM option: The
js.commonjs-core-modules-replacementsoption was removed in GraalVM 25.x but RESTHeart still set it, causing every Context creation to fail. -
Virtual threads crash (
ArrayIndexOutOfBoundsException): Truffle'sDefaultContextThreadLocaldoesn't support Java virtual threads (oracle/graal#7520). Since RESTHeart uses virtual threads for request handling and plugin deployment, any polyglot Context operation crashed. Fixed by routing all Truffle operations through a dedicated platform thread ("RH JS PLT"), gated by the system propertyrestheart.polyglot.force-platform-threads(default:true). -
Native image interceptor reflection:
Intercept Point: nullin native images caused byreachability-metadata.jsonregistering theinterceptPointfield onJSPlugininstead ofJSInterceptor.
GraalVM native image fixes
- Fixed plugin
enabledByDefaultannotation not being read in native images - Fixed null provider type in
ProvidersCheckerfor native images - Added static metrics to native image configuration
- Improved native image resource extraction and indexing
- Added
restheart-emailsdependency to native image
Improvements
- Dockerfile: Optimized JVM options for better performance in containers
- Bootstrap logging: Improved consistency during startup
- CI: Increased native image build timeout to 60 minutes
Known limitations
- JS plugin execution is serialized on a single platform thread under concurrent load (performance trade-off for correctness). Full virtual thread parallelism will be restored when oracle/graal#7520 is fixed upstream (tracked in #665).
- Escape hatch:
-Drestheart.polyglot.force-platform-threads=falsebypasses the workaround.
Dependencies
- GraalVM: 25.1.3
- Java: 25+
9.7.1
RESTHeart 9.7.1
RESTHeart 9.7.1 is a patch release that fixes JavaScript plugin loading. It also aligns native image CI workflows with the GraalVM 25.1.x release train.
Bug Fixes
Fix JS plugins not loading due to classloader issue (#663)
JavaScript plugins failed to load because Source.findLanguage() used the system classloader to discover languages via ServiceLoader. The js-language JAR lives in plugins/lib/, which is only visible to the PluginsClassloader, not the system classloader.
A new PolyglotClassloaderHelper utility temporarily sets the thread context classloader to the PluginsClassloader (obtained via reflection, since the polyglot module cannot depend on core) before calling Source.findLanguage(). Applied in:
PolyglotDeployer.findDeclaredPlugins()JSInterceptorFactory.create()JSStringServiceconstructor
The GraalVM version alignment done in 9.7.0 (Dockerfile, aws-build-native.sh) was a prerequisite but not sufficient on its own.
Other Improvements
- Native image CI workflows updated to use
version: '25.1'withdistribution: 'graalvm'(Oracle GraalVM innovation release) - Native image workflow timeout increased from 45 to 60 minutes
Full Changelog: 9.7.0...9.7.1