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 @@ -66,6 +66,8 @@ Changes log
used by the server to reply (for example HTTP/1.1, HTTP/2.0 or HTTP/3.0), set by the HTTP
client connector. When unknown, the protocol of the request is returned.
`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.
- Bugs fixed
- Slf4j bridge reports wrong line number and caller class in log4j. Issue #233.
- When GET is an allowed method of a server resource, HEAD is now also listed (e.g. in the Allow header of a 405 response). Issue #266.
Expand Down
25 changes: 25 additions & 0 deletions org.restlet/src/main/java/org/restlet/Request.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.restlet.data.Conditions;
import org.restlet.data.Cookie;
import org.restlet.data.Encoding;
import org.restlet.data.Header;
import org.restlet.data.Language;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
Expand Down Expand Up @@ -80,6 +81,9 @@ public static Request getCurrent() {
/** The cookies provided by the client. */
private volatile Series<Cookie> cookies;

/** The trailing headers received with the request entity. */
private volatile Series<Header> trailers;

/** The host reference. */
private volatile Reference hostRef;

Expand Down Expand Up @@ -198,6 +202,8 @@ public Request(Request request) {
getCookies().add(o);
}

getTrailers().addAll(request.getTrailers());

this.hostRef = request.getHostRef();
this.maxForwards = request.getMaxForwards();
this.originalRef =
Expand Down Expand Up @@ -428,6 +434,25 @@ public Series<Cookie> getCookies() {
return c;
}

/**
* Returns the modifiable series of trailing headers received with the request entity. Trailers
* become available when the entity has been fully read.
*
* @return The trailing headers.
*/
public Series<Header> getTrailers() {
Series<Header> result = this.trailers;
if (result == null) {
synchronized (this) {
result = this.trailers;
if (result == null) {
this.trailers = result = new Series<>(Header.class);
}
}
}
return result;
}

/**
* Returns the host reference. This may be different from the resourceRef's host, for example,
* for URNs and other URIs that don't contain host information.<br>
Expand Down
22 changes: 22 additions & 0 deletions org.restlet/src/main/java/org/restlet/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.restlet.data.ChallengeRequest;
import org.restlet.data.CookieSetting;
import org.restlet.data.Dimension;
import org.restlet.data.Header;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.data.Reference;
Expand Down Expand Up @@ -121,6 +122,9 @@ public static void setCurrent(Response response) {
/** The cookie settings provided by the server. */
private volatile Series<CookieSetting> cookieSettings;

/** The trailing headers sent with the response entity. */
private volatile Series<Header> trailers;

/** The set of dimensions on which the response entity may vary. */
private volatile Set<Dimension> dimensions;

Expand Down Expand Up @@ -409,6 +413,24 @@ public Series<CookieSetting> getCookieSettings() {
return c;
}

/**
* Returns the modifiable series of trailing headers to send with the response entity.
*
* @return The trailing headers.
*/
public Series<Header> getTrailers() {
Series<Header> result = this.trailers;
if (result == null) {
synchronized (this) {
result = this.trailers;
if (result == null) {
this.trailers = result = new Series<>(Header.class);
}
}
}
return result;
}

/**
* Returns the modifiable set of selecting dimensions on which the response entity may vary. If
* some server-side content negotiation is done, this set should be properly updated, other it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
package org.restlet.engine.netty;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
Expand Down Expand Up @@ -93,6 +95,12 @@ class NettyClientCall {
/** The response body stream. */
private volatile ChannelInputStream body;

/** The response being updated by this exchange. */
private volatile org.restlet.Response response;

/** The request trailers to send after the request entity. */
private volatile Series<Header> requestTrailers = new Series<>(Header.class);

/** The response status code. */
private volatile int statusCode;

Expand Down Expand Up @@ -174,6 +182,8 @@ boolean isConfidential() {
* @param response The Restlet response to update.
*/
void handle(org.restlet.Request request, org.restlet.Response response) {
this.response = response;
this.requestTrailers = request.getTrailers();
HeaderUtils.addGeneralHeaders(request, requestHeaders);
if (request.getEntity() != null) {
HeaderUtils.addEntityHeaders(request.getEntity(), requestHeaders);
Expand Down Expand Up @@ -363,6 +373,11 @@ private void writeRequest(Representation entity) throws IOException {
Channel ch = this.channel;
HttpRequest nettyRequest = newRequestHead();
boolean hasBody = setBodyHeaders(nettyRequest, entity);
if (!requestTrailers.isEmpty() && !kind.isStream()) {
HttpUtil.setTransferEncodingChunked(nettyRequest, true);
nettyRequest.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
NettyServerCall.setTrailerNames(nettyRequest.headers(), requestTrailers);
}

long timeout = helper.getIdleTimeout();
Writability.await(ch, timeout);
Expand All @@ -379,7 +394,15 @@ private void writeRequest(Representation entity) throws IOException {
}

Writability.await(ch, timeout);
check(ch.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT));
LastHttpContent lastContent = LastHttpContent.EMPTY_LAST_CONTENT;
if (!requestTrailers.isEmpty()) {
DefaultLastHttpContent withTrailers = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER);
for (Header trailer : requestTrailers) {
withTrailers.trailingHeaders().add(trailer.getName(), trailer.getValue());
}
lastContent = withTrailers;
}
check(ch.writeAndFlush(lastContent));

lock.lock();
try {
Expand Down Expand Up @@ -550,7 +573,13 @@ boolean onResponseContent(ByteBuf content) {
}

/** Called on the event loop when the response is fully received. */
void onResponseComplete() {
void onResponseComplete(io.netty.handler.codec.http.HttpHeaders trailers) {
org.restlet.Response currentResponse = response;
if (currentResponse != null) {
for (Map.Entry<String, String> trailer : trailers) {
currentResponse.getTrailers().add(trailer.getKey(), trailer.getValue());
}
}
ChannelInputStream stream = this.body;
if (stream != null) {
stream.end();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ private boolean onContent(NettyClientCall current, HttpContent content) {
}
boolean readMore = current.onResponseContent(content.content());
if (last) {
current.onResponseComplete();
current.onResponseComplete(((LastHttpContent) content).trailingHeaders());
return false;
}
return readMore;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ class NettyRequest extends Request {
setDate((date != null) ? date : new Date());
}

@Override
public Series<Header> getTrailers() {
return call.getRequestTrailers();
}

/** Returns the host reference built from the "Host" header. */
private static String hostRef(Protocol protocol, Series<Header> headers) {
StringBuilder sb = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_HTTPS_SSL_SESSION_ID;
import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_VERSION;

import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
Expand Down Expand Up @@ -97,6 +100,15 @@
/** The response headers. */
private final Series<Header> responseHeaders = new Series<>(Header.class);

/** The trailing headers received with the request entity. */
private final Series<Header> requestTrailers = new Series<>(Header.class);

/** The trailing headers to send with the response entity. */
private Series<Header> responseTrailers = new Series<>(Header.class);

/** Indicates whether response trailers are valid for the response being sent. */
private boolean responseTrailersEnabled;

/** The response status code. */
private volatile int statusCode = 200;

Expand Down Expand Up @@ -246,6 +258,18 @@
return requestHeaders;
}

/** The trailing headers received with the request entity. */
Series<Header> getRequestTrailers() {
return requestTrailers;
}

/** Copies the request trailing headers before the body stream is ended. */
void setRequestTrailers(HttpHeaders trailers) {
for (Map.Entry<String, String> trailer : trailers) {
requestTrailers.add(trailer.getKey(), trailer.getValue());
}
}

/** The exact protocol (HTTP or HTTPS). */
Protocol getProtocol() {
return confidential ? Protocol.HTTPS : Protocol.HTTP;
Expand Down Expand Up @@ -320,6 +344,7 @@
*/
void commit(Response response) {
try {
responseTrailers = response.getTrailers();
prepareEntity(response);
addResponseHeaders(response);
sendResponse(response);
Expand Down Expand Up @@ -499,7 +524,7 @@
*
* @param emptyBody True if no body follows.
*/
private void writeHead(boolean emptyBody) {

Check failure on line 527 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 24 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=restlet_restlet-framework-java&issues=AaFNDesWidhKYWVJUqpX&open=AaFNDesWidhKYWVJUqpX&pullRequest=1539
if (responseHeadWritten) {
return;
}
Expand All @@ -521,22 +546,37 @@
head.headers().set("Alt-Svc", altSvc);
}

boolean hasTrailers = !responseTrailers.isEmpty();
boolean hasLength = head.headers().contains(HttpHeaderNames.CONTENT_LENGTH);
boolean noBodyStatus = code == 204 || code == 304 || code < 200;

if (noBodyStatus || "HEAD".equals(getMethod())) {
hasTrailers = false;
}
responseTrailersEnabled = hasTrailers;

if (kind.isStream()) {
// Connection-specific headers are forbidden in HTTP/2 and HTTP/3.
head.headers().remove(HttpHeaderNames.CONNECTION);
head.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
head.headers().remove("Keep-Alive");
} else {
if (emptyBody) {
if (!hasLength && !noBodyStatus && !"HEAD".equals(getMethod())) {
if (hasTrailers) {
head.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
HttpUtil.setTransferEncodingChunked(head, true);
} else if (!hasLength && !noBodyStatus && !"HEAD".equals(getMethod())) {
head.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
}
} else if (hasTrailers) {
head.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
HttpUtil.setTransferEncodingChunked(head, true);
} else if (!hasLength) {
head.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
}
if (hasTrailers) {
setTrailerNames(head.headers(), responseTrailers);
}
if (!HttpUtil.isKeepAlive(request)) {
head.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
}
Expand Down Expand Up @@ -569,7 +609,13 @@
}

boolean keepAlive = HttpUtil.isKeepAlive(request) && requestBody.isFullyConsumed();
ChannelFuture future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
LastHttpContent lastContent = LastHttpContent.EMPTY_LAST_CONTENT;
if (responseTrailersEnabled) {
DefaultLastHttpContent withTrailers = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER);
copyHeaders(responseTrailers, withTrailers.trailingHeaders());
lastContent = withTrailers;
}
ChannelFuture future = channel.writeAndFlush(lastContent);
requestBody.close();

if (!kind.isStream()) {
Expand All @@ -582,6 +628,23 @@
}
}

static void setTrailerNames(
io.netty.handler.codec.http.HttpHeaders headers, Series<Header> trailers) {
String names =
trailers.stream()
.map(Header::getName)
.distinct()
.collect(java.util.stream.Collectors.joining(", "));
headers.set("Trailer", names);
}

private static void copyHeaders(
Series<Header> source, io.netty.handler.codec.http.HttpHeaders target) {
for (Header header : source) {
target.add(header.getName(), header.getValue());
}
}

/**
* Aborts the exchange by closing the connection.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class NettyServerHandler extends ChannelInboundHandlerAdapter {
/** The body stream of the request in progress, if any. */
private ChannelInputStream currentBody;

/** The call receiving the current request body. */
private NettyServerCall currentCall;

NettyServerHandler(NettyServerHelper helper, boolean confidential, StreamKind kind) {
this.helper = helper;
this.confidential = confidential;
Expand Down Expand Up @@ -108,6 +111,7 @@ private void onRequest(ChannelHandlerContext ctx, HttpRequest request) {

NettyServerCall call =
new NettyServerCall(helper, ctx.channel(), kind, request, body, confidential);
currentCall = call;
if (kind != StreamKind.HTTP3) {
call.setAltSvc(helper.getAltSvc());
}
Expand Down Expand Up @@ -144,6 +148,10 @@ private void onContent(ChannelHandlerContext ctx, HttpContent content) {
readMore = body.offer(content.content().retain());
}
boolean last = content instanceof LastHttpContent;
if (last && currentCall != null) {
currentCall.setRequestTrailers(((LastHttpContent) content).trailingHeaders());
currentCall = null;
}
content.release();

if (last) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.restlet.data.ClientInfo;
import org.restlet.data.Conditions;
import org.restlet.data.Cookie;
import org.restlet.data.Header;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
Expand Down Expand Up @@ -133,6 +134,11 @@ public Series<Cookie> getCookies() {
return getWrappedRequest().getCookies();
}

@Override
public Series<Header> getTrailers() {
return getWrappedRequest().getTrailers();
}

/**
* Returns the entity representation.
*
Expand Down
Loading
Loading