Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Changes log
`Response.toString()` now shows it.
- Added request and response trailers to the HTTP connectors, supporting HTTP/1.1 chunked
transfers and HTTP/2 trailing headers.
- HTTP/2 and HTTP/3 responses can add trailers after streaming the response head and body;
HTTP/1.1 still requires trailer names to be known before the response head is sent.
- HTTP/2 clients receive a response as soon as its headers arrive, while the request entity
continues to be written on a virtual thread.
- Active client requests can be aborted; HTTP stream failures surface as I/O errors and a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@
*
* @param emptyBody True if no body follows.
*/
private void writeHead(boolean emptyBody) {

Check failure on line 555 in org.restlet/src/main/java/org/restlet/engine/netty/NettyServerCall.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 30 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=restlet_restlet-framework-java&issues=AaFbCAEOK5Ri1uV4FaWc&open=AaFbCAEOK5Ri1uV4FaWc&pullRequest=1553
if (responseHeadWritten) {
return;
}
Expand Down Expand Up @@ -583,7 +583,8 @@
if (noBodyStatus || "HEAD".equals(getMethod())) {
hasTrailers = false;
}
responseTrailersEnabled = hasTrailers;
responseTrailersEnabled =
hasTrailers || (kind.isStream() && !noBodyStatus && !"HEAD".equals(getMethod()));

if (kind.isStream()) {
// Connection-specific headers are forbidden in HTTP/2 and HTTP/3.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.X509Certificate;
Expand Down Expand Up @@ -175,6 +176,32 @@ void getAndPutOverHttp3() throws Exception {
assertEquals(Set.of("HTTP/3.0"), versions);
}

@Test
void exchangesTrailersAddedAfterTheFirstBodyFlushOverHttp3() throws Exception {
Restlet streaming =
new Restlet() {
@Override
public void handle(Request request, Response response) {
response.setEntity(
new OutputRepresentation(MediaType.APPLICATION_OCTET_STREAM) {
@Override
public void write(OutputStream output) throws IOException {
output.write("first".getBytes(StandardCharsets.UTF_8));
output.flush();
response.getTrailers().add("X-Response-Trailer", "late");
output.write("second".getBytes(StandardCharsets.UTF_8));
}
});
}
};
startServer("HTTP1_1,HTTP3", streaming);
startClient("HTTP3");

Response response = client.handle(new Request(Method.GET, uri("/")));
assertEquals("firstsecond", response.getEntityAsText());
assertEquals("late", response.getTrailers().getFirstValue("X-Response-Trailer", true));
}

@Test
void tcpResponsesAdvertiseHttp3() throws Exception {
startServer("HTTP2,HTTP1_1,HTTP3", echo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -25,6 +28,7 @@
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.engine.Engine;
import org.restlet.representation.OutputRepresentation;
import org.restlet.representation.StringRepresentation;

class HttpTrailersTestCase {
Expand Down Expand Up @@ -107,4 +111,38 @@ void exchangesTrailersOverHttp11() throws Exception {
void exchangesTrailersOverHttp2() throws Exception {
roundTrip("HTTP2", "HTTP2");
}

@Test
void exchangesHttp2TrailersAddedAfterTheFirstBodyFlush() throws Exception {
Restlet streaming =
new Restlet() {
@Override
public void handle(Request request, Response response) {
response.setEntity(
new OutputRepresentation(MediaType.APPLICATION_OCTET_STREAM) {
@Override
public void write(OutputStream output) throws IOException {
output.write("first".getBytes(StandardCharsets.UTF_8));
output.flush();
response.getTrailers().add(RESPONSE_TRAILER, "late");
output.write("second".getBytes(StandardCharsets.UTF_8));
}
});
}
};
Context serverContext = new Context();
serverContext.getParameters().add("http.transport.protocol", "HTTP2");
server = new Server(serverContext, Protocol.HTTP, 0, streaming);
server.start();
Context clientContext = new Context();
clientContext.getParameters().add("httpClientTransportMode", "HTTP2");
client = new Client(clientContext, Protocol.HTTP);
client.start();

Response response =
client.handle(
new Request(Method.GET, "http://localhost:" + server.getActualPort()));
assertEquals("firstsecond", response.getEntityAsText());
assertEquals("late", response.getTrailers().getFirstValue(RESPONSE_TRAILER, true));
}
}
Loading