Skip to content

Releases: SoftInstigate/restheart

9.10.0

Choose a tag to compare

@github-actions github-actions released this 05 Oct 13:06
d5a0699

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 Requests whatever credentials it sends. Until 9.9.2 the answer of a blocked source depended on the credentials: 401 for wrong ones, 429 for 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-attempts requests went through every 10 seconds.
  • Every request without an authenticated account counts, whatever its status. Until 9.9.2 only a 401 did, so wrong credentials on a collection that does not exist, answered 404, 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_AUTH is 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-attempts of 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 429 also 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.
  • 404 and 401. A request without valid credentials for a db or a collection that does not exist gets 401, as before; the difference is that the database is not queried for it.

For plugin developers

Check your interceptors against these changes.

  • dbPropsInjector and collectionPropsInjector moved from REQUEST_BEFORE_AUTH to REQUEST_AFTER_AUTH, with priority Integer.MIN_VALUE and Integer.MIN_VALUE + 1. An interceptor that reads MongoRequest.getDbProps() or getCollectionProps() must run at REQUEST_AFTER_AUTH with a priority value higher than those. At REQUEST_BEFORE_AUTH the 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 every resolve() of an intercept point ran first, then every handle(). An interceptor whose resolve() relied on the request as it was before the others handled it should be checked.
  • vectorScanInterceptor moved from priority Integer.MIN_VALUE to Integer.MIN_VALUE + 10, still at REQUEST_AFTER_AUTH. The priorities from Integer.MIN_VALUE + 2 to + 9 are free for interceptors that declare an aggregation for the request: $vectorScan is recognized in the stages they add.
  • An error set at REQUEST_BEFORE_AUTH ends the request there: authentication does not run, and the interceptors at REQUEST_AFTER_FAILED_AUTH are not called for it.

9.9.2

Choose a tag to compare

@github-actions github-actions released this 02 Oct 18:26
554ff99

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-For on each. x-forwarded-for-overrides sets the position per request, by predicate: the first that matches decides, the others use x-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:, with key=value fields. The requests in between go to DEBUG. It replaces a multi-line box written for every blocked request.
  • Alarm by mail. With the emails provider configured, notify-email mails the alarm, at most once per source every notify-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 read x-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: true and x-forwarded-for-value-from-last-element now counts failures per X-Forwarded-For value, 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. voyageRerankProvider defaults to rerank-3 instead of rerank-2.5, at the same price. Set model: rerank-2.5 to keep the previous model.

9.9.1

Choose a tag to compare

@github-actions github-actions released this 01 Oct 15:40
1578351

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-ms interval, as SSE change streams already got a comment. Default 20 seconds, 0 sends 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-seconds interval, in mcpService. Default 20 seconds, 0 sends 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: 20

restheart-accounts: token delivery follows the server's authentication

  • New cookie-delivery setting in accountsConfig: whether a token can be handed over as the auth cookie. Unset, it follows whether authCookieHandler is enabled. When it is false, delivery=cookie is refused with 400, 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-name defaults to the name authCookieSetter uses (/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 authCookieHandler disabled and cookie-delivery unset, delivery=cookie on the restheart-accounts endpoints now answers 400 instead of setting a cookie the server does not read. Set cookie-delivery: true to keep the old behavior.

9.9.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 16:03
16aef6d

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-ai module: 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 PATCH is 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 override execute() (#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_apis to discover, how_to_call to learn the HTTP request, call_api to run it. call_api returns structuredContent (#744). The resources primitive 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 vectorSearch collection metadata. A collection can declare several embedding rules (#752).
  • $vectorize computes 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 on maxCandidates (#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_token on /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 PATCH is validated against jsonSchema (#731).
  • JSON Schema: draft-04, draft-06 and draft-07 are honored as declared; draft-07 is the default (#750).
  • A 403 from a vetoer carries the reason in its body, for instance the rejected Origin (#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 Location header said http:// behind a TLS-terminating proxy (#749).
  • A permission using @qparams in its predicate was discarded at load (#742).
  • A refused write answered with the Location and ETag of 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 VarsInterpolator in restheart-commons now take Request<?>. Source compiles unchanged, but plugins built against 9.8 must be rebuilt.
  • Behavior changes for JSON Schema, bulk PATCH, @qparams permissions and the Location header: see the upgrade guide.

Dependencies

  • MCP Java SDK: 2.0.1
  • GraalVM: 25.1.3

9.8.2

Choose a tag to compare

@github-actions github-actions released this 20 Sep 22:49
c8c08b7

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 readFilter is rewritten onto fullDocument, so events that carry none — deletes — are not
    delivered when the filter is about the document's own fields;
  • an exclusion projectResponse removes the properties from fullDocument,
    fullDocumentBeforeChange and updateDescription.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 with 403.

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)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Sep 23:20
00beb57

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

Choose a tag to compare

@github-actions github-actions released this 08 Sep 18:22
15cb0f6

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

Choose a tag to compare

@github-actions github-actions released this 31 Aug 14:31
d70d143

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 @subscription ACL 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

Choose a tag to compare

@github-actions github-actions released this 11 Aug 10:31
142d881

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:

  1. Classloader issue (Source.findLanguage): JS language discovery failed because plugins/lib/js-language-*.jar was invisible to the system classloader. Fixed by routing through PluginsClassloader.

  2. Removed GraalVM option: The js.commonjs-core-modules-replacements option was removed in GraalVM 25.x but RESTHeart still set it, causing every Context creation to fail.

  3. Virtual threads crash (ArrayIndexOutOfBoundsException): Truffle's DefaultContextThreadLocal doesn'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 property restheart.polyglot.force-platform-threads (default: true).

  4. Native image interceptor reflection: Intercept Point: null in native images caused by reachability-metadata.json registering the interceptPoint field on JSPlugin instead of JSInterceptor.

GraalVM native image fixes

  • Fixed plugin enabledByDefault annotation not being read in native images
  • Fixed null provider type in ProvidersChecker for native images
  • Added static metrics to native image configuration
  • Improved native image resource extraction and indexing
  • Added restheart-emails dependency 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=false bypasses the workaround.

Dependencies

  • GraalVM: 25.1.3
  • Java: 25+

9.7.1

Choose a tag to compare

@github-actions github-actions released this 09 Aug 14:57
b72d0b5

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()
  • JSStringService constructor

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' with distribution: 'graalvm' (Oracle GraalVM innovation release)
  • Native image workflow timeout increased from 45 to 60 minutes

Full Changelog: 9.7.0...9.7.1