Fixes #12249 - HTTP/2 responses with Content-Length may have no content. (#12250)

Fixed HttpReceiverOverHTTP2.read() to return a failed chunk if the stream has been reset.
This closes the window where a RST_STREAM frame may be received, drain the DATA frames in HTTP2Stream, and the application reading exactly at that point.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2024-09-09 18:20:17 +03:00 committed by GitHub
parent 5bff971fb0
commit fa0502db54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 12 deletions

View File

@ -68,6 +68,7 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
Stream stream = getHttpChannel().getStream(); Stream stream = getHttpChannel().getStream();
if (stream == null) if (stream == null)
return Content.Chunk.from(new EOFException("Channel has been released")); return Content.Chunk.from(new EOFException("Channel has been released"));
Stream.Data data = stream.readData(); Stream.Data data = stream.readData();
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Read stream data {} in {}", data, this); LOG.debug("Read stream data {} in {}", data, this);
@ -77,14 +78,26 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
stream.demand(); stream.demand();
return null; return null;
} }
DataFrame frame = data.frame(); DataFrame frame = data.frame();
boolean last = frame.remaining() == 0 && frame.isEndStream(); boolean last = frame.remaining() == 0 && frame.isEndStream();
if (!last) if (!last)
return Content.Chunk.asChunk(frame.getByteBuffer(), last, data); return Content.Chunk.asChunk(frame.getByteBuffer(), false, data);
data.release(); data.release();
if (stream.isReset())
{
Throwable failure = new EOFException("Stream has been reset");
responseFailure(failure, Promise.noop());
return Content.Chunk.from(failure);
}
else
{
responseSuccess(getHttpExchange(), null); responseSuccess(getHttpExchange(), null);
return Content.Chunk.EOF; return Content.Chunk.EOF;
} }
}
@Override @Override
public void failAndClose(Throwable failure) public void failAndClose(Throwable failure)

View File

@ -46,6 +46,7 @@ import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.Response; import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.client.Result; import org.eclipse.jetty.client.Result;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpURI;
@ -91,6 +92,7 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -826,6 +828,54 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
assertEquals(HttpStatus.OK_200, response.getStatus()); assertEquals(HttpStatus.OK_200, response.getStatus());
} }
@Test
public void testUnreadRequestContentDrainsResponseContent() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, org.eclipse.jetty.server.Response response, Callback callback)
{
// Do not read the request content,
// the server will reset the stream,
// then send a response with content.
ByteBuffer content = ByteBuffer.allocate(1024);
response.getHeaders().put(HttpHeader.CONTENT_LENGTH, content.remaining());
response.write(true, content, callback);
return true;
}
});
AtomicReference<Content.Source> contentSourceRef = new AtomicReference<>();
AtomicReference<Content.Chunk> chunkRef = new AtomicReference<>();
CountDownLatch responseFailureLatch = new CountDownLatch(1);
AtomicReference<Result> resultRef = new AtomicReference<>();
httpClient.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.body(new AsyncRequestContent(ByteBuffer.allocate(1024)))
.onResponseContentSource((response, contentSource) -> contentSourceRef.set(contentSource))
// The request is failed before the response, verify that
// reading at the request failure event yields a failure chunk.
.onRequestFailure((request, failure) -> chunkRef.set(contentSourceRef.get().read()))
.onResponseFailure((response, failure) -> responseFailureLatch.countDown())
.send(resultRef::set);
// Wait for the RST_STREAM to arrive and drain the response content.
assertTrue(responseFailureLatch.await(5, TimeUnit.SECONDS));
// Verify that the chunk read at the request failure event is a failure chunk.
Content.Chunk chunk = chunkRef.get();
assertTrue(Content.Chunk.isFailure(chunk, true));
// Reading more also yields a failure chunk.
chunk = contentSourceRef.get().read();
assertTrue(Content.Chunk.isFailure(chunk, true));
Result result = await().atMost(5, TimeUnit.SECONDS).until(resultRef::get, notNullValue());
assertEquals(HttpStatus.OK_200, result.getResponse().getStatus());
assertNotNull(result.getRequestFailure());
assertNotNull(result.getResponseFailure());
}
@Test @Test
@Tag("external") @Tag("external")
public void testExternalServer() throws Exception public void testExternalServer() throws Exception

View File

@ -269,11 +269,18 @@ public class HttpClientContinueTest extends AbstractTest
{ {
assertTrue(result.isFailed()); assertTrue(result.isFailed());
assertNotNull(result.getRequestFailure()); assertNotNull(result.getRequestFailure());
assertNull(result.getResponseFailure()); assertEquals(error, result.getResponse().getStatus());
Throwable responseFailure = result.getResponseFailure();
// For HTTP/2 the response may fail because the
// server may not fully read the request content,
// and sends a reset that may drop the response
// content and cause the response failure.
if (responseFailure == null)
{
byte[] content = getContent(); byte[] content = getContent();
assertNotNull(content); assertNotNull(content);
assertTrue(content.length > 0); assertTrue(content.length > 0);
assertEquals(error, result.getResponse().getStatus()); }
latch.countDown(); latch.countDown();
} }
}); });
@ -828,7 +835,7 @@ public class HttpClientContinueTest extends AbstractTest
startServer(Transport.HTTP, new HttpServlet() startServer(Transport.HTTP, new HttpServlet()
{ {
@Override @Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{ {
assertEquals(0, request.getContentLengthLong()); assertEquals(0, request.getContentLengthLong());
assertNotNull(request.getHeader(HttpHeader.EXPECT.asString())); assertNotNull(request.getHeader(HttpHeader.EXPECT.asString()));

View File

@ -196,11 +196,18 @@ public class HttpClientContinueTest extends AbstractTest
{ {
assertTrue(result.isFailed()); assertTrue(result.isFailed());
assertNotNull(result.getRequestFailure()); assertNotNull(result.getRequestFailure());
assertNull(result.getResponseFailure()); assertEquals(error, result.getResponse().getStatus());
Throwable responseFailure = result.getResponseFailure();
// For HTTP/2 the response may fail because the
// server may not fully read the request content,
// and sends a reset that may drop the response
// content and cause the response failure.
if (responseFailure == null)
{
byte[] content = getContent(); byte[] content = getContent();
assertNotNull(content); assertNotNull(content);
assertTrue(content.length > 0); assertTrue(content.length > 0);
assertEquals(error, result.getResponse().getStatus()); }
latch.countDown(); latch.countDown();
} }
}); });