Skip to content

Support ${headers.NAME} syntax to forward upstream API headers to toolsets#1725

Merged
dgageot merged 1 commit intodocker:mainfrom
dgageot:headers
Feb 14, 2026
Merged

Support ${headers.NAME} syntax to forward upstream API headers to toolsets#1725
dgageot merged 1 commit intodocker:mainfrom
dgageot:headers

Conversation

@dgageot
Copy link
Member

@dgageot dgageot commented Feb 13, 2026

Allow toolset header values to reference incoming API request headers using ${headers.NAME} placeholders.

For example, a toolset config like:

headers:
  Authorization: ${headers.Authorization}

will resolve the Authorization value at request time from the upstream HTTP request that triggered the agent.

@dgageot dgageot requested a review from a team as a code owner February 13, 2026 18:24
Copy link

@docker-agent docker-agent bot left a comment

Choose a reason for hiding this comment

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

Review Summary

I've reviewed the changes for header placeholder support (${headers.NAME} syntax). The implementation looks solid overall, but I found a few issues worth noting around edge case handling and context propagation.

Found 3 issues:

  • 1 edge case in placeholder parsing that could mask configuration errors
  • 2 potential context propagation issues where placeholders may not resolve correctly

All issues are marked as MEDIUM severity - they represent potential problems in specific scenarios rather than critical bugs.

end := strings.Index(v, "}")
if end == -1 {
b.WriteString(v)
break
Copy link

Choose a reason for hiding this comment

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

Missing validation for empty header names in placeholders

The code calls upstream.Get(strings.TrimSpace(name)) after extracting the placeholder name. If the extracted name is empty or whitespace-only (e.g., ${headers.} or ${headers. }), TrimSpace returns "", and upstream.Get("") returns an empty string.

While this gracefully degrades by resolving to empty string (matching HTTP convention for missing headers), it could mask configuration errors. For example, a misconfigured placeholder like ${headers.} silently becomes an empty string instead of being flagged as malformed.

Suggestion: Consider adding validation that the header name is non-empty after trimming, or document this as intentional behavior:

name := strings.TrimSpace(v[len(prefix):end])
if name == "" {
    // Either: return error, or document this returns empty string
}
b.WriteString(upstream.Get(name))

Note: The test suite includes a test for "trimmed spaces in name" but doesn't cover the edge case of empty names after trimming.

}

req.Header.Set("User-Agent", useragent.Header)
setHeaders(req, t.config.Headers)
Copy link

Choose a reason for hiding this comment

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

Context may not contain upstream headers for placeholder resolution

The setHeaders function is called to resolve ${headers.NAME} placeholders from req.Context(). However, the context comes from the ctx parameter passed to callTool. If this tool is invoked outside the normal HTTP handler middleware chain (or if the upstream.Handler middleware wasn't applied), the context won't contain upstream headers and placeholders won't resolve.

Current flow:

  1. HTTP request → upstream.Handler middleware → stores headers in context
  2. Handler invokes tool via some chain → callTool(ctx, ...)
  3. setHeaders tries to resolve placeholders from ctx

The implementation assumes the ctx parameter contains upstream headers, but this isn't validated or guaranteed.

Evidence: The test cases in api_test.go all use t.Context() (a plain testing context with no upstream headers) and don't test placeholder resolution.

Suggestion: Either:

  • Add documentation that this tool requires contexts derived from HTTP requests processed by upstream.Handler
  • Add integration tests showing placeholder resolution works through the full handler chain
  • Add runtime validation that logs a warning when placeholders are used but upstream headers aren't available in context

func setHeaders(req *http.Request, headers map[string]string) {
req.Header.Set("User-Agent", useragent.Header)
for k, v := range headers {
for k, v := range upstream.ResolveHeaders(req.Context(), headers) {
Copy link

Choose a reason for hiding this comment

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

Context may not contain upstream headers for placeholder resolution

Similar to api.go, this code calls setHeaders(req, h.headers) which attempts to resolve ${headers.NAME} placeholders from req.Context(). The context is derived from the ctx parameter passed to callTool.

If the caller didn't propagate the context from an HTTP request that went through the upstream.Handler middleware, upstream headers won't be available in the context, and placeholders won't resolve.

Evidence: The test openapi_test.go:TestOpenAPITool_CustomHeaders shows static headers work, but there's no test validating ${headers.X} placeholder resolution works when invoked through an HTTP handler with the middleware chain.

Suggestion:

  • Add integration tests that verify placeholder resolution through the full HTTP handler → tool invocation chain
  • Document the requirement that tools using header placeholders must be invoked with contexts from HTTP requests
  • Consider adding a helper that validates upstream headers are present when placeholders are detected in config

jonathandieu

This comment was marked as outdated.

}

// resolveValue replaces ${headers.NAME} in a single string value.
func resolveValue(v string, upstream http.Header) string {
Copy link
Member

Choose a reason for hiding this comment

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

Let's use goja for thids no?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll take a look. the agent didn't want to do it :-)

…lsets

Add a new pkg/upstream package that allows toolset header values to
reference incoming API request headers using ${headers.NAME} placeholders.

For example, a toolset config like:

    headers:
      Authorization: ${headers.Authorization}

will resolve the Authorization value at request time from the upstream
HTTP request that triggered the agent.

The middleware (Echo and ConnectRPC) stores the incoming request headers
in the context. At tool-call time, header values containing ${headers.X}
are resolved from that context. Static header values without placeholders
are unaffected.

Assisted-By: cagent
@dgageot dgageot merged commit 8df5bc5 into docker:main Feb 14, 2026
5 checks passed
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.

3 participants