@@ -76,9 +76,6 @@ public async ValueTask<bool> HandlePostAsync(JsonRpcMessage message, Cancellatio
7676 return false ;
7777 }
7878
79- CancellationTokenSource ? deferredFlushCts = null ;
80- Task ? deferredFlushTask = null ;
81- bool deferHeaderFlush = false ;
8279 using ( await _messageLock . LockAsync ( cancellationToken ) . ConfigureAwait ( false ) )
8380 {
8481 var primingItem = await TryStartSseEventStreamAsync ( _pendingRequest ) . ConfigureAwait ( false ) ;
@@ -87,88 +84,33 @@ public async ValueTask<bool> HandlePostAsync(JsonRpcMessage message, Cancellatio
8784 await NotifyResponseStartingAsync ( firstMessage : null ) . ConfigureAwait ( false ) ;
8885 await _httpSseWriter . WriteAsync ( primingItem . Value , cancellationToken ) . ConfigureAwait ( false ) ;
8986 }
90- else if ( onResponseStarting is null )
87+ else if ( onResponseStarting is not null &&
88+ McpProtocolVersions . RequiresPerRequestMetadata ( message . Context . ProtocolVersion ) )
9189 {
92- // If there's no priming write, flush the stream to ensure HTTP response headers are
93- // sent to the client now that the server is ready to process the request.
94- // This prevents HttpClient timeout for long-running requests.
95- await responseStream . FlushAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
90+ // Per-request-metadata protocol revisions map the first JSON-RPC error onto the HTTP
91+ // status line. Keep the headers uncommitted until that message arrives so the mapping
92+ // cannot depend on how long dispatch takes.
9693 }
9794 else
9895 {
99- deferHeaderFlush = true ;
96+ // Earlier protocol revisions keep the eager header flush for long-running handlers.
97+ // Mark the response as started before flushing because any later JSON-RPC error must
98+ // not attempt to change an already committed status line.
99+ await NotifyResponseStartingAsync ( firstMessage : null ) . ConfigureAwait ( false ) ;
100+ await responseStream . FlushAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
100101 }
101102
102103 // Ensure that we've sent the priming event before processing the incoming request.
103104 await parentTransport . MessageWriter . WriteAsync ( message , cancellationToken ) . ConfigureAwait ( false ) ;
104105 }
105106
106- if ( deferHeaderFlush )
107- {
108- // Defer the flush (and the header commit it implies) so the callback can still choose
109- // the HTTP status line for an immediate JSON-RPC error. Start the bounded grace period
110- // only after the request has been queued for dispatch.
111- deferredFlushCts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
112- deferredFlushTask = DeferredHeaderFlushAsync ( deferredFlushCts . Token ) ;
113- }
114-
115- try
116- {
117- // Wait for the response to be written before returning from the handler.
118- // This keeps the HTTP response open until the final response message is sent.
119- await _httpResponseTcs . Task . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
120- }
121- finally
122- {
123- if ( deferredFlushCts is not null )
124- {
125- deferredFlushCts . Cancel ( ) ;
126- await deferredFlushTask ! . ConfigureAwait ( false ) ;
127- deferredFlushCts . Dispose ( ) ;
128- }
129- }
107+ // Wait for the response to be written before returning from the handler.
108+ // This keeps the HTTP response open until the final response message is sent.
109+ await _httpResponseTcs . Task . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
130110
131111 return true ;
132112 }
133113
134- /// <summary>
135- /// Bounds the deferred header flush: after a short grace window, flushes the response headers
136- /// if no response message has been written yet. Immediate rejections land well inside the
137- /// window, so the response-starting callback can still map their JSON-RPC error codes onto the
138- /// HTTP status line; a handler that runs longer commits the headers here so clients see them
139- /// promptly (long-running tool calls must not trip HttpClient's response timeout).
140- /// </summary>
141- private async Task DeferredHeaderFlushAsync ( CancellationToken cancellationToken )
142- {
143- try
144- {
145- await Task . Delay ( DeferredHeaderFlushGrace , cancellationToken ) . ConfigureAwait ( false ) ;
146- using var _ = await _messageLock . LockAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
147- if ( ! _httpResponseStarted && ! _httpResponseCompleted )
148- {
149- await NotifyResponseStartingAsync ( firstMessage : null ) . ConfigureAwait ( false ) ;
150- await responseStream . FlushAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
151- }
152- }
153- catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested )
154- {
155- // The response was written or the request ended before the grace window elapsed.
156- }
157- catch ( Exception ex )
158- {
159- // Surface the failure to the awaiting HandlePostAsync when possible. If the response
160- // future has already been resolved (the response started or completed on another path),
161- // TrySetException is a no-op, so log here to keep the deferred-flush failure diagnosable.
162- if ( ! _httpResponseTcs . TrySetException ( ex ) )
163- {
164- LogDeferredHeaderFlushFailed ( ex ) ;
165- }
166- }
167- }
168-
169- /// <summary>How long the response-header flush may be deferred waiting for the first response message.</summary>
170- internal static readonly TimeSpan DeferredHeaderFlushGrace = TimeSpan . FromMilliseconds ( 250 ) ;
171-
172114 /// <summary>
173115 /// Invokes the response-starting callback exactly once, immediately before the first write to
174116 /// the HTTP response stream, so the HTTP application can still set the response status line.
@@ -343,6 +285,4 @@ public async ValueTask DisposeAsync()
343285 [ LoggerMessage ( Level = LogLevel . Warning , Message = "Failed to dispose SSE event stream writer." ) ]
344286 private partial void LogStoreStreamDisposalFailed ( Exception exception ) ;
345287
346- [ LoggerMessage ( Level = LogLevel . Warning , Message = "Failed to flush deferred Streamable HTTP response headers." ) ]
347- private partial void LogDeferredHeaderFlushFailed ( Exception exception ) ;
348288}
0 commit comments