Skip to content

Commit 1194b3a

Browse files
committed
fix: send HeadersResponse with first chunk, not last
In FULL_DUPLEX_STREAMED mode, envoy needs the deferred HeadersResponse before it forwards response body chunks to the client. Previously, buildStreamedChunkResponse only sent HeadersResponse on endOfStream, which caused empty response bodies when EoS was delayed or missing. Track ResponseHeadersSent on RequestContext and send HeadersResponse with the first chunk instead. Signed-off-by: Noy Itzikowitz <nitzikow@redhat.com>
1 parent c5b83af commit 1194b3a

2 files changed

Lines changed: 8 additions & 4 deletions

File tree

pkg/handlers/response.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func (s *Server) generateEmptyResponseBodyResponse(responseBodyBytes []byte) []*
136136
func (s *Server) HandleResponseChunk(ctx context.Context, reqCtx *RequestContext, chunkBytes []byte, endOfStream bool) ([]*eppb.ProcessingResponse, error) {
137137
// Bodiless requests (e.g., GET /v1/models) may not have a profile set.
138138
if reqCtx.Profile == nil || len(reqCtx.Profile.ResponseChunkProcessors) == 0 {
139-
return s.buildStreamedChunkResponse(chunkBytes, endOfStream), nil
139+
return s.buildStreamedChunkResponse(reqCtx, chunkBytes, endOfStream), nil
140140
}
141141

142142
logger := log.FromContext(ctx).V(logutil.DEFAULT)
@@ -148,7 +148,7 @@ func (s *Server) HandleResponseChunk(ctx context.Context, reqCtx *RequestContext
148148
return nil, err
149149
}
150150

151-
return s.buildStreamedChunkResponse(chunkBytes, endOfStream), nil
151+
return s.buildStreamedChunkResponse(reqCtx, chunkBytes, endOfStream), nil
152152
}
153153

154154
// runResponseChunkProcessors executes chunk processors in the order they were registered.
@@ -171,7 +171,9 @@ func (s *Server) runResponseChunkProcessors(ctx context.Context, cycleState *plu
171171
}
172172

173173
// buildStreamedChunkResponse wraps a chunk in the ext_proc streaming response format.
174-
func (s *Server) buildStreamedChunkResponse(chunk []byte, endOfStream bool) []*eppb.ProcessingResponse {
174+
// On the first call (responseHeadersSent=false), it prepends a HeadersResponse to answer
175+
// the deferred response headers — envoy requires this before it accepts body responses.
176+
func (s *Server) buildStreamedChunkResponse(reqCtx *RequestContext, chunk []byte, endOfStream bool) []*eppb.ProcessingResponse {
175177
responses := []*eppb.ProcessingResponse{
176178
{
177179
Response: &eppb.ProcessingResponse_ResponseBody{
@@ -191,13 +193,14 @@ func (s *Server) buildStreamedChunkResponse(chunk []byte, endOfStream bool) []*e
191193
},
192194
}
193195

194-
if endOfStream {
196+
if !reqCtx.ResponseHeadersSent {
195197
headerResp := &eppb.ProcessingResponse{
196198
Response: &eppb.ProcessingResponse_ResponseHeaders{
197199
ResponseHeaders: &eppb.HeadersResponse{},
198200
},
199201
}
200202
responses = append([]*eppb.ProcessingResponse{headerResp}, responses...)
203+
reqCtx.ResponseHeadersSent = true
201204
}
202205

203206
return responses

pkg/handlers/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type RequestContext struct {
7676
RequestSentTimestamp time.Time
7777
ResponseFirstChunkTimestamp time.Time
7878
ResponseCompleteTimestamp time.Time
79+
ResponseHeadersSent bool
7980
Profile *requesthandling.Profile
8081
CycleState *plugin.CycleState
8182
Request *requesthandling.InferenceRequest

0 commit comments

Comments
 (0)