Skip to content

feat: add middelwares to the UI#202

Open
xavidop wants to merge 1 commit into
mainfrom
xavier/middleware-v2-ui
Open

feat: add middelwares to the UI#202
xavidop wants to merge 1 commit into
mainfrom
xavier/middleware-v2-ui

Conversation

@xavidop
Copy link
Copy Markdown
Collaborator

@xavidop xavidop commented May 25, 2026

Description

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test improvements
  • CI/CD improvements

Changes Made

Add middleware support to the Dev UI

Checklist

  • My code follows the project's code style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Screenshots (if applicable)

Screenshot 2026-05-25 at 16 41 33

Documentation

  • README updated
  • Plugin documentation updated
  • API documentation updated
  • Sample code updated

Reviewer Notes

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for registering and resolving generation middleware from the Genkit Dev UI. It adds a new use field to GenerateActionOptions to receive selected middleware names, resolves these names to fresh middleware instances in GenerateAction, and executes them through the wrapGenerate, wrapModel, and wrapTool chains. It also updates the registry to support typed value buckets (e.g., "middleware"), exposes these values via the reflection API endpoints (/api/values), and provides builder methods to pre-register middleware. The reviewer feedback highlights three key issues: a concurrency bug in Genkit.registerMiddlewareForDevUi due to a non-atomic check-then-act pattern, a thread-safety issue in DefaultRegistry.registerValue that can be resolved using putIfAbsent, and a potential deserialization issue in GenerateAction when converting tool inputs, suggesting the use of JsonUtils.convert(...) instead of the local objectMapper.

Comment on lines +872 to +884
private void registerMiddlewareForDevUi(List<GenerationMiddleware> use) {
if (use == null || use.isEmpty()) {
return;
}
for (GenerationMiddleware mw : use) {
if (mw == null) continue;
String name = mw.name();
if (name == null || name.isEmpty()) continue;
if (registry.lookupValue("middleware", name) == null) {
registry.registerValue("middleware", name, mw);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

There is a critical concurrency bug here. The check-then-act pattern (lookupValue(...) == null followed by registerValue(...)) is not atomic. If multiple concurrent generate() calls are made using the same unregistered middleware, both threads can simultaneously pass the null check and attempt to register the middleware. This will cause one of the threads to throw an IllegalStateException from the registry, crashing that entire generation request.

To prevent this, we should wrap the registration in a try-catch block to safely ignore IllegalStateException if the middleware was already registered concurrently.

  private void registerMiddlewareForDevUi(List<GenerationMiddleware> use) {
    if (use == null || use.isEmpty()) {
      return;
    }
    for (GenerationMiddleware mw : use) {
      if (mw == null) continue;
      String name = mw.name();
      if (name == null || name.isEmpty()) continue;
      if (registry.lookupValue("middleware", name) == null) {
        try {
          registry.registerValue("middleware", name, mw);
        } catch (IllegalStateException e) {
          // Safely ignore concurrent registration
        }
      }
    }
  }

Comment on lines +95 to +103
public void registerValue(String type, String name, Object value) {
Map<String, Object> bucket = valuesByType.computeIfAbsent(type, t -> new ConcurrentHashMap<>());
if (bucket.containsKey(name)) {
throw new IllegalStateException("Value already registered: " + type + "/" + name);
}
bucket.put(name, value);
logger.debug("Registered value: {}/{}", type, 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.

high

The check-then-act pattern (containsKey followed by put) on the ConcurrentHashMap bucket is not atomic. In a highly concurrent environment, multiple threads registering the same value simultaneously could bypass the containsKey check and overwrite each other, or cause unexpected race conditions. Using putIfAbsent ensures atomicity and thread safety.

Suggested change
public void registerValue(String type, String name, Object value) {
Map<String, Object> bucket = valuesByType.computeIfAbsent(type, t -> new ConcurrentHashMap<>());
if (bucket.containsKey(name)) {
throw new IllegalStateException("Value already registered: " + type + "/" + name);
}
bucket.put(name, value);
logger.debug("Registered value: {}/{}", type, name);
}
@Override
public void registerValue(String type, String name, Object value) {
Map<String, Object> bucket = valuesByType.computeIfAbsent(type, t -> new ConcurrentHashMap<>());
if (bucket.putIfAbsent(name, value) != null) {
throw new IllegalStateException("Value already registered: " + type + "/" + name);
}
logger.debug("Registered value: {}/{}", type, name);
}

Comment on lines +340 to +345
// Convert input if necessary
if (toolInput instanceof Map
&& tool.getInputClass() != null
&& !Map.class.isAssignableFrom(tool.getInputClass())) {
toolInput = objectMapper.convertValue(toolInput, tool.getInputClass());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using a local objectMapper directly to convert the tool input can lead to deserialization failures if the input contains custom types, custom date formats, or custom naming strategies registered globally. It is highly recommended to use JsonUtils.convert(...) instead, which is consistent with Genkit.java and uses the centrally configured object mapper.

          // Convert input if necessary
          Class<?> inputClass = tool.getInputClass();
          if (inputClass != null && toolInput != null && !inputClass.isInstance(toolInput)) {
            toolInput = JsonUtils.convert(toolInput, inputClass);
          }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant