Skip to content

cmd/launcher/web: expose an HTTP middleware seam on launcher.Config for embedders #965

@Nithish555

Description

@Nithish555

** Please make sure you read the contribution guide and file the issues in the right place. **
Contribution guide.

🔴 Required Information

Please ensure all items in this section are completed to allow for efficient
triaging. If an item is not applicable to you - please mark it as N/A

Is your feature request related to a specific problem?

Embedders that wrap ADK Go inside a larger application can't insert HTTP middleware into the launcher's request pipeline. webLauncher.Run in cmd/launcher/web/web.go constructs:

srv := http.Server{
    Addr:    fmt.Sprintf(":%v", w.config.port),
    Handler: router,
    // ... timeouts ...
}

launcher.Config has no Handler, Middleware, or WrapHandler field, so there's no public way to wrap the inbound handler before it reaches ADK's routes.

The concrete case where this hurts is distributed-tracing context extraction. For cross-process tracing, an embedder needs to wrap the handler with otelhttp.NewHandler so the W3C traceparent header gets extracted into req.Context() before any handler runs. Without that wrap:

Inbound HTTP requests carry a traceparent populated by an upstream caller.
ADK handlers see req.Context() without an extracted span context.
Every span ADK creates (invoke_agent, generate_content, execute_tool) becomes a fresh trace root.
Cross-service trace stitching breaks at the ADK boundary.
The TracerProvider half of the OTel embedding story is already publicly handleable via launcher.Config.TelemetryOptions with telemetry.WithTracerProvider(...) — that prevents SetGlobalOtelProviders from clobbering the embedder's TP. But the HTTP pipeline has no symmetric escape hatch.

Proposed Solution

Add a field to launcher.Config:

type Config struct {
    // ... existing fields ...

    // HTTPMiddleware is applied in order to the HTTP handler before
    // serving. Each middleware wraps the handler returned by the previous
    // one. The first middleware in the slice is the outermost (runs first
    // on inbound requests).
    HTTPMiddleware []func(http.Handler) http.Handler
}

And apply it in webLauncher.Run:

var handler http.Handler = router
for i := len(config.HTTPMiddleware) - 1; i >= 0; i-- {
    handler = config.HTTPMiddleware[i](handler)
}
srv := http.Server{
    Addr:    fmt.Sprintf(":%v", w.config.port),
    Handler: handler,                 // was: router
    // ... timeouts unchanged ...
}

The change is purely additive — existing callers that don't set HTTPMiddleware see no behavior difference.

Impact on your work

How does this feature impact your work and what are you trying to achieve?

We embed ADK Go inside an internal agent platform that already has its own OpenTelemetry pipeline (TracerProvider, propagators, span processors, OTLP exporter, etc.). Today we bypass webLauncher.Run entirely and re-implement its setup (BuildBaseRouter, apiLauncher.SetupSubrouters, session.InMemoryService default) so we can insert otelhttp.NewHandler around the router before constructing the http.Server. The bypass is ~50 lines of code in our repo and pins us against ADK internals — every ADK upgrade requires re-checking whether our replay still mirrors webLauncher.Run correctly.

With this feature, the bypass goes away. We'd call full.NewLauncher().Execute(ctx, config, args) with both TelemetryOptions and HTTPMiddleware set, and stop tracking ADK launcher internals.


🟡 Recommended Information

Alternatives Considered

  1. Bypass webLauncher.Run (current workaround). Re-implement its setup, wrap with otelhttp.NewHandler, serve. Works, but fragile against ADK upgrades.
  2. Sidecar reverse proxy on a different port. Stand up a separate http.Server with otelhttp.NewHandler that reverse-proxies to ADK's port. This does NOT solve the problem — httputil.NewSingleHostReverseProxy forwards a new HTTP request whose req.Context() is freshly populated by ADK's http.Server. ADK's handlers still don't extract the inbound traceparent. Verified empirically: the trace tree remains broken at the ADK boundary.
  3. Pre-bind the listener and hand it to ADK. launcher.Config has no Listener field; srv.ListenAndServe() binds internally. Not viable through the public API go:linkname or reflection to grab ADK's router after construction. Possible but unmaintainable; breaks on every ADK patch release.
  4. The proposed HTTPMiddleware field is the smallest public-API change that genuinely solves the problem.

Willingness to contribute

Are you interested in implementing this feature yourself or submitting a PR?

Yes — happy to submit a PR if the design is acceptable. Would prefer to align on the API shape first (single func(http.Handler) http.Handler vs. slice; field name; doc-comment phrasing; whether the same hook should also apply to a2a and webui sublaunchers or only api) before writing the change.

Proposed API / Implementation

If you have ideas on how this should look in code, please share a
pseudo-code example.

See Proposed Solution for the field and the webLauncher.Run loop. Embedder usage after the change would be:

config := &launcher.Config{
    AgentLoader: agent.NewSingleLoader(myAgent),
    TelemetryOptions: []telemetry.Option{
        telemetry.WithTracerProvider(myTP),
    },
    HTTPMiddleware: []func(http.Handler) http.Handler{
        func(h http.Handler) http.Handler {
            return otelhttp.NewHandler(h, "agent-name",
                otelhttp.WithPropagators(otel.GetTextMapPropagator()))
        },
    },
}
return full.NewLauncher().Execute(ctx, config, args)

The middleware chain order is outer-first: [A, B] means A wraps B wraps router, so on inbound requests A runs first. This matches the common chi.Use(...) / mux.Use(...) convention in Go HTTP routers.

Additional Context

  • launcher.Config.TelemetryOptions already provides the analogous public seam for the TracerProvider clobber problem — telemetry.WithTracerProvider(tp) makes SetGlobalOtelProviders re-install the same TP (no-op clobber). HTTPMiddleware would close the symmetric gap on the HTTP pipeline.
  • Per CONTRIBUTING.md, alignment with adk-python matters. I haven't yet checked whether Python ADK has an analogous embedding seam (e.g. a FastAPI middleware list) — if it does, naming and semantics should follow it. Happy to research that before the design is finalized.
  • Verified the proposal against ADK v0.6.0 source: cmd/launcher/launcher.go: Config, cmd/launcher/web/web.go: webLauncher.Run, cmd/launcher/internal/telemetry/telemetry.go: InitAndSetGlobalOtelProviders, telemetry/config.go: WithTracerProvider, telemetry/telemetry.go: SetGlobalOtelProviders.

Metadata

Metadata

Labels

enhancementNew feature or request
No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions