diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go index 082020f1e32..0ce052005ca 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go @@ -378,9 +378,8 @@ func (a *InvokeAction) responsesLocal(ctx context.Context) error { } if resp.StatusCode >= 400 { - requestID := resp.Header.Get("apim-request-id") - if requestID != "" { - fmt.Printf("Trace ID: %s\n", requestID) + if traceID := responseTraceID(resp); traceID != "" { + fmt.Printf("Trace ID: %s\n", traceID) } return fmt.Errorf( "POST %s failed with HTTP %d: %s\n%s", @@ -614,9 +613,8 @@ func (a *InvokeAction) responsesRemote(ctx context.Context) error { } defer resp.Body.Close() - requestID := resp.Header.Get("apim-request-id") - if requestID != "" { - fmt.Printf("Trace ID: %s\n", requestID) + if traceID := responseTraceID(resp); traceID != "" { + fmt.Printf("Trace ID: %s\n", traceID) } captureResponseSession(ctx, rc.azdClient, agentKey, sid, resp, "Session: ") @@ -816,9 +814,8 @@ func handleInvocationResponse( agentName string, timeout time.Duration, ) error { - requestID := resp.Header.Get("apim-request-id") - if requestID != "" { - fmt.Printf("Trace ID: %s\n", requestID) + if traceID := responseTraceID(resp); traceID != "" { + fmt.Printf("Trace ID: %s\n", traceID) } if resp.StatusCode >= 400 { @@ -1125,6 +1122,23 @@ func createConversation(ctx context.Context, projectEndpoint, agentName, bearerT return "", fmt.Errorf("conversation response missing 'id' field") } +// responseTraceID returns the trace ID from the response, preferring x-request-id +// and falling back to apim-request-id. If a header value is comma-folded (which +// can happen when an intermediary like APIM combines duplicate headers per +// RFC 7230 ยง3.2.2), the first non-empty token is returned. +func responseTraceID(resp *http.Response) string { + raw := resp.Header.Get("x-request-id") + if raw == "" { + raw = resp.Header.Get("apim-request-id") + } + for part := range strings.SplitSeq(raw, ",") { + if id := strings.TrimSpace(part); id != "" { + return id + } + } + return "" +} + // readSSEStream reads a Server-Sent Events stream from the Foundry Responses API, // printing text deltas in real-time and returning the final response or any error. func readSSEStream(body io.Reader, agentName string) error { diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go index 27aa72d4ab7..c7f16326ccc 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go @@ -1025,3 +1025,69 @@ func TestCreateConversation(t *testing.T) { }) } } + +func TestResponseTraceID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + headers map[string]string + want string + }{ + { + name: "prefers x-request-id when both present", + headers: map[string]string{"X-Request-ID": "trace-abc", "apim-request-id": "apim-xyz"}, + want: "trace-abc", + }, + { + name: "falls back to apim-request-id", + headers: map[string]string{"apim-request-id": "apim-xyz"}, + want: "apim-xyz", + }, + { + name: "returns empty when neither present", + headers: map[string]string{}, + want: "", + }, + { + name: "returns x-request-id when only it is present", + headers: map[string]string{"X-Request-ID": "trace-only"}, + want: "trace-only", + }, + { + name: "deduplicates comma-folded x-request-id", + headers: map[string]string{"X-Request-ID": "trace-abc,trace-abc"}, + want: "trace-abc", + }, + { + name: "returns first token when x-request-id is comma-list", + headers: map[string]string{"X-Request-ID": "trace-first, trace-second"}, + want: "trace-first", + }, + { + name: "skips leading empty token in comma-folded x-request-id", + headers: map[string]string{"X-Request-ID": ", trace-second"}, + want: "trace-second", + }, + { + name: "deduplicates comma-folded apim-request-id fallback", + headers: map[string]string{"apim-request-id": "apim-xyz, apim-xyz"}, + want: "apim-xyz", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + resp := &http.Response{Header: http.Header{}} + for k, v := range tt.headers { + resp.Header.Set(k, v) + } + + if got := responseTraceID(resp); got != tt.want { + t.Errorf("responseTraceID() = %q, want %q", got, tt.want) + } + }) + } +}