Skip to content
This repository was archived by the owner on Feb 4, 2026. It is now read-only.

Release 0.4.0#17

Merged
siy merged 27 commits intomainfrom
release/0.4.0
Dec 24, 2025
Merged

Release 0.4.0#17
siy merged 27 commits intomainfrom
release/0.4.0

Conversation

@siy
Copy link
Copy Markdown
Owner

@siy siy commented Dec 24, 2025

Summary

  • HTTP router for external request handling
  • SharedLibraryClassLoader for slice dependency isolation
  • Demo-order module with 5 slices showcasing slice patterns
  • ClusterFailoverIT for testing cluster resilience
  • C-level resilience demo with visual dashboard
  • AI layered architecture documentation (Lizard Brain → SLM → LLM → User)
  • Infrastructure services roadmap (distributed hash map, artifact repo, HTTP routing)

Test plan

  • Run ./mvnw clean install - all tests pass
  • Verify ClusterFailoverIT demonstrates proper failover
  • Review demo-order slices for correct patterns

Summary by CodeRabbit

  • New Features

    • HTTP router and standalone HTTP server for REST access; demo dashboard and an Order Demo (place/get/cancel workflows); CLI updated to v0.4.0.
  • Documentation

    • New AI integration (layered autonomy), Infrastructure Services, architecture, roadmap and demos documentation.
  • Improvements

    • Shared-library support for stronger slice isolation; enhanced routing, request binding and dependency resolution.
  • Tests

    • Expanded unit and integration tests covering HTTP server, routing, dependency parsing and slice loading.

✏️ Tip: You can customize this high-level summary in your review settings.

siy and others added 15 commits December 21, 2025 15:10
Removes package-based loading from SliceClassLoader and introduces
explicit dependency declaration via [shared] and [slices] sections.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add demo/ module with DemoCluster, LoadGenerator, DemoMetrics
- Implement REST API for cluster operations (kill, crash, add node, rolling restart)
- Create dark-themed dashboard with D3.js topology and Chart.js metrics
- Support dynamic load scaling (1K-10K req/sec)
- Include Docker/docker-compose for containerized deployment
- Dashboard shows real-time success rate, latency, and event timeline

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 24, 2025

📝 Walkthrough

Walkthrough

Adds an HTTP server and router with Netty integration, shared-library-aware slice classloading and dependency resolution, a demo-order multi-module (services and use cases), a resilience demo app and dashboard, many docs, tests, and moves routing types to org.pragmatica.aether.slice.routing. Project version bumped to 0.4.0.

Changes

Cohort / File(s) Summary
Version & Reactor
pom.xml, */pom.xml (cli/pom.xml, cluster/pom.xml, common/pom.xml, slice-api/pom.xml, slice/pom.xml, mcp/pom.xml, node/pom.xml, example-slice/pom.xml)
Bumped root and module parent versions to 0.4.0; added http-server, demo-order, and demo modules to reactor.
HTTP Server module
http-server/pom.xml, http-server/src/main/java/org/pragmatica/http/server/*, http-server/src/test/java/org/pragmatica/http/server/*
New http-server public API (HttpServer, HttpServerConfig, HttpMethod, HttpStatus, RequestContext, ResponseWriter, TlsConfig) and Netty-based implementations + tests.
HTTP Router (node)
node/pom.xml, node/src/main/java/org/pragmatica/aether/http/*, node/src/test/java/...
New router infra: RouterConfig, PathPattern, RouteMatcher, BindingResolver, SliceDispatcher, HttpRouter, HttpRouterError, MatchResult, RequestContext/ResponseWriter variants and Netty request handler wiring.
Node wiring & lifecycle
node/src/main/java/org/pragmatica/aether/node/*, node/src/main/java/org/pragmatica/aether/invoke/*, node/src/main/java/org/pragmatica/aether/invoke/SliceInvoker.java
Added optional HttpRouter via AetherNodeConfig.httpRouter and AetherNode.httpRouter(); lifecycle start/stop hooks; InvocationHandler.getLocalSlice and SliceInvoker.invokeLocal to support local dispatch.
Slice classloading & dependency resolution
slice/src/main/java/org/pragmatica/aether/slice/*, slice/src/main/java/org/pragmatica/aether/slice/dependency/*
Added SharedLibraryClassLoader, SharedDependencyLoader, DependencyFile, ArtifactDependency, CompatibilityResult; updated DependencyResolver and SliceStore/SliceStoreImpl to support shared-loader path, conflict handling, and new loading logic; simplified SliceClassLoader delegation.
Routing package reorg
slice/src/main/java/org/pragmatica/aether/slice/routing/*
Relocated routing types (Binding, BindingSource, Route, RouteTarget, RoutingSection, SliceSpec) from slice.blueprintslice.routing; updated imports/tests.
Demo: order-domain & services
demo-order/pom.xml, demo-order/order-domain/*, demo-order/inventory-service/*, demo-order/pricing-service/*
New demo-order multi-module with domain value objects (OrderId, ProductId, Money, Currency, CustomerId, Quantity), inventory & pricing service slices, POMs and META-INF dependency descriptors.
Demo: use-case slices & blueprint
demo-order/place-order/*, demo-order/get-order-status/*, demo-order/cancel-order/*, demo-order/demo-order.blueprint
New use-case slices (place-order, get-order-status, cancel-order) with DTOs, validators, errors, in-memory stores, inter-slice invocation scaffolding and HTTP→slice route mappings in blueprint.
Resilience demo app & UI
demo/pom.xml, demo/src/main/java/org/pragmatica/aether/demo/*, demo/src/main/resources/static/*, demo/Dockerfile, demo/docker-compose.yml, demo/src/main/resources/tinylog.properties
New demo application: DemoServer, DemoCluster, LoadGenerator, DemoMetrics, DemoApiHandler, StaticFileHandler, dashboard (JS/CSS/HTML), Docker artifacts and logging config.
Slice API & store changes
slice/src/main/java/.../SliceStore.java, SliceStoreImpl.java, slice/src/main/java/.../SharedLibraryClassLoader.java
Added new SliceStore overloads accepting SharedLibraryClassLoader; updated implementations to use shared loader when present.
Docs & changelog
docs/*, CHANGELOG.md, CLAUDE.md
Extensive documentation additions/updates (AI integration, infra services, classloader architecture, demos, priorities), and CHANGELOG entry for v0.4.0.
Tests & integrations
node/src/test/java/*, slice/src/test/java/*, http-server/src/test/java/*
Many new/updated tests: PathPattern, RouteMatcher, RequestContext, HttpServer integration and TLS tests, SharedLibraryClassLoader, DependencyFile, ArtifactDependency, CompatibilityResult, cluster failover ITs, router-related node ITs.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NettyServer
    participant RouteMatcher
    participant BindingResolver
    participant SliceDispatcher
    participant SliceInvoker
    participant Slice
    participant ResponseWriter

    Client->>NettyServer: HTTP request (method, path, headers, body)
    NettyServer->>RouteMatcher: match(method, path)
    RouteMatcher-->>NettyServer: MatchResult(route, pathVars)

    par Resolve bindings
      NettyServer->>BindingResolver: resolve(route.bindings, RequestContext)
      BindingResolver-->>NettyServer: resolvedParams
    end

    par Dispatch
      NettyServer->>SliceDispatcher: dispatch(route, resolvedParams)
      SliceDispatcher->>SliceInvoker: invokeLocal/remote(artifact, method, request)
      SliceInvoker->>Slice: execute(method, request)
      Slice-->>SliceInvoker: Promise(response)
      SliceInvoker-->>SliceDispatcher: Promise(response)
    end

    NettyServer->>ResponseWriter: writeSuccess(ctx, response)
    ResponseWriter->>Client: HTTP 200 + JSON
Loading
sequenceDiagram
    participant Browser
    participant DemoApiHandler
    participant DemoCluster
    participant DemoMetrics
    participant LoadGenerator

    Browser->>DemoApiHandler: GET /api/status
    DemoApiHandler->>DemoCluster: status()
    DemoCluster-->>DemoApiHandler: ClusterStatus
    DemoApiHandler->>DemoMetrics: currentMetrics()
    DemoMetrics-->>DemoApiHandler: MetricsSnapshot
    DemoApiHandler-->>Browser: JSON status

    Browser->>DemoApiHandler: POST /api/kill-node
    DemoApiHandler->>DemoCluster: killNode(nodeId)
    DemoCluster-->>DemoApiHandler: Promise<Unit>
    DemoApiHandler-->>Browser: Success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I hopped through routes and jars today,
Binding params and guiding the way.
Slices hum, dashboards glow,
Orders flow and metrics show.
A tiny hop for v0.4—hooray! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.05% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Release 0.4.0' is concise, clear, and directly reflects the main change: a version release from 0.3.0 to 0.4.0 across the entire codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch release/0.4.0

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/aether-high-level-overview.md (1)

158-159: Stale last-updated date.

The date shows 2025-08-09 but this PR is from December 2025. Consider updating to reflect the current release date.

🔎 Proposed fix
-*Last Updated: 2025-08-09*
+*Last Updated: 2025-12-24*
🟡 Minor comments (16)
slice/src/main/java/org/pragmatica/aether/slice/dependency/DependencyFile.java-151-161 (1)

151-161: Resource stream not closed on parse failure.

If parse(resource) fails, the InputStream from getResourceAsStream is never closed. Consider wrapping with try-with-resources or ensuring the stream is closed in all paths.

🔎 Proposed fix
     public static Result<DependencyFile> load(String sliceClassName, ClassLoader classLoader) {
         var resourcePath = "META-INF/dependencies/" + sliceClassName;
-        var resource = classLoader.getResourceAsStream(resourcePath);
-
-        if (resource == null) {
-            // No dependencies file means no dependencies - this is valid
-            return Result.success(new DependencyFile(List.of(), List.of()));
+        try (var resource = classLoader.getResourceAsStream(resourcePath)) {
+            if (resource == null) {
+                // No dependencies file means no dependencies - this is valid
+                return Result.success(new DependencyFile(List.of(), List.of()));
+            }
+            return parse(resource);
+        } catch (IOException e) {
+            return Causes.fromThrowable(e).result();
         }
-
-        return parse(resource);
     }
slice/src/main/java/org/pragmatica/aether/slice/dependency/DependencyResolver.java-134-134 (1)

134-134: Temporary SliceClassLoader is never closed.

createTempLoader at line 134 creates a SliceClassLoader for reading the dependency file, but it's never closed. This can lead to resource leaks (JAR file handles).

The loader should be closed after DependencyFile.load completes:

🔎 Proposed fix
-        // Load dependency file to get shared and slice dependencies
-        return DependencyFile.load(manifest.sliceClassName(), createTempLoader(location.url(), sharedLibraryLoader))
-                             .async()
+        // Load dependency file to get shared and slice dependencies
+        var tempLoader = createTempLoader(location.url(), sharedLibraryLoader);
+        return DependencyFile.load(manifest.sliceClassName(), tempLoader)
+                             .onSuccessRun(() -> closeSilently(tempLoader))
+                             .onFailureRun(() -> closeSilently(tempLoader))
+                             .async()

Add a helper method:

private static void closeSilently(SliceClassLoader loader) {
    try {
        loader.close();
    } catch (IOException e) {
        log.warn("Failed to close temp classloader: {}", e.getMessage());
    }
}

Committable suggestion skipped: line range outside the PR's diff.

node/src/main/java/org/pragmatica/aether/http/BindingResolver.java-92-104 (1)

92-104: Release the ByteBuf after deserialization.

The Unpooled.wrappedBuffer(body) creates a ByteBuf that should be explicitly released after use. The Deserializer.decode() method establishes the expected pattern: wrap bytes in a buffer, call read(), and release in a finally block. Wrap the deserializer.read(buf) call in a try-finally to ensure the buffer is released, even if an exception occurs during deserialization.

demo-order/inventory-service/src/main/java/org/pragmatica/aether/demo/order/inventory/CheckStockRequest.java-3-3 (1)

3-3: Add validation to prevent invalid quantity values.

The quantity parameter accepts negative or zero values, which are invalid for stock check requests. Consider adding a compact constructor to validate that productId is non-null/non-empty and quantity is positive.

🔎 Proposed validation enhancement
-public record CheckStockRequest(String productId, int quantity) {}
+public record CheckStockRequest(String productId, int quantity) {
+    public CheckStockRequest {
+        if (productId == null || productId.isBlank()) {
+            throw new IllegalArgumentException("productId must not be null or blank");
+        }
+        if (quantity <= 0) {
+            throw new IllegalArgumentException("quantity must be positive");
+        }
+    }
+}
node/src/main/java/org/pragmatica/aether/http/HttpMethod.java-15-26 (1)

15-26: Fix locale-dependent string comparison.

The toUpperCase() call without a Locale parameter is locale-dependent and can cause unexpected behavior in certain locales (e.g., Turkish locale where "i".toUpperCase() != "I"). For consistent, locale-independent parsing of HTTP methods, use toUpperCase(Locale.ROOT) or Locale.ENGLISH.

Details 🔎 Proposed fix
+import java.util.Locale;
+
 public static HttpMethod fromString(String method) {
-    return switch (method.toUpperCase()) {
+    return switch (method.toUpperCase(Locale.ROOT)) {
         case "GET" -> GET;
         case "POST" -> POST;
         case "PUT" -> PUT;
         case "DELETE" -> DELETE;
         case "PATCH" -> PATCH;
         case "HEAD" -> HEAD;
         case "OPTIONS" -> OPTIONS;
         default -> throw new IllegalArgumentException("Unknown HTTP method: " + method);
     };
 }

Committable suggestion skipped: line range outside the PR's diff.

http-server/src/test/java/org/pragmatica/http/server/HttpServerIntegrationTest.java-15-38 (1)

15-38: Incomplete test: no assertions after server starts.

This test starts the server with port 0 but the comments indicate awareness that getting the actual port is unresolved. The test has no HTTP request or assertion, making it effectively a no-op.

Either complete the test by retrieving the actual bound port from the server, or remove it to avoid confusion.

demo/src/main/java/org/pragmatica/aether/demo/StaticFileHandler.java-59-63 (1)

59-63: Directory traversal check is vulnerable to percent-encoded path bypass.

The check for ".." at line 60 occurs on the raw URI before any URL decoding. Netty's HttpRequest.uri() returns percent-encoded paths unchanged. A request like %2e%2e/etc/passwd will pass the check, and depending on the ClassLoader implementation, getResourceAsStream() at line 89 may decode the percent-encoded bytes during resource lookup, enabling directory traversal.

Decode the path before the security check:

+import java.net.URLDecoder;
+
 // Security: prevent directory traversal
+path = URLDecoder.decode(path, StandardCharsets.UTF_8);
 if (path.contains("..")) {
     sendError(ctx, FORBIDDEN, "Invalid path");
     return;
 }

Committable suggestion skipped: line range outside the PR's diff.

demo-order/place-order/src/main/java/org/pragmatica/aether/demo/order/usecase/placeorder/PlaceOrderSlice.java-101-132 (1)

101-132: Missing compensation for partial reservation failures.

If some reservations succeed but others fail, the successful reservations are not released. This leaves stock in an inconsistent state.

For a demo this may be acceptable, but worth noting for production readiness.

🔎 Suggested approach
// On failure, release successful reservations before returning error
if (successful.size() != results.size()) {
    // Release already-made reservations
    successful.forEach(res -> 
        invoker.invokeAndWait(INVENTORY, 
            MethodName.methodName("releaseStock").unwrap(),
            new ReleaseStockRequest(res.reservationId()),
            StockReleased.class));
    
    return new PlaceOrderError.ReservationFailed(
        Causes.cause("Failed to reserve all items")
    ).promise();
}
demo/src/main/java/org/pragmatica/aether/demo/DemoApiHandler.java-271-278 (1)

271-278: JSON escaping is incomplete.

Missing escape sequences for backspace (\b) and form feed (\f). Also, Unicode control characters should be escaped as \uXXXX.

For a demo with controlled inputs, this is acceptable but could cause malformed JSON with edge-case strings.

demo-order/place-order/src/main/java/org/pragmatica/aether/demo/order/usecase/placeorder/PlaceOrderSlice.java-134-141 (1)

134-141: Created order is not persisted for CancelOrderSlice.

The order is returned but not stored in CancelOrderSlice.ORDERS. This means newly placed orders cannot be cancelled. Either persist here or document that cancellation only works for pre-seeded orders.

🔎 Suggested fix
     private PlaceOrderResponse createOrder(ValidWithReservations context) {
+        // Store order for cancellation support
+        CancelOrderSlice.storeOrderWithReservations(
+            context.orderId(),
+            OrderStatus.CONFIRMED,
+            context.reservations().stream().map(StockReservation::reservationId).toList()
+        );
+
         return new PlaceOrderResponse(
             context.orderId(),
             OrderStatus.CONFIRMED,

Committable suggestion skipped: line range outside the PR's diff.

demo-order/cancel-order/src/main/java/org/pragmatica/aether/demo/order/usecase/cancelorder/CancelOrderSlice.java-106-129 (1)

106-129: Stock release calls may not be parallel and error wrapping is semantically incorrect.

  1. The stream().map().toList() pattern collects all Promises but whether they execute in parallel depends on invokeAndWait implementation. Consider using explicit parallel execution if parallelism is intended.

  2. Line 122-124: StockReleaseFailed wraps InvalidRequest, which is semantically misleading. The inner cause should describe the actual release failure.

🔎 Suggested fix for error wrapping
                          if (!allSuccess) {
-                              // In production, we'd handle partial releases
-                              return new CancelOrderError.StockReleaseFailed(
-                                  new CancelOrderError.InvalidRequest("Some reservations could not be released")
-                              ).promise();
+                              return new CancelOrderError.StockReleaseFailed(
+                                  Causes.cause("Some reservations could not be released")
+                              ).promise();
                          }

Committable suggestion skipped: line range outside the PR's diff.

demo/src/main/java/org/pragmatica/aether/demo/DemoApiHandler.java-206-216 (1)

206-216: Race condition in event list trimming.

CopyOnWriteArrayList.removeFirst() is not atomic with the size() check. Under concurrent access, multiple threads could attempt removal simultaneously. Additionally, removeFirst() on CopyOnWriteArrayList is O(n) as it copies the entire array.

For a demo with low event frequency this is likely fine, but worth noting.

🔎 Suggested alternative using bounded queue
-    private final List<DemoEvent> events = new CopyOnWriteArrayList<>();
+    private final Deque<DemoEvent> events = new ConcurrentLinkedDeque<>();
...
     public void addEvent(String type, String message) {
         var event = new DemoEvent(Instant.now().toString(), type, message);
         events.add(event);
-
-        // Keep only last 100 events
-        while (events.size() > 100) {
-            events.removeFirst();
+        // Trim excess - approximate, but avoids expensive operations
+        while (events.size() > 100) {
+            events.pollFirst();
         }

Committable suggestion skipped: line range outside the PR's diff.

demo/src/main/resources/static/dashboard.js-128-138 (1)

128-138: Simulation tick handler is re-registered on every update.

Each call to updateTopology adds a new on('tick', ...) handler without removing the previous one. This could cause performance degradation over time with multiple handlers executing.

🔎 Suggested fix
     // Update simulation
     simulation.nodes(nodeData);
     simulation.force('link').links(links);
     simulation.alpha(0.3).restart();

+    // Remove previous tick handler before adding new one
+    simulation.on('tick', null);
     simulation.on('tick', () => {
         svg.selectAll('.link')
demo-order/inventory-service/src/main/java/org/pragmatica/aether/demo/order/inventory/InventoryServiceSlice.java-70-90 (1)

70-90: Race condition in reserveStock (demo-acceptable).

There's a TOCTOU gap between reading available (line 72) and the compute call (line 84). Concurrent reservations could drive stock negative since the compute lambda doesn't re-verify sufficiency:

STOCK.compute(productId, (k, v) -> v - requested);  // v could now be < requested

For production, wrap the entire check-and-reserve in a single atomic operation:

STOCK.compute(productId, (k, v) -> {
    if (v == null || v < requested) return v;  // signal failure differently
    return v - requested;
});

Since this is demo code, this is acceptable, but worth noting for future reference.

demo/src/main/java/org/pragmatica/aether/demo/DemoCluster.java-252-267 (1)

252-267: Potential NPE in status() if maps are inconsistent.

At line 257, nodeInfos.get(entry.getKey()) could return null if a node exists in nodes but not in nodeInfos, causing a NPE when calling .address().port(). This could happen during concurrent add/remove operations.

🔎 Proposed defensive fix
     public ClusterStatus status() {
         var nodeStatuses = nodes.entrySet()
                                 .stream()
+                                .filter(entry -> nodeInfos.containsKey(entry.getKey()))
                                 .map(entry -> new NodeStatus(
                                         entry.getKey(),
                                         nodeInfos.get(entry.getKey()).address().port(),
node/src/main/java/org/pragmatica/aether/http/HttpRouter.java-126-142 (1)

126-142: Graceful shutdown doesn't wait for completion.

The stop() method calls shutdownGracefully() but doesn't wait for the event loop groups to actually shut down before resolving the promise. This could cause resource leaks or race conditions if the node is restarted quickly.

🔎 Proposed fix to await graceful shutdown
     @Override
     public Promise<Unit> stop() {
         return Promise.promise(promise -> {
             if (serverChannel != null) {
                 serverChannel.close().addListener(f -> {
-                    bossGroup.shutdownGracefully();
-                    workerGroup.shutdownGracefully();
-                    log.info("HTTP router stopped");
-                    promise.succeed(unit());
+                    var bossFuture = bossGroup.shutdownGracefully();
+                    var workerFuture = workerGroup.shutdownGracefully();
+                    bossFuture.addListener(bf -> 
+                        workerFuture.addListener(wf -> {
+                            log.info("HTTP router stopped");
+                            promise.succeed(unit());
+                        })
+                    );
                 });
             } else {
-                bossGroup.shutdownGracefully();
-                workerGroup.shutdownGracefully();
-                promise.succeed(unit());
+                var bossFuture = bossGroup.shutdownGracefully();
+                var workerFuture = workerGroup.shutdownGracefully();
+                bossFuture.addListener(bf -> 
+                    workerFuture.addListener(wf -> promise.succeed(unit()))
+                );
             }
         });
     }

Comment on lines +490 to +514
function updateTimeline(newEvents) {
const timeline = document.getElementById('timeline');

// Check for new events
if (newEvents.length > events.length) {
const added = newEvents.slice(events.length);
added.forEach(event => {
const div = document.createElement('div');
div.className = 'timeline-event';
div.innerHTML = `
<span class="event-time">${formatEventTime(event.timestamp)}</span>
<span class="event-type ${event.type}">${event.type}</span>
<span class="event-message">${event.message}</span>
`;
timeline.insertBefore(div, timeline.firstChild);
});

// Keep only last 20 events in DOM
while (timeline.children.length > 20) {
timeline.removeChild(timeline.lastChild);
}
}

events = newEvents;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential XSS vulnerability and timeline reset handling.

  1. innerHTML is used with event.message which comes from the server. If an attacker controls event messages, they could inject malicious HTML/JS.

  2. The timeline logic assumes newEvents.length > events.length to detect new events. After a server-side reset (via /api/reset-metrics), newEvents will be shorter, causing the condition to fail and stale DOM elements to remain.

🔎 Suggested fix for XSS
+function escapeHtml(text) {
+    const div = document.createElement('div');
+    div.textContent = text;
+    return div.innerHTML;
+}

 function updateTimeline(newEvents) {
     const timeline = document.getElementById('timeline');

-    // Check for new events
-    if (newEvents.length > events.length) {
+    // Handle reset case
+    if (newEvents.length < events.length) {
+        timeline.innerHTML = '';
+        events = [];
+    }
+
+    if (newEvents.length > events.length) {
         const added = newEvents.slice(events.length);
         added.forEach(event => {
             const div = document.createElement('div');
             div.className = 'timeline-event';
-            div.innerHTML = `
-                <span class="event-time">${formatEventTime(event.timestamp)}</span>
-                <span class="event-type ${event.type}">${event.type}</span>
-                <span class="event-message">${event.message}</span>
-            `;
+            div.innerHTML = `
+                <span class="event-time">${escapeHtml(formatEventTime(event.timestamp))}</span>
+                <span class="event-type ${escapeHtml(event.type)}">${escapeHtml(event.type)}</span>
+                <span class="event-message">${escapeHtml(event.message)}</span>
+            `;
             timeline.insertBefore(div, timeline.firstChild);
         });
🤖 Prompt for AI Agents
In demo/src/main/resources/static/dashboard.js around lines 490 to 514, the code
uses innerHTML to inject server-provided event.message (XSS risk) and only
appends new events when newEvents.length > events.length (so DOM isn’t
reconciled when server resets or sends fewer events). Fix by escaping or
avoiding innerHTML: create elements and set their text via textContent or
createTextNode for event.message, event.type and time instead of templated
innerHTML; and change the update logic to reconcile the DOM whenever newEvents
differs from events (if newEvents is shorter or not strictly longer, rebuild the
timeline from newEvents or remove stale nodes before inserting added events),
then enforce trimming to the last 20 items and finally assign events =
newEvents.

Comment on lines +161 to +169
private static Version extractVersion(VersionPattern pattern) {
return switch (pattern) {
case VersionPattern.Exact(Version version) -> version;
case VersionPattern.Range(Version from, _, _, _) -> from;
case VersionPattern.Comparison(_, Version version) -> version;
case VersionPattern.Tilde(Version version) -> version;
case VersionPattern.Caret(Version version) -> version;
};
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find the Repository interface definition
fd -t f "Repository.java"

Repository: siy/aether

Length of output: 214


🏁 Script executed:

# Search for the Repository.locate method definition
rg "interface Repository" -A 20

Repository: siy/aether

Length of output: 3473


🏁 Script executed:

# Search for Artifact.artifact method
rg "static.*artifact\(" -A 5 -B 2

Repository: siy/aether

Length of output: 5100


🏁 Script executed:

# Check how extractVersion is used in the codebase
rg "extractVersion" -B 3 -A 3

Repository: siy/aether

Length of output: 5715


🏁 Script executed:

# Check LocalRepository implementation
cat slice/src/main/java/org/pragmatica/aether/slice/repository/maven/LocalRepository.java

Repository: siy/aether

Length of output: 3002


🏁 Script executed:

# Search for other Repository implementations
fd -t f -path "*/repository/*" -name "*.java" | head -20

Repository: siy/aether

Length of output: 222


🏁 Script executed:

# Check how the dependency resolution actually works - look at the caller context
rg "toArtifact.*dependency" -B 5 -A 10

Repository: siy/aether

Length of output: 8792


extractVersion returns an arbitrary version from the pattern rather than resolving to an available artifact.

For Range and Comparison patterns, extracting a lower bound or base version does not guarantee that version exists in the repository. Since Repository.locate() performs a direct file lookup for the exact version (e.g., {repo}/{groupId}/{artifactId}/1.0.0/artifact-1.0.0.jar), specifying a pattern like [1.0.0,2.0.0) will fail if only 1.5.0 is available—the method will return "artifact not found" rather than finding a compatible version within the range.

🤖 Prompt for AI Agents
In
slice/src/main/java/org/pragmatica/aether/slice/dependency/SharedDependencyLoader.java
around lines 161 to 169, extractVersion currently returns an arbitrary bound
(e.g., lower bound for Range) which may not exist in the repository; change the
logic so Range and Comparison patterns are resolved against the repository’s
available versions and return an actual existing version: update the method to
accept (or make a new helper that accepts) the Repository (or a list of
available versions) and, for Range/Comparison, query the repo for candidate
versions, filter them by the pattern predicate, choose the best match (e.g.,
highest compatible version), and return that Version (or Optional<Version>) so
Repository.locate() receives an existing version; if no compatible version is
found surface a clear "artifact not found" result instead of returning the
pattern bound.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. This is a known limitation of the current implementation. Version range resolution against available artifacts is planned for when the artifact repository infrastructure is implemented. The current code works for exact versions; range patterns will fail gracefully with 'artifact not found' until proper resolution is added.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@siy, thank you for acknowledging this limitation. I understand that version range resolution will be implemented later when the artifact repository infrastructure is in place, and that the current graceful failure with 'artifact not found' is an acceptable interim behavior. I've noted this for future reviews.


✏️ Learnings added
Learnt from: siy
Repo: siy/aether PR: 17
File: slice/src/main/java/org/pragmatica/aether/slice/dependency/SharedDependencyLoader.java:161-169
Timestamp: 2025-12-24T21:31:46.925Z
Learning: In SharedDependencyLoader.java, the extractVersion method returns an arbitrary version from patterns (e.g., lower bound for Range/Comparison) rather than resolving against available repository versions. This is a known limitation that will be addressed when artifact repository infrastructure is implemented. Current behavior: works correctly for exact versions, range patterns fail gracefully with 'artifact not found'.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +104 to 116
private Promise<LoadedSlice> loadWithSharedLoader(Artifact artifact, SharedLibraryClassLoader shared) {
return DependencyResolver.resolve(artifact, compositeRepository(), registry, shared)
.map(slice -> {
// Extract the classloader from the slice's class
var sliceClassLoader = slice.getClass().getClassLoader();
if (sliceClassLoader instanceof SliceClassLoader scl) {
return createEntry(artifact, slice, scl);
}
// Fallback - create a minimal classloader entry
return createEntry(artifact, slice, new SliceClassLoader(new URL[0], shared));
})
.onFailure(cause -> log.error("Failed to load slice {}: {}", artifact, cause.message()));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fallback creates SliceClassLoader without slice JAR URL.

At line 113, if the slice's class loader is not a SliceClassLoader, the fallback creates one with new URL[0]. This means the slice won't have access to its own JAR resources, which could cause ClassNotFoundException or missing resources.

This appears to be a "should never happen" defensive case. Consider logging a warning or throwing an error instead.

🔎 Proposed fix - log warning
                         // Fallback - create a minimal classloader entry
+                        log.warn("Slice {} loaded with unexpected classloader type: {}. Resource access may be limited.",
+                                 artifact, sliceClassLoader.getClass().getName());
                         return createEntry(artifact, slice, new SliceClassLoader(new URL[0], shared));

Or consider failing explicitly if this state is unexpected:

-                        // Fallback - create a minimal classloader entry
-                        return createEntry(artifact, slice, new SliceClassLoader(new URL[0], shared));
+                        throw new IllegalStateException(
+                            "Slice " + artifact + " loaded with unexpected classloader: " + sliceClassLoader.getClass());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Promise<LoadedSlice> loadWithSharedLoader(Artifact artifact, SharedLibraryClassLoader shared) {
return DependencyResolver.resolve(artifact, compositeRepository(), registry, shared)
.map(slice -> {
// Extract the classloader from the slice's class
var sliceClassLoader = slice.getClass().getClassLoader();
if (sliceClassLoader instanceof SliceClassLoader scl) {
return createEntry(artifact, slice, scl);
}
// Fallback - create a minimal classloader entry
return createEntry(artifact, slice, new SliceClassLoader(new URL[0], shared));
})
.onFailure(cause -> log.error("Failed to load slice {}: {}", artifact, cause.message()));
}
private Promise<LoadedSlice> loadWithSharedLoader(Artifact artifact, SharedLibraryClassLoader shared) {
return DependencyResolver.resolve(artifact, compositeRepository(), registry, shared)
.map(slice -> {
// Extract the classloader from the slice's class
var sliceClassLoader = slice.getClass().getClassLoader();
if (sliceClassLoader instanceof SliceClassLoader scl) {
return createEntry(artifact, slice, scl);
}
// Fallback - create a minimal classloader entry
log.warn("Slice {} loaded with unexpected classloader type: {}. Resource access may be limited.",
artifact, sliceClassLoader.getClass().getName());
return createEntry(artifact, slice, new SliceClassLoader(new URL[0], shared));
})
.onFailure(cause -> log.error("Failed to load slice {}: {}", artifact, cause.message()));
}
Suggested change
private Promise<LoadedSlice> loadWithSharedLoader(Artifact artifact, SharedLibraryClassLoader shared) {
return DependencyResolver.resolve(artifact, compositeRepository(), registry, shared)
.map(slice -> {
// Extract the classloader from the slice's class
var sliceClassLoader = slice.getClass().getClassLoader();
if (sliceClassLoader instanceof SliceClassLoader scl) {
return createEntry(artifact, slice, scl);
}
// Fallback - create a minimal classloader entry
return createEntry(artifact, slice, new SliceClassLoader(new URL[0], shared));
})
.onFailure(cause -> log.error("Failed to load slice {}: {}", artifact, cause.message()));
}
private Promise<LoadedSlice> loadWithSharedLoader(Artifact artifact, SharedLibraryClassLoader shared) {
return DependencyResolver.resolve(artifact, compositeRepository(), registry, shared)
.map(slice -> {
// Extract the classloader from the slice's class
var sliceClassLoader = slice.getClass().getClassLoader();
if (sliceClassLoader instanceof SliceClassLoader scl) {
return createEntry(artifact, slice, scl);
}
throw new IllegalStateException(
"Slice " + artifact + " loaded with unexpected classloader: " + sliceClassLoader.getClass());
})
.onFailure(cause -> log.error("Failed to load slice {}: {}", artifact, cause.message()));
}
🤖 Prompt for AI Agents
In slice/src/main/java/org/pragmatica/aether/slice/SliceStoreImpl.java around
lines 104 to 116, the fallback creates a SliceClassLoader with an empty URL
array which prevents the slice from accessing its own JAR; change the fallback
to extract the slice JAR URL from
slice.getClass().getProtectionDomain().getCodeSource().getLocation() (guarding
for null), construct a URL[] containing that location and pass it into the new
SliceClassLoader so the slice has its resources; if the
ProtectionDomain/CodeSource or URL is null, log a warning with artifact details
and return a failed Promise (or throw an IllegalStateException) rather than
silently creating an empty classloader.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
demo/src/main/resources/static/dashboard.js (1)

492-539: XSS mitigation and timeline reset handling properly addressed.

The previous review concerns have been resolved:

  • escapeHtml function added and used for safe output
  • Reset case handled when newEvents.length < events.length
  • DOM elements created with textContent instead of innerHTML template literals
🧹 Nitpick comments (5)
docs/aether-high-level-overview.md (1)

141-147: Clarify Phase 3 status hierarchy.

Phase 3 is labeled "Planned," but Line 144 indicates "Layer 1: Decision tree controller (done)," creating ambiguity about which items are actually completed versus planned. Consider restructuring this section to make the per-item status more explicit.

🔎 Suggested revision for clarity
~### Phase 3: AI Integration (Planned)
+### Phase 3: AI Integration (In Progress)
~
~- Layered autonomy architecture (see [ai-integration.md](ai-integration.md))
~- Layer 1: Decision tree controller (done)
~- Layer 2: SLM integration (planned)
~- Layer 3: LLM integration (planned)
~- Direct agent API (no MCP)
+- Layered autonomy architecture (see [ai-integration.md](ai-integration.md))
+  - Layer 1: Decision tree controller ✅
+  - Layer 2: SLM integration (planned)
+  - Layer 3: LLM integration (planned)
+  - Direct agent API (no MCP)
demo/src/main/resources/static/dashboard.js (1)

541-545: Consider handling fractional seconds.

If status.uptimeSeconds contains a fractional value (e.g., 65.5), the modulo operation will produce 5.5, and the display may show "1:5.5" instead of "1:05".

🔎 Suggested fix
 function formatUptime(seconds) {
-    const mins = Math.floor(seconds / 60);
-    const secs = seconds % 60;
+    const totalSecs = Math.floor(seconds);
+    const mins = Math.floor(totalSecs / 60);
+    const secs = totalSecs % 60;
     return `${mins}:${secs.toString().padStart(2, '0')}`;
 }
demo/src/main/java/org/pragmatica/aether/demo/StaticFileHandler.java (3)

60-67: URL decoding before security check is correct.

The implementation correctly decodes the URL (line 61) before checking for directory traversal (line 64), which prevents bypass attacks using percent-encoded paths like %2e%2e. This aligns with the security fix mentioned in the PR commit messages.


Consider more robust path normalization.

While the current contains("..") check is effective for this use case, consider using java.nio.file.Path normalization for additional robustness against edge cases like ./, multiple slashes, or other path manipulation attempts.

🔎 Example approach using Path normalization
import java.nio.file.Path;
import java.nio.file.Paths;

// After URL decoding
path = URLDecoder.decode(path, StandardCharsets.UTF_8);

// Normalize and verify the path stays within bounds
try {
    var normalizedPath = Paths.get(path).normalize().toString();
    if (normalizedPath.contains("..") || !normalizedPath.startsWith("/")) {
        sendError(ctx, FORBIDDEN, "Invalid path");
        return;
    }
    path = normalizedPath;
} catch (Exception e) {
    sendError(ctx, FORBIDDEN, "Invalid path");
    return;
}

This would handle edge cases more comprehensively while maintaining the same security guarantee.


92-102: Resource loading implementation is correct for demo scope.

The implementation properly uses try-with-resources and loads the entire resource into memory using readAllBytes(). This is acceptable for a demo serving typical web assets (HTML, CSS, JavaScript, images).


For production, consider streaming large files.

Loading entire files into memory works well for small static assets but could be memory-intensive for larger files. For a production static file server, consider using Netty's ChunkedWriteHandler and DefaultFileRegion or ChunkedFile for efficient streaming.


89-89: Consider HTTP keep-alive for production use.

The handler closes the connection after every response (line 89, also line 118 in sendError). This is simple and acceptable for a demo, but in a production environment, supporting HTTP keep-alive would reduce connection overhead and improve performance.

To support keep-alive, check if the request includes Connection: keep-alive and conditionally use ChannelFutureListener.CLOSE only when needed.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d72cb77 and 8f06e4c.

📒 Files selected for processing (6)
  • demo/src/main/java/org/pragmatica/aether/demo/StaticFileHandler.java
  • demo/src/main/resources/static/dashboard.js
  • docs/aether-high-level-overview.md
  • http-server/src/main/java/org/pragmatica/http/server/RequestContext.java
  • http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java
  • node/src/main/java/org/pragmatica/aether/http/HttpMethod.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • node/src/main/java/org/pragmatica/aether/http/HttpMethod.java
  • http-server/src/main/java/org/pragmatica/http/server/RequestContext.java
🧰 Additional context used
🧬 Code graph analysis (1)
demo/src/main/java/org/pragmatica/aether/demo/StaticFileHandler.java (1)
demo/src/main/java/org/pragmatica/aether/demo/DemoServer.java (1)
  • ChannelHandler (210-252)
🔇 Additional comments (15)
docs/aether-high-level-overview.md (2)

5-5: Documentation updates align well with release objectives.

The v0.4.0 updates accurately reflect the PR objectives: version bump, module restructuring (demo-order replaces example-slice, cli added), Phase 2 completion with HTTP Router, Management API, Health monitoring, and CLI implementation, and the timestamp refresh. The document is well-structured and comprehensive.

Also applies to: 40-41, 135-139, 158-158


72-72: No action needed. The file ai-integration.md exists in the docs/ directory and the markdown links at lines 72 and 143 are correctly formatted with valid relative paths.

demo/src/main/resources/static/dashboard.js (7)

1-17: LGTM!

State initialization and constants are well-organized. The MAX_HISTORY calculation correctly corresponds to 30 seconds at 500ms intervals.


19-25: LGTM!

Clean initialization sequence using DOMContentLoaded.


128-139: Good fix for tick handler accumulation.

Clearing the previous tick handler with simulation.on('tick', null) before setting a new one correctly prevents handler accumulation on topology updates.


60-127: LGTM!

The D3 data-join pattern (enter/update/exit) is correctly implemented. The full-mesh link generation is appropriate for visualizing small cluster topologies.


163-289: LGTM!

Chart configuration is well-structured with:

  • Bounded history arrays preventing memory growth
  • Disabled animations for high-frequency updates
  • Color-coded thresholds for visual feedback

295-383: LGTM!

Control handlers are well-implemented:

  • Safe DOM manipulation using textContent
  • Reset button properly clears local history state
  • Modal pattern cleanly manages node selection

547-550: LGTM!

Standard date parsing and locale-aware formatting.

http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java (5)

22-29: LGTM: Clean handler structure.

The class properly extends SimpleChannelInboundHandler and follows standard Netty patterns. The constructor injection of the handler is appropriate.


52-56: LGTM: Proper exception handling.

The exception handler correctly logs the error and closes the channel, following standard Netty patterns.


87-90: LGTM: Appropriate bad request handling.

The method correctly creates a response writer with keepAlive=false and sends a BAD_REQUEST response. Closing the connection on malformed requests is appropriate.


47-49: Finally block properly addresses the buffer leak.

The context.release() call in the finally block correctly ensures that RequestContext.release() is invoked regardless of handler success or exception. The RequestContext.release() method properly handles buffer cleanup by checking the reference count and calling body.release() to decrement it.


58-77: Path is passed to downstream handlers without decoding or validation.

The path extracted from the URI is passed directly to RequestContext without any decoding, normalization, or traversal prevention checks. Unlike query parameters (which are properly decoded with URLDecoder), the path remains raw. This means:

  • Percent-encoded paths (e.g., %2e%2e%2f) are not decoded
  • Directory traversal sequences (../) are not filtered
  • Paths are not normalized for consistent routing

The responsibility for these security checks is delegated entirely to downstream handlers. While this separation of concerns is reasonable, the asymmetry with query parameter handling (which ARE decoded) may mask security issues if handlers assume normalized paths.

demo/src/main/java/org/pragmatica/aether/demo/StaticFileHandler.java (1)

24-43: Well-structured handler with appropriate patterns.

The class correctly uses the @Sharable annotation with a stateless design, implements the factory method pattern, and properly extends SimpleChannelInboundHandler. The implementation is clean and well-organized for its demo scope.

Comment on lines +45 to +67
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
var path = request.uri();

// Handle root path
if (path.equals("/") || path.equals("/index.html")) {
path = "/index.html";
}

// Remove query string
var queryIdx = path.indexOf('?');
if (queryIdx >= 0) {
path = path.substring(0, queryIdx);
}

// Decode URL before security check to prevent bypass via percent-encoding
path = URLDecoder.decode(path, StandardCharsets.UTF_8);

// Security: prevent directory traversal
if (path.contains("..")) {
sendError(ctx, FORBIDDEN, "Invalid path");
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add HTTP method validation for proper REST semantics.

Static file handlers should typically only respond to GET and HEAD requests. Currently, the handler will serve files for any HTTP method (POST, PUT, DELETE, etc.), which violates HTTP semantics for static resources.

🔎 Proposed fix
 @Override
 protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
+    // Only handle GET and HEAD requests
+    if (request.method() != HttpMethod.GET && request.method() != HttpMethod.HEAD) {
+        sendError(ctx, METHOD_NOT_ALLOWED, "Method not allowed");
+        return;
+    }
+
     var path = request.uri();

     // Handle root path

For HEAD requests, you may also want to avoid sending the response body while keeping the headers.


Simplify redundant root path condition.

Line 50 checks for both "/" and "/index.html", but if the path is already "/index.html", setting it to "/index.html" is redundant.

🔎 Proposed fix
-    // Handle root path
-    if (path.equals("/") || path.equals("/index.html")) {
-        path = "/index.html";
-    }
+    // Handle root path
+    if (path.equals("/")) {
+        path = "/index.html";
+    }
🤖 Prompt for AI Agents
In demo/src/main/java/org/pragmatica/aether/demo/StaticFileHandler.java around
lines 45 to 67, add HTTP method validation so the handler only serves GET and
HEAD: read the request.method(), if it's neither GET nor HEAD respond with 405
Method Not Allowed (and close or set Allow: GET, HEAD header) and return; for
HEAD requests perform the same header generation as GET but do not write the
response body. Also simplify the root-path logic by replacing the redundant
check (path.equals("/index.html") || path.equals("/")) with a single condition
that maps only "/" to "/index.html" (leave "/index.html" unchanged). Ensure
these checks occur before any file lookup or body generation and keep the
existing query-string removal, URL decoding, and directory traversal check in
place.

Comment on lines +411 to +423
async function apiPost(endpoint, body = {}) {
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return await response.json();
} catch (e) {
console.error('API error:', e);
return null;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing response.ok check before parsing JSON.

Unlike fetchStatus and fetchEvents, apiPost doesn't verify response.ok before calling response.json(). If the server returns an error status with a non-JSON body, this will throw an exception that gets caught but may mask the actual HTTP error.

🔎 Suggested fix
 async function apiPost(endpoint, body = {}) {
     try {
         const response = await fetch(`${API_BASE}${endpoint}`, {
             method: 'POST',
             headers: { 'Content-Type': 'application/json' },
             body: JSON.stringify(body)
         });
+        if (!response.ok) {
+            console.error('API error:', response.status, response.statusText);
+            return null;
+        }
         return await response.json();
     } catch (e) {
         console.error('API error:', e);
         return null;
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function apiPost(endpoint, body = {}) {
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return await response.json();
} catch (e) {
console.error('API error:', e);
return null;
}
}
async function apiPost(endpoint, body = {}) {
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
console.error('API error:', response.status, response.statusText);
return null;
}
return await response.json();
} catch (e) {
console.error('API error:', e);
return null;
}
}
🤖 Prompt for AI Agents
In demo/src/main/resources/static/dashboard.js around lines 411 to 423, apiPost
currently calls response.json() unconditionally which can throw or mask HTTP
errors; update it to check response.ok first: if response.ok parse and return
JSON, otherwise attempt to parse JSON (catching parse errors) or fallback to
response.text(), log the status and body/error, and return null (or a structured
error) so HTTP error details aren't lost.

query = null;
}

var method = HttpMethod.from(request.method().name());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find HttpMethod class file
fd -n 'HttpMethod' --type f | head -20

# If found, examine the file
fd -n 'HttpMethod.java' --type f -exec cat -n {} \;

Repository: siy/aether

Length of output: 515


🏁 Script executed:

#!/bin/bash
# Find HttpMethod class file
find . -type f -name '*HttpMethod*'

# Search for HttpMethod.from() implementation
rg -A 10 'public static.*from\(' --type java

Repository: siy/aether

Length of output: 1433


🏁 Script executed:

cat http-server/src/main/java/org/pragmatica/http/server/HttpMethod.java

Repository: siy/aether

Length of output: 824


Fix locale-independent conversion: use Locale.ROOT in HttpMethod.from().

The from() method currently uses method.toUpperCase() without specifying Locale.ROOT, which contradicts the commit message. Replace with method.toUpperCase(Locale.ROOT) to ensure locale-independent case conversion and avoid issues in locales like Turkish where character case conversions differ from the default English behavior.

🤖 Prompt for AI Agents
In
http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java
around line 72, the call that converts the incoming method to an HttpMethod uses
a locale-dependent toUpperCase; update the conversion to be locale-independent
by passing Locale.ROOT (i.e., call toUpperCase(Locale.ROOT) when
building/normalizing the method string before passing it to HttpMethod.from) so
case conversion behaves consistently across locales.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
slice/src/main/java/org/pragmatica/aether/slice/SliceStoreImpl.java (1)

104-118: Warning added per previous review, but fallback could be more robust.

The warning at lines 113-114 addresses the observability concern from the previous review. However, the fallback SliceClassLoader is still created with an empty URL array, which prevents the slice from accessing its own JAR resources. If this case occurs in practice, it could cause ClassNotFoundException or missing resource errors.

Consider extracting the slice JAR URL from slice.getClass().getProtectionDomain().getCodeSource().getLocation() to make the fallback more robust.

🔎 Optional enhancement: extract JAR URL for fallback
                        // Fallback - create a minimal classloader entry
                        log.warn("Slice {} loaded with unexpected classloader type: {}. Resource access may be limited.",
                                 artifact, sliceClassLoader.getClass().getName());
-                       return createEntry(artifact, slice, new SliceClassLoader(new URL[0], shared));
+                       
+                       // Attempt to extract the slice's JAR URL from its ProtectionDomain
+                       var urls = new URL[0];
+                       try {
+                           var codeSource = slice.getClass().getProtectionDomain().getCodeSource();
+                           if (codeSource != null && codeSource.getLocation() != null) {
+                               urls = new URL[]{codeSource.getLocation()};
+                           }
+                       } catch (SecurityException e) {
+                           log.debug("Unable to access ProtectionDomain for slice {}: {}", artifact, e.getMessage());
+                       }
+                       
+                       return createEntry(artifact, slice, new SliceClassLoader(urls, shared));
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f06e4c and 4b30aca.

📒 Files selected for processing (3)
  • node/src/main/java/org/pragmatica/aether/http/SliceDispatcher.java
  • node/src/main/java/org/pragmatica/aether/invoke/SliceInvoker.java
  • slice/src/main/java/org/pragmatica/aether/slice/SliceStoreImpl.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-24T21:31:46.925Z
Learnt from: siy
Repo: siy/aether PR: 17
File: slice/src/main/java/org/pragmatica/aether/slice/dependency/SharedDependencyLoader.java:161-169
Timestamp: 2025-12-24T21:31:46.925Z
Learning: In SharedDependencyLoader.java, the extractVersion method returns an arbitrary version from patterns (e.g., lower bound for Range/Comparison) rather than resolving against available repository versions. This is a known limitation that will be addressed when artifact repository infrastructure is implemented. Current behavior: works correctly for exact versions, range patterns fail gracefully with 'artifact not found'.

Applied to files:

  • slice/src/main/java/org/pragmatica/aether/slice/SliceStoreImpl.java
📚 Learning: 2025-08-24T04:55:51.502Z
Learnt from: siy
Repo: siy/pragmatica-lite PR: 32
File: core/src/main/java/org/pragmatica/lang/Option.java:393-399
Timestamp: 2025-08-24T04:55:51.502Z
Learning: In the pragmatica-lite library, Option.lift() and Result.lift() methods are designed as boundary functions for integrating external APIs into the functional monad ecosystem. They intentionally catch all Throwables (including Errors) to prevent external API failures from breaking into functional code, maintaining a clear separation between external imperative APIs and internal functional code.

Applied to files:

  • slice/src/main/java/org/pragmatica/aether/slice/SliceStoreImpl.java
🔇 Additional comments (12)
node/src/main/java/org/pragmatica/aether/http/SliceDispatcher.java (3)

46-49: LGTM!

The constructor correctly initializes the invoker and artifact resolver fields.


62-71: Fix confirmed: Empty params now use the map instead of null.

The previous concern about NPE from null requests has been properly addressed. The code now assigns resolvedParams (which may be empty) instead of null, preventing downstream null-pointer issues in slice methods.


73-83: LGTM!

The method resolution and invocation logic is correct. Error handling properly wraps invocation failures with HttpRouterError.InvocationFailed.

node/src/main/java/org/pragmatica/aether/invoke/SliceInvoker.java (4)

83-94: LGTM!

The public API addition for local invocation is well-documented and consistent with the existing invoke patterns. The method signature is clear and appropriate for HTTP router integration.


120-142: LGTM!

Factory method signatures have been consistently updated to accept the InvocationHandler parameter, enabling local invocation support. The changes are applied to both overloads.


149-149: LGTM!

The SLICE_NOT_FOUND constant provides clear error semantics for local invocation failures.


154-154: LGTM!

Internal state changes correctly integrate the InvocationHandler into the class.

Also applies to: 166-166, 173-173

slice/src/main/java/org/pragmatica/aether/slice/SliceStoreImpl.java (5)

10-10: LGTM!

Import is necessary for the new Option field.


62-68: LGTM!

Proper deprecation of the legacy factory method while maintaining backward compatibility.


70-72: LGTM!

Record properly updated to include the optional shared library classloader field.


88-93: LGTM!

Clean delegation logic using Option.fold() to select between legacy and shared loader paths.


95-102: LGTM!

Legacy loading path properly extracted with appropriate deprecation warning suppression. Error handling includes proper classloader cleanup.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java (1)

59-78: Consider URL-decoding the path.

The path is not URL-decoded, so encoded characters (e.g., %20 for space) remain encoded. This could cause issues with route matching if the router expects decoded paths.

If this is intentional (to let the handler or router decode as needed), it's fine. Otherwise, consider decoding:

🔎 Suggested enhancement
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+
 private RequestContext createRequestContext(FullHttpRequest request) {
     var uri = request.uri();
     var queryIndex = uri.indexOf('?');

     String path;
     String query;
     if (queryIndex >= 0) {
-        path = uri.substring(0, queryIndex);
+        path = URLDecoder.decode(uri.substring(0, queryIndex), StandardCharsets.UTF_8);
         query = uri.substring(queryIndex + 1);
     } else {
-        path = uri;
+        path = URLDecoder.decode(uri, StandardCharsets.UTF_8);
         query = null;
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b30aca and 8e22671.

📒 Files selected for processing (2)
  • http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java
  • node/src/main/java/org/pragmatica/aether/invoke/SliceInvoker.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • node/src/main/java/org/pragmatica/aether/invoke/SliceInvoker.java
🔇 Additional comments (2)
http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java (2)

53-57: LGTM: Standard Netty error handling.

The exception handler correctly logs the error and closes the channel.


88-91: LGTM: Bad requests close the connection.

Setting keepAlive=false for malformed requests is appropriate, as the connection state may be unpredictable after a decoder failure.

Comment on lines +32 to +51
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
if (!request.decoderResult().isSuccess()) {
sendBadRequest(ctx, "Invalid HTTP request");
return;
}

var keepAlive = HttpUtil.isKeepAlive(request);
var context = createRequestContext(request);
var writer = new NettyResponseWriter(ctx, keepAlive);

try {
handler.accept(context, writer);
} catch (Exception e) {
LOG.error("Handler error", e);
writer.internalError(Causes.fromThrowable(e));
} finally {
context.release();
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Buffer leak edge case: exception before try block.

The finally block at line 49 correctly addresses the buffer leak flagged in the previous review. However, if createRequestContext (line 40) throws an exception after calling retain() at line 75, or if the NettyResponseWriter constructor (line 41) throws, the finally block is never entered and the retained buffer leaks.

Consider wrapping lines 40-41 in the try block or adding a separate try-catch around createRequestContext to ensure cleanup:

🔎 Refactored approach
-        var keepAlive = HttpUtil.isKeepAlive(request);
-        var context = createRequestContext(request);
-        var writer = new NettyResponseWriter(ctx, keepAlive);
-
-        try {
+        var keepAlive = HttpUtil.isKeepAlive(request);
+        RequestContext context = null;
+        try {
+            context = createRequestContext(request);
+            var writer = new NettyResponseWriter(ctx, keepAlive);
             handler.accept(context, writer);
         } catch (Exception e) {
             LOG.error("Handler error", e);
-            writer.internalError(Causes.fromThrowable(e));
+            new NettyResponseWriter(ctx, keepAlive).internalError(Causes.fromThrowable(e));
         } finally {
-            context.release();
+            if (context != null) {
+                context.release();
+            }
         }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +80 to +86
private Map<String, String> extractHeaders(FullHttpRequest request) {
var headers = new HashMap<String, String>();
for (var entry : request.headers()) {
headers.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue());
}
return headers;
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Multiple header values are lost.

HTTP allows multiple headers with the same name (e.g., Set-Cookie, Accept, Accept-Encoding). The current implementation overwrites earlier values when a duplicate name (case-insensitive) is encountered, keeping only the last one. This breaks functionality for multi-valued headers.

Consider using a Map<String, List<String>> or concatenating values with a delimiter (e.g., comma) to preserve all values:

🔎 Proposed fix with List
-    private Map<String, String> extractHeaders(FullHttpRequest request) {
-        var headers = new HashMap<String, String>();
+    private Map<String, java.util.List<String>> extractHeaders(FullHttpRequest request) {
+        var headers = new HashMap<String, java.util.List<String>>();
         for (var entry : request.headers()) {
-            headers.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue());
+            headers.computeIfAbsent(entry.getKey().toLowerCase(Locale.ROOT), k -> new java.util.ArrayList<>())
+                   .add(entry.getValue());
         }
         return headers;
     }

Note: This requires updating RequestContext to accept Map<String, List<String>> for headers.

🤖 Prompt for AI Agents
In
http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java
around lines 80 to 86, the current extractHeaders implementation overwrites
duplicate header names and loses multiple values; change it to collect all
values per header name (case-insensitive) by returning Map<String, List<String>>
(or alternatively a Map<String, String> where values are joined by commas) and
append each header occurrence to the list instead of replacing; update the
RequestContext and any callers to accept Map<String, List<String>> (or the
chosen joined-string format) and adjust usages (e.g., parsing cookies, Accept
headers) to handle multiple values accordingly.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged as a design simplification. For this use case, single-value headers are sufficient. Multi-value header support can be added later if needed for specific scenarios like Set-Cookie handling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@siy, understood. I've noted that single-value headers are the intended design for this HTTP server implementation. The simplified API is appropriate when multi-value header scenarios aren't needed for the current use cases.


✏️ Learnings added
Learnt from: siy
Repo: siy/aether PR: 17
File: http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java:80-86
Timestamp: 2025-12-24T21:48:43.366Z
Learning: In the http-server module (http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java), extractHeaders uses Map<String, String> and keeps only the last value when duplicate header names appear. This is a deliberate design simplification—single-value headers are sufficient for the current use case. Multi-value header support (e.g., for Set-Cookie) can be added later if needed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java (1)

61-80: Consider URL decoding for path and query parameters.

The current URI parsing extracts path and query components but doesn't decode percent-encoded characters (e.g., %20 for space, %2F for /). This may cause issues if handlers expect decoded values.

🔎 Add URL decoding
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;

 private RequestContext createRequestContext(FullHttpRequest request) {
     var uri = request.uri();
     var queryIndex = uri.indexOf('?');
     
     String path;
     String query;
     if (queryIndex >= 0) {
-        path = uri.substring(0, queryIndex);
-        query = uri.substring(queryIndex + 1);
+        path = URLDecoder.decode(uri.substring(0, queryIndex), StandardCharsets.UTF_8);
+        query = uri.substring(queryIndex + 1); // Keep query encoded for later parameter parsing
     } else {
-        path = uri;
+        path = URLDecoder.decode(uri, StandardCharsets.UTF_8);
         query = null;
     }

Note: Query string should typically remain encoded until parameter parsing to preserve = and & delimiters.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e22671 and 9ff0579.

📒 Files selected for processing (1)
  • http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-24T21:48:43.366Z
Learnt from: siy
Repo: siy/aether PR: 17
File: http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java:80-86
Timestamp: 2025-12-24T21:48:43.366Z
Learning: In the http-server module (http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java), extractHeaders uses Map<String, String> and keeps only the last value when duplicate header names appear. This is a deliberate design simplification—single-value headers are sufficient for the current use case. Multi-value header support (e.g., for Set-Cookie) can be added later if needed.

Applied to files:

  • http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java
🔇 Additional comments (5)
http-server/src/main/java/org/pragmatica/http/server/impl/HttpRequestHandler.java (5)

20-30: LGTM!

Clean handler setup following standard Netty patterns. The BiConsumer delegation provides good flexibility for routing.


55-59: LGTM!

Appropriate channel-level exception handling with logging and cleanup.


82-88: LGTM!

Locale-independent header name conversion is correctly implemented with Locale.ROOT. Based on learnings, the single-value header design is an acceptable simplification for the current use case.


90-93: LGTM!

Appropriate error response handling with connection closure for malformed requests.


32-53: The code is safe as structured. The edge case buffer leak concern is based on an incorrect premise.

HttpMethod.from() is the only operation in createRequestContext that can throw an exception, and it occurs before request.content().retain() is called. If it throws, no buffer is retained. The RequestContext record has no constructor validation logic—it's auto-generated field assignment—so it won't throw. Therefore, the scenario of "throws AFTER retain() but BEFORE constructor completes" cannot occur.

The try-finally pattern in channelRead0 correctly handles the actual risk: if handler processing throws, the context is properly released.

@siy siy merged commit 7868905 into main Dec 24, 2025
1 of 2 checks passed
@siy siy deleted the release/0.4.0 branch December 24, 2025 23:10
This was referenced Dec 25, 2025
This was referenced Jan 1, 2026
This was referenced Jan 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant