Files
Jordan Ritter 514f630904 fix(showcase/ms-agent-harness-dotnet): unify aimock header casing and dedupe-safety
The header propagation chain (middleware -> context -> policy) had two bugs
that combined to threaten D5/D6 header-forwarding:

1. AimockHeaderMiddleware.ToDictionary used the default ORDINAL case-sensitive
   comparer. ASP.NET's IHeaderDictionary is case-insensitive, but iterating
   the underlying store can yield case-variant duplicates (e.g., a misbehaving
   proxy injecting both `X-Foo` and `x-foo`). Default-comparer ToDictionary
   throws ArgumentException on duplicates and fails the request.

2. AimockHeaderContext.Set lowercased all keys via ToLowerInvariant.
   AimockHeaderMiddleware captured original case; the context then mutated
   the casing; AimockHeaderPolicy.TryGetValue then matched against whatever
   case the OpenAI SDK happened to use. This is inconsistent and aimock
   fixture matching can be case-sensitive depending on configuration.

Canonical strategy: preserve original header casing as captured by the
middleware, but compare case-insensitively throughout via
StringComparer.OrdinalIgnoreCase. The middleware now also groups variants
defensively so duplicate keys cannot blow up the dictionary build.
AimockHeaderPolicy's add-if-absent TryGetValue then works correctly under
case-insensitive comparison without any further changes.
2026-05-26 13:36:38 -07:00

24 lines
1.2 KiB
C#

// STOPGAP: This integration-level header propagation replaces once copilotkit-sdk-dotnet
// ships (Microsoft contribution, ETA mid-2026). When that SDK lands, delete this code
// and use the SDK's built-in header propagation.
// See: https://www.notion.so/copilotkit/3543aa3818528150b6acc5b872ad7fe5
// TODO(copilotkit-sdk-dotnet): migrate to SDK-level header propagation
public static class AimockHeaderContext
{
private static readonly AsyncLocal<Dictionary<string, string>> _headers = new();
public static void Set(Dictionary<string, string> headers)
{
// Canonical strategy: preserve the original header casing captured upstream by
// AimockHeaderMiddleware, but compare keys case-insensitively throughout.
// AimockHeaderPolicy relies on case-insensitive TryGetValue to add-if-absent.
var filtered = headers
.Where(h => h.Key.StartsWith("x-", StringComparison.OrdinalIgnoreCase))
.ToDictionary(h => h.Key, h => h.Value, StringComparer.OrdinalIgnoreCase);
_headers.Value = filtered;
}
public static Dictionary<string, string> Get() => _headers.Value ?? new(StringComparer.OrdinalIgnoreCase);
}