Goblin - Chaos Engineering Extension for Quarkus

Goblin is a chaos engineering extension for Quarkus that lets you inject latency, exceptions, HTTP failures, and dependency degradation into your running application — without touching a single line of source code.

It is designed for one specific purpose: verifying that your resilience mechanisms actually work. You declared @Timeout, @Fallback, @Retry, health probes, or circuit breakers? Goblin gives you the tool to prove they hold under real failure conditions.

Think of Goblin as Chaos Monkey for the Quarkus ecosystem — but with stronger safety guarantees.

Why Goblin?

Quarkus has excellent resilience primitives (MicroProfile Fault Tolerance, Mutiny reactive timeouts, health probes), but no built-in way to trigger the failures these mechanisms are supposed to handle. You end up writing fragile integration tests that mock exceptions, or worse, discovering production failure modes for the first time in production.

Goblin fills that gap: inject realistic failures at the HTTP layer during development, observe how your application reacts, and iterate.

Key design decisions:

  • Dev mode only. Chaos artifacts are physically absent from production builds. Not disabled — absent. There is no toggle to forget to switch off.

  • Zero code modification. No @ChaosLatency annotations to scatter across your endpoints. Configuration is the only interface.

  • Non-destructive. You can target specific packages, exclude annotated methods, and control the percentage of affected requests.

  • Composable. Multiple assault types can be active simultaneously on the same request.

  • Persistent state. Dev UI config changes survive restarts via automatic disk persistence (.goblin-state.json).

Quick start

Add the dependency

<dependency>
    <groupId>io.quarkiverse.goblin</groupId>
    <artifactId>quarkus-goblin</artifactId>
    <version>${goblin.version}</version>
</dependency>

That’s it. Start your application in dev mode:

./mvnw quarkus:dev

You will see a warning log confirming chaos is active:

WARN  Chaos engineering active: 100% of REST requests subject to assault (profile=NONE, latency=true, exception=false, httpStatus=false, dependencyDegradation=false, clientLatency=false, clientException=false, responseBody=false)

Verify it works

With the default configuration (latency assault enabled, 100% of requests), every REST endpoint will have an artificial delay applied. Hit any endpoint and observe the added latency:

curl -w "\nTime: %{time_total}s\n" http://localhost:8080/api/hello

You should see a response time well above your normal baseline.

Assault types

Goblin supports four types of server-side chaos assaults, plus client-side assaults for outgoing REST Client calls. Each type has its own boolean toggle — you can enable multiple types simultaneously on the same request.

When multiple types are active, they are applied in order:

  1. Latency — adds delay before processing (applied first).

  2. Exception — throws an exception, aborting the request.

  3. HTTP Status — returns a specific status code, aborting the request.

  4. Dependency Degradation — returns 503, aborting the request.

  5. Response Body — rewrites the entity of an emitted response (truncate or inflate).

In addition, client-side assaults (latency and exception) can be enabled for outgoing MicroProfile REST Client calls — see Client-side assaults. The response body assault is applied when the response is emitted, so it composes with every other type: for example, HTTP status + response body turns a 503 into a 503 with a truncated body.

If both latency and exception are enabled, the delay is applied first, then the exception is thrown. This simulates a slow failure — realistic for testing timeouts followed by fallbacks.

LATENCY

Injects a random delay (between min-milliseconds and max-milliseconds) before processing the request. Useful for testing timeouts, circuit breakers, and retry mechanisms.

quarkus.goblin.assault.latency.min-milliseconds=500
quarkus.goblin.assault.latency.max-milliseconds=5000

The delay is applied via Thread.sleep() in the JAX-RS filter, before the request reaches your endpoint logic.

If min-milliseconds is greater than max-milliseconds, the values are swapped (with a WARN log) both at startup and when saving the range in the Dev UI.

EXCEPTION

Throws a configurable exception before the request reaches the endpoint. The exception class and message are fully configurable.

quarkus.goblin.assault.exception.type=java.io.IOException
quarkus.goblin.assault.exception.message=Connection refused to downstream service

The exception must extend RuntimeException and have a constructor that accepts a single String parameter (the message). RuntimeException is used as a fallback if the specified class cannot be instantiated.

If the class cannot be found, does not extend RuntimeException, or lacks a String constructor, an ERROR is logged at startup (and when saving in the Dev UI), and the engine falls back to RuntimeException.

The Dev UI quick-picks are served by the engine (exceptionPresets in the config) and only offer classes validated against those constraints (e.g. RuntimeException, IllegalStateException, WebApplicationException, InternalServerErrorException) — a preset can never trigger the runtime fallback, so each proposed choice is guaranteed to be throwable. Checked exceptions such as java.io.IOException or java.util.concurrent.TimeoutException are rejected because the filter layer can only throw unchecked exceptions.

HTTP_STATUS

Returns a specific HTTP status code without executing the endpoint method at all. The request is aborted at the filter level.

quarkus.goblin.assault.http-status.code=503
quarkus.goblin.assault.http-status.message=Service Unavailable (simulated outage)

Common use case: verify that your frontend handles 503 responses gracefully.

The status code must be in the 100-599 range. Out-of-range values are detected at startup (and when saving in the Dev UI) and default to 503 with an ERROR log.

DEPENDENCY_DEGRADATION

Returns 503 with a "Dependency unavailable" message. Designed for testing @Fallback and @Retry on outbound service calls. No additional configuration needed — it uses a fixed 503 response.

RESPONSE_BODY

Rewrites the entity returned by eligible endpoints to a corrupted or unexpected size. Useful for validating that your JSON clients (React, mobile apps, REST Client consumers) fail gracefully when the payload they receive is truncated or padded beyond the declared Content-Length.

quarkus.goblin.assault.type=RESPONSE_BODY
quarkus.goblin.assault.body.mode=truncate
quarkus.goblin.assault.body.percentage=50

Two transformation modes are available:

  • TRUNCATE — keeps only the first percentage% of the body. For example, truncating a JSON payload to 50% yields a prefix that is no longer valid JSON, so a strict JSON client fails to parse it. The Content-Length header is updated to the truncated size, so the response stays well-framed and the corruption is purely at the payload level.

  • INFLATE — pads the body with a fixed [goblin-response-inflated] marker so the effective size reaches percentage% of the original (typically over 100%, e.g. 200). The Content-Length header keeps advertising the original, smaller length while more bytes are emitted, exercising length-validating proxies and parsers. The transformed bytes are written verbatim: truncation never re-encodes a split multibyte character, so the delivered size always matches the configured percentage.

The percentage is validated per mode: 0-100 for TRUNCATE, 101-1000 for INFLATE. Out-of-range values are clamped with a WARN log both at startup and when saving in the Dev UI (e.g. INFLATE with 50 becomes 101). In both modes the target size is floor(originalBytes * percentage / 100): the emitted size is rounded down so it never exceeds the requested percentage, and any non-zero percentage keeps at least one byte (0 yields an empty body).

The response body assault rewrites the entity in the JAX-RS response filter, so it only applies to responses that carry a body your application produces (String or byte-array entities). It follows the same targeting rules as the other server-side assaults (target level and package/annotation filters), and it is recorded in the assault history like any other assault.

Client-side assaults (outgoing REST Client calls)

Goblin also attacks outgoing calls made with MicroProfile REST Client or Quarkus REST Client Reactive (quarkus-rest-client). A JAX-RS ClientRequestFilter is registered globally and intercepts every outbound REST Client call:

  • Client latency — sleeps for a random duration within the configured latency range before the request is dispatched, simulating a slow downstream service. The remote service is reached, but after the delay.

  • Client exception — throws the configured exception before the request is sent. The remote service is never reached.

Client-side assaults reuse the latency range and exception class/message configured for the incoming side, but are controlled by their own toggles, which default to off:

  • client latency / client exception toggles in the Dev UI Chaos Dashboard;

  • toggleClientLatency() / toggleClientException() via JSON-RPC.

Client-side assaults are off by default. They are recorded in the assault history like any other assault, with the HTTP method and URI of the outbound call as the method (e.g. /api/books inside REST-Client GET http://localhost:8081/api/books). They respect the shared target.level percentage but ignore the package/annotation filters, which only select incoming endpoints. HTTP status and dependency degradation assaults are server-side response manipulations and are never applied to outgoing calls.

Profiles

Realistic failure scenarios usually combine several effects, and tuning every parameter by hand for each scenario is repetitive. A profile bundles a common assault combination into a single config line.

quarkus.goblin.assault.profile=SLOW_FAILURE

Available profiles:

Profile

Assaults enabled

Sensible defaults

NONE

none (manual mode, assault.type is used instead)

SLOW_FAILURE

latency + exception

Latency 100-5000 ms, RuntimeException("Goblin chaos: simulated exception")

INTERMITTENT

HTTP status

HTTP 500 — combine with quarkus.goblin.target.level to control frequency

TIMEOUT

latency

Fixed 30000 ms (30 s) — ideal for exercising client-side @Timeout

Profiles are a starting point, not a lock-in. Once a profile is active, each assault toggle stays independently overridable through the Dev UI or JSON-RPC, and those overrides survive restarts like any other Dev UI change.
At startup, a non-NONE profile takes precedence over the static configuration: fromConfig first copies the static assault.type and per-assault parameters, then the profile defaults are applied on top — so a configured profile overrides the static toggles and parameters at boot. Tune the values afterwards through the Dev UI if needed.
Profile selector in the Dev UI

The Profile section of the Chaos Dashboard: pick a predefined assault mode in one click — the active profile also appears in the status bar.

The active profile is displayed in the Dev UI status bar and included in the Markdown report and the describeAssaults() snapshot recorded in the assault history.

Targeting

By default, Goblin affects all REST endpoints in your application. You can narrow or expand this scope with targeting configuration.

Percentage-based targeting

Control what fraction of incoming requests are affected (0-100%):

# Only affect 10% of requests (useful for simulating intermittent failures)
quarkus.goblin.target.level=10

Setting level=0 effectively disables chaos without removing the extension. Setting level=100 (default) affects every request.

Values outside the 0-100 range are clamped (with a WARN log) both at startup and when saved in the Dev UI.

Package filtering

Include only specific packages:

quarkus.goblin.target.include-packages=com.example.api,com.example.internal

Exclude specific packages:

quarkus.goblin.target.exclude-packages=com.example.health,com.example.metrics

The filter matches on the declaring class’s package name using startsWith, so com.example will match com.example.api, com.example.internal, etc.

Annotation-based exclusion

Exclude methods (or their declaring classes) that carry specific annotations. This is particularly useful for coexisting with MicroProfile Fault Tolerance:

# Don't inject chaos on endpoints that already have fault tolerance annotations
quarkus.goblin.target.exclude-annotations=org.eclipse.microprofile.faulttolerance.Timeout,org.eclipse.microprofile.faulttolerance.Fallback

This prevents double-interception: you can chaos-test your unprotected endpoints while leaving fault-tolerant ones to their own mechanisms.

Exclusion by annotation never requires you to add an annotation to your application. Goblin only reacts to annotations that are already present on your endpoints (such as MicroProfile Fault Tolerance types). There is deliberately no Goblin-owned annotation to scatter across your code — consistent with the zero code modification principle.

Full configuration reference

All configuration keys are under the quarkus.goblin prefix.

Key Type Default Description

quarkus.goblin.enabled

boolean

true

Enable/disable the Goblin extension. Only takes effect in dev mode. Never overridden by the persisted state file; disabling this property keeps chaos off even if a previous session left assault toggles enabled.

quarkus.goblin.assault.type

AssaultType

LATENCY

Initial assault type enabled at startup. Values: LATENCY, EXCEPTION, HTTP_STATUS, DEPENDENCY_DEGRADATION, RESPONSE_BODY. Can be changed at runtime via the Dev UI. Ignored when a non-NONE profile is selected.

quarkus.goblin.assault.profile

AssaultProfile

NONE

Predefined composite assault mode. Values: NONE, SLOW_FAILURE, INTERMITTENT, TIMEOUT. A non-NONE profile enables a set of assaults with sensible defaults; individual assaults stay user-overridable afterwards. Can be changed at runtime via the Dev UI.

quarkus.goblin.assault.latency.min-milliseconds

long

100

Minimum latency in milliseconds. Must be lower than or equal to max-milliseconds; inverted values are swapped with a WARN log

quarkus.goblin.assault.latency.max-milliseconds

long

5000

Maximum latency in milliseconds

quarkus.goblin.assault.exception.type

String

java.lang.RuntimeException

Fully qualified exception class name. Must have a String constructor; otherwise the engine falls back to RuntimeException with an ERROR log

quarkus.goblin.assault.exception.message

String

Goblin chaos: simulated exception

Exception message

quarkus.goblin.assault.http-status.code

int

503

HTTP status code to return. Must be in the 100-599 range; out-of-range values default to 503 with an ERROR log

quarkus.goblin.assault.http-status.message

String

Service Unavailable (Goblin chaos)

HTTP response body

quarkus.goblin.assault.body.mode

ResponseBodyMode

TRUNCATE

Response body transformation to apply. Values: TRUNCATE (keep the first percentage% of the body), INFLATE (pad the body up to percentage% of its original size).

quarkus.goblin.assault.body.percentage

int

50

Target size of the transformed body in percent of the original, rounded down (floor). Valid range: 0-100 for TRUNCATE, 101-1000 for INFLATE. Out-of-range values are clamped with a WARN log.

quarkus.goblin.target.level

int

100

Percentage of requests to affect (0-100; values are clamped with a WARN log)

quarkus.goblin.target.include-packages

Optional<String[]>

empty

Packages to include (empty = all packages)

quarkus.goblin.target.exclude-packages

Optional<String[]>

empty

Packages to exclude from chaos

quarkus.goblin.target.exclude-annotations

Optional<String[]>

empty

Exclude methods/classes carrying these annotations

Configuration validation

Goblin validates assault parameters at startup and whenever you save a change in the Dev UI. Invalid values are never applied: Goblin logs a clear message and applies a safe fallback, so a misconfiguration cannot silently corrupt your chaos experiments.

Parameter

Rule

What happens

assault.latency.min-milliseconds / assault.latency.max-milliseconds

min must be lower than or equal to max

Values are swapped and a WARN is logged

assault.http-status.code

Must be in the 100-599 range

Defaults to 503 and an ERROR is logged

assault.exception.type

Class must exist and expose a String constructor

Falls back to RuntimeException and an ERROR is logged

assault.body.percentage

Must be in 0-100 for TRUNCATE, 101-1000 for INFLATE

Clamped to the valid range and a WARN is logged

target.level

Must be in the 0-100 range

Clamped and a WARN is logged

At startup the checks run in AssaultEngine.onStart() and cover both the application.properties configuration and the state loaded from .goblin-state.json. When you edit a parameter in the Dev UI, the same rules apply: the corrected value is persisted back to .goblin-state.json, and the dashboard shows a warning toast explaining what changed — for example, entering min=10000, max=500 swaps them to min=500, max=10000.

Dev UI panel

When running in dev mode, Goblin provides a control panel in the Quarkus Dev UI (accessible at /q/dev). You reach it by clicking the Goblin card in the extension grid:

Goblin card in the Quarkus Dev UI

The Goblin card in the Dev UI extension grid.

Chaos Dashboard

The dashboard is the main control surface. It exposes the master toggle, per-type toggles, per-type configuration forms, the target level, blast radius controls, quick-pick presets, and live assault counters.

Goblin Chaos Dashboard

The Chaos Dashboard with latency enabled at 100% target level.

  • Master toggle — Activate/deactivate all chaos with a single click. When inactive, the status dot turns red and the Deactivate button becomes Activate.

  • Kill switch — Turn every assault off at once: deactivates the engine, disables each assault type and client-side assault, and resets the profile to NONE.

  • Auto-off — Optionally arm the dashboard to automatically deactivate chaos after a countdown (between 15 and 600 seconds). The remaining time ticks down and a banner appears, and chaos is turned off when the deadline arrives.

  • Blast radius — Set a chaos blast radius (0-100%) next to the master toggle: it maps to the target level with its own inline save input.

  • Live counters — A status-bar widget shows the total number of assaults recorded for the session, how many attackers are live, and the per-type breakdown, refreshed every 2 seconds together with the config.

  • Profile selector — Pick a predefined composite assault mode (NONE, SLOW_FAILURE, INTERMITTENT, TIMEOUT) with one click. The active profile is also shown in the status bar. Individual toggles stay overridable on top of the profile.

  • Custom profiles — Save the current configuration under a custom name, and re-apply it later from the profile selector. Custom profiles are stored in the browser and marked with a badge when one is active (a manual override).

  • Assault type toggles — Independent on/off switches for each assault type (Latency, Exception, HTTP Status, Dependency Degradation, Response Body). Multiple types can be active simultaneously. Enabled types are highlighted with the Quarkus primary color, and each row shows its priority.

  • Priority ordering — Assault types are grouped by execution priority: the server-side assaults and client-side assaults that preempt normal request processing form the preemptive group; the terminal response damaging transformations (truncate/inflate body, HTTP status substitution) form the final group.

  • Client-side assault toggles — A dedicated section controls client latency and client exception, applied to outgoing MicroProfile REST Client calls. They default to off and reuse the latency range and exception class/message configured above.

  • Quick picks — One-click presets pinned to the top of each assault type section (e.g. latency 100-500 ms, HTTP status 503, response body TRUNCATE 50%, and several verified exception classes), applied immediately with a single action. The exception quick picks are served by the engine and are guaranteed to be throwable — no fallback.

  • Config sections — Each assault type has its own configuration section (latency range, exception class/message, HTTP status code/message, response body mode/percentage). Sections are always visible but disabled with placeholder text when the type is off. Enable the type to edit its parameters. Every section has its own Save button; the button is only enabled when the form is dirty and valid.

  • Inline validation — Entering an out-of-range value disables the Save button and shows a warning — nothing invalid is ever persisted. Validation rules match the startup rules (see Configuration validation).

  • Target level — Adjust the percentage of affected requests (0-100%) on the fly, saved with its own button.

  • Danger zone — Export the current configuration as a JSON file, import a previously exported one (or any partial config), or reset the whole configuration to its defaults.

  • All changes apply instantly — no restart needed. A WARN log is emitted in the console for every change, and a toast confirms each action in the UI. If a change violates a validation rule (see Configuration validation), the toast shows the applied correction, e.g. a swapped latency range.

When chaos is deactivated, the configuration sections remain visible but are dimmed and their inputs disabled — you can prepare your assault configuration before turning chaos on.

History

The History panel is a live chaos-testing console. It auto-refreshes every 2 seconds while the panel is visible, so new assaults appear as they happen without a manual reload.

Goblin Assault History

The Assault History panel showing past triggered assaults.

Each row shows:

  • the timestamp of the assault — millisecond precision, with the full ISO timestamp shown on hover

  • the endpoint method targeted

  • the assault type shown as a colored badge — Latency, Exception, HTTP Status, Dependency, and for response body assaults two distinct badges depending on the mode: Truncate (response-body-truncate) and Inflate (response-body-inflate)

  • the actual applied latency duration when the assault was a latency injection

  • the active assault configuration at the time of the assault, stored compactly in an Active Config cell — click the cell to expand or collapse it

Newest assaults appear first. A summary band above the table shows the totals per assault type and the average injected latency, updated live (the response body total combines both modes).

A filter bar narrows the list: filter by assault type (the Response Body entry groups the truncate and inflate modes), by a method text search, or by time period (last 5 minutes, 30 minutes, 1 hour, 24 hours, or all time). The counter shows how many of the total assaults match the current filters.

The history is an in-memory buffer capped at the latest 1000 assaults. Use it to confirm that your targeting rules and level are applying to the expected endpoints before drawing conclusions about your resilience checks. Clear History wipes the buffer between test sessions — the first click arms the button, a second click within 3 seconds performs the clear.

Markdown report export

The History panel also offers an Export Markdown button. Clicking it generates a factual Markdown report of the current Goblin session and displays it in a panel with Copy and Download actions:

  • the current status (active/inactive) and target level

  • the current assault configuration (active profile, latency, exception, HTTP status, dependency degradation, response body toggles and values)

  • the full assault history, including the actual latency duration applied for each latency assault and a snapshot of the active configuration at the time of each assault — so a config change between two calls is clearly visible (e.g. latency enabled (100 - 500 ms) vs latency enabled (1000 - 5000 ms))

Goblin Markdown report export

The exported Markdown report with the assault history table.

The report is intentionally factual: it records what Goblin observed (configuration and assault history), without judging your application’s resilience. This makes it directly usable as input for an LLM assistant tasked with reviewing and hardening your endpoints — the facts ("`GET /api/orders` was subject to a 4945 ms latency at 10:37 UTC") are there, the verdict is left to the reviewer.

How it works

Goblin operates at the JAX-RS filter layer using a ContainerRequestFilter (incoming) and a ClientRequestFilter (outgoing REST Client calls):

  1. At build time, the deployment module registers both filters and the goblin feature with Quarkus.

  2. At runtime startup, the AssaultEngine is initialized: it first checks for a persisted state file (.goblin-state.json), then falls back to the configuration from application.properties. In dev mode a change listener is registered so that every subsequent config mutation automatically persists the state to disk.

  3. For every incoming HTTP request, the filter checks:

    • Is chaos active?

    • Should this specific request be affected? (based on the level percentage)

    • Is the target endpoint eligible? (based on package/annotation filters)

  4. If all conditions are met, all enabled assault types are applied in order: latency first (delay), then exception/HTTP-status/dependency-degradation (which abort the request). The response body assault is applied on the way out, in the ContainerResponseFilter, rewriting the entity before it reaches the client.

  5. An assault record is added to the history buffer (capped at 1000 entries).

For every outbound MicroProfile REST Client call, the ClientRequestFilter checks the same level gate and the client-side toggles, then applies latency and/or exception (the remote service is never reached when the exception assault fires). The filter runs early in the JAX-RS pipeline, before any endpoint logic executes. This means:

  • Exception and HTTP_STATUS assaults completely bypass your endpoint code — no side effects, no partial state mutations.

  • Latency assaults add delay before processing, simulating network or upstream slowness.

  • Client-side exceptions abort the outbound call before it is dispatched — the downstream service never sees the request.

Runtime config modification

All assault parameters can be modified at runtime through the Dev UI or via JSON-RPC. The initial config is loaded from application.properties at startup, then held in a mutable in-memory config (MutableAssaultConfig). This means you can:

  • Enable latency AND exception simultaneously to simulate a slow failure.

  • Toggle assault types on/off without restarting.

  • Change the latency range, exception class, or HTTP status code on the fly.

  • Drop the target level from 100% to 10% to test intermittent failures.

Changes take effect on the next request — no restart, no redeployment. A WARN log confirms every change in the console.

State persistence

Runtime config changes made through the Dev UI are automatically persisted to a .goblin-state.json file in the project working directory. On the next startup, Goblin checks for this file and restores the previous configuration instead of starting fresh from application.properties.

This means your chaos scenario survives restarts — no need to reconfigure the dashboard every time you restart in dev mode.

The persisted state only covers the assault configuration (profile, toggles, latency range, exception settings, HTTP status settings, response body mode/percentage, target level). The global enabled/active flag is deliberately excluded: it is always taken from quarkus.goblin.enabled at startup and, during a session, from the Dev UI toggle — a state file can never reactivate an extension you disabled via quarkus.goblin.enabled=false.

The persistence listener is only registered in dev mode, so integration or system tests never overwrite the chaos configuration you saved from the Dev UI.

Add .goblin-state.json to your .gitignore to avoid committing local chaos state to version control.

If the state file is corrupted or unreadable, Goblin logs a warning and falls back to the configuration from application.properties.

Adding your own assault type

Goblin ships with four built-in assault types and lets you register your own. Implement the io.quarkiverse.goblin.assault.Assault interface, annotate the class @ApplicationScoped, and it is discovered automatically at build time — no other wiring required:

  • type() — the AssaultType this assault represents.

  • isEnabled(config) — whether the assault should run for the current mutable configuration.

  • recordLabel() — the label used in the assault history and the Markdown report.

  • order() — the execution position in the chain. Latency (order 10) runs first; assaults that abort the request run afterwards.

  • apply(context) — perform the assault. Return AssaultOutcome.CONTINUE to let the next enabled type run, or ABORTED to stop processing. To short-circuit the request call context.getRequestContext().abortWith(…​); to throw a simulated failure, throw a RuntimeException.

At build time Goblin scans the application index for implementors of Assault and registers them as beans, so a custom type provided by your application is picked up automatically. The filter resolves the ordered list of assaults for every request and applies the enabled ones in order() sequence.

Compatibility

MicroProfile Fault Tolerance

Goblin is designed to coexist with Fault Tolerance annotations. The typical workflow:

  1. Configure chaos on unprotected endpoints (default behavior).

  2. Exclude fault-tolerant endpoints via quarkus.goblin.target.exclude-annotations.

  3. Verify that the unprotected endpoints fail as expected.

  4. Remove the exclusion, chaos-test the fault-tolerant endpoints to confirm @Timeout and @Fallback trigger correctly.

RESTEasy Reactive vs classic JAX-RS

Goblin uses standard JAX-RS ContainerRequestFilter / ContainerResponseFilter, which work with both RESTEasy Reactive and the classic RESTEasy runtime.

MicroProfile REST Client / Quarkus REST Client

Client-side assaults are implemented as a standard JAX-RS ClientRequestFilter registered as a global provider. They apply to any client built with quarkus-rest-client (the RESTEasy Reactive-based MicroProfile REST Client implementation). The filter is inert in production builds and, being opt-in, off by default, so it has no effect unless you enable the client-side toggles.

Troubleshooting

Chaos is not activating:

  • Verify you are running in dev mode (quarkus:dev), not in test or production mode.

  • Check that quarkus.goblin.enabled=true (the default).

  • Verify at least one assault type is enabled in the Dev UI.

  • Look for the WARN log at startup confirming chaos activation.

Previous config is not restored on restart:

  • Check that .goblin-state.json exists in the project working directory.

  • If the file is corrupted (e.g. manual edits), delete it and reconfigure via the Dev UI. Goblin will recreate it automatically.

  • Look for a WARN log: Failed to load .goblin-state.json, falling back to application.properties.

Endpoints are returning 500:

  • If using EXCEPTION assault, verify the exception class exists and has a String constructor.

  • Check the Quarkus dev console for stack traces.

Targeting filters not working:

  • Package matching uses startsWith, so com.example matches com.example.api and com.example.internal.

  • Annotation matching uses fully qualified class names, not simple names. Use jakarta.ws.rs.GET, not GET.

Dev UI config changes not visible:

  • Check the Quarkus console for WARN logs confirming the change.

  • Changes are persisted to .goblin-state.json and restored on restart. If the file is missing or corrupted, Goblin falls back to application.properties defaults.

End-to-end example: prove your @Timeout works

This walkthrough exercises a realistic scenario: verifying that a MicroProfile @Timeout and @Fallback react correctly when a downstream call becomes slow.

  1. Start the sample application in dev mode:

    ./mvnw -pl integration-tests quarkus:dev
  2. Configure a latency assault that exceeds your @Timeout threshold. If the downstream endpoint has a @Timeout(1000) (1 second), set a latency range well above it:

    quarkus.goblin.enabled=true
    quarkus.goblin.assault.type=LATENCY
    quarkus.goblin.assault.latency.min-milliseconds=2000
    quarkus.goblin.assault.latency.max-milliseconds=4000
    quarkus.goblin.target.level=100
    # Exclude other fault-tolerant endpoints so only the one under test is affected
  3. Open the Dev UI at http://localhost:8080/q/dev and confirm latency is enabled in the Chaos Dashboard.

  4. Call the endpoint under test and observe the response time and whether the fallback was invoked:

    curl -w "\nstatus=%{http_code} time=%{time_total}s\n" http://localhost:8080/api/hello
  5. Check the Assault History panel to confirm the latency assault was recorded against the expected method.

  6. Repeat with quarkus.goblin.target.level=10 to observe intermittent, non-deterministic failures — the hardest scenario to get right in production.

JSON-RPC reference

The Dev UI talks to the runtime through a JSON-RPC service (GoblinJsonRPCService). The same service is available over the Dev UI’s built-in JSON-RPC bridge, which is useful for scripting or CI validation. All methods read or mutate the in-memory MutableAssaultConfig.

Method Effect

getStatus

Returns active, the active profile, the four assault toggles, the two client-side assault toggles, the responseBodyEnabled toggle, and the target level.

getConfig

Returns the full mutable config (profile, toggles, client-side toggles, latency range, exception config, HTTP status config, response body config, level) plus exceptionPresets, the exception classes offered as quick picks in the Dev UI. Every preset is validated by the engine (loadable, extends RuntimeException, has a single-String constructor) so picking one never triggers the fallback.

toggleActive / setActive(boolean)

Enable/disable chaos entirely. Emits a WARN log. Returns { ok, active }.

setProfile(profile)

Activate a predefined composite assault mode (NONE, SLOW_FAILURE, INTERMITTENT, TIMEOUT). Applies the profile defaults to the assault toggles; each toggle stays overridable afterwards.

toggleLatency / toggleException / toggleHttpStatus / toggleDependencyDegradation / toggleResponseBody

Flip the corresponding assault toggle on/off.

toggleClientLatency / toggleClientException

Flip the corresponding client-side assault toggle on/off (outgoing REST Client calls).

setResponseBodyConfig(mode, percentage)

Set the response body transformation (TRUNCATE or INFLATE) and its target size in percent. Invalid modes are rejected; out-of-range percentages are clamped with the correction surfaced in warning.

setLatencyRange(minMs, maxMs)

Set the min/max latency range.

setExceptionConfig(type, message)

Set the exception class name and message. The class must extend RuntimeException and have a single-String constructor; a non-RuntimeException or unknown class is kept in the config but flagged in warning (the engine falls back to RuntimeException).

setHttpStatusConfig(code, message)

Set the HTTP status code and response body.

setTargetLevel(level)

Set the percentage of affected requests (clamped to 0-100).

disableAll()

Kill switch: deactivates the engine, disables every assault type and client-side assault, and resets the profile to NONE. Returns { ok, active: false } plus the full config.

resetDefaults()

Restore every toggle and parameter to its default value (profile NONE, latency 100-5000 ms, HTTP status 503, body TRUNCATE 50%, level 100). Returns { ok } plus the full config.

applyConfig(config)

Apply a (partial) config object: profile is applied first, then any provided field overrides — omitted fields keep their current value. Returns { ok, warning } plus the full config. Used by the dashboard’s import and custom-profile features.

getCounters

Return { total, since, byType } with the session assault counters: the total number of assaults since the engine started (or the counters were last reset), the epoch timestamp of that window start (since), and a per-type breakdown (byType).

resetCounters()

Reset the assault counters to zero. Returns { ok }.

getHistory

Return the assault history buffer. Each entry exposes the method, type, timestamp, the applied latency duration (latencyMs, 0 for non-latency assaults), and the active assault config snapshot at the time of the assault (config).

clearHistory

Empty the assault history buffer.

getMarkdownReport

Return { markdown, generatedAt } with a factual Markdown report of the current configuration and assault history.

Every mutating method returns the full configuration as JsonObject (the same shape as getConfig), so the Dev UI treats the response as a single source of truth. The legacy top-level keys (mode, percentage, minMilliseconds, maxMilliseconds, type, message, code, level) remain present for backward compatibility.

FAQ

Does Goblin run in production?

No. The chaos implementation is only activated in dev and test mode. The runtime ships in quarkus-goblin, while the Dev UI components live in the runtime-dev module (quarkus-goblin-dev), which is only attached in dev mode.

Do I need to annotate my endpoints?

No. Goblin intercepts at the JAX-RS filter level automatically. The only interface is configuration, either static (application.properties) or dynamic (Dev UI / JSON-RPC).

What is the maximum assault history size?

The history buffer is capped at the latest 1000 entries; the oldest are dropped automatically.

Can I combine latency and exception to simulate a slow failure?

Yes. Multiple assault types can be active on the same request. In that case the latency (delay) is applied first, then the exception is thrown — ideal for verifying that @Timeout triggers before @Fallback, or that a fallback handles an exception raised after a slow response.

What happens if my exception class cannot be constructed?

If the configured class does not exist or has no String constructor, Goblin falls back to a RuntimeException carrying the configured message. See the Troubleshooting section.

Are Dev UI changes persisted?

Yes. Runtime changes are automatically saved to a .goblin-state.json file in the project working directory and restored on the next startup. The file is gitignored by default.

Non-goals (out of scope for V1)

  • Infrastructure chaos (pod killing, network partitioning) — use Chaos Mesh, Litmus, or Pumba for that.

  • Production/staging chaos — may be explored in a future version with a completely different safety model (explicit opt-in, time-bounded windows, audit trail).

  • Native compilation — to be validated separately once the core mechanism is proven on JVM.

Requirements

  • Java 25 or later

  • Quarkus 3.38.3 or later