JerseyClientHandler.java
/*
* Copyright (c) 2016, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.netty.connector;
import java.io.IOException;
import java.net.URI;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import javax.ws.rs.core.Response;
import javax.ws.rs.client.ResponseProcessingException;
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.client.ClientResponse;
import org.glassfish.jersey.http.HttpHeaders;
import org.glassfish.jersey.http.ResponseStatus;
import org.glassfish.jersey.netty.connector.internal.NettyInputStream;
import org.glassfish.jersey.netty.connector.internal.RedirectException;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.timeout.IdleStateEvent;
import org.glassfish.jersey.uri.internal.JerseyUriBuilder;
/**
* Jersey implementation of Netty channel handler.
*
* @author Pavel Bucek
*/
class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {
// Modified only by the same thread. No need to synchronize it.
private final Set<URI> redirectUriHistory;
private final ClientRequest jerseyRequest;
private final CompletableFuture<ClientResponse> responseAvailable;
private final CompletableFuture<?> responseDone;
private final NettyConnector connector;
private final NettyHttpRedirectController redirectController;
private final NettyConnectorProvider.Config.RW requestConfiguration;
private NettyInputStream nis;
private ClientResponse jerseyResponse;
private boolean readTimedOut;
JerseyClientHandler(ClientRequest request, CompletableFuture<ClientResponse> responseAvailable,
CompletableFuture<?> responseDone, Set<URI> redirectUriHistory, NettyConnector connector,
NettyConnectorProvider.Config.RW requestConfiguration) {
this.redirectUriHistory = redirectUriHistory;
this.jerseyRequest = request;
this.responseAvailable = responseAvailable;
this.responseDone = responseDone;
this.requestConfiguration = requestConfiguration;
this.connector = connector;
// Follow redirects by default
requestConfiguration.followRedirects(jerseyRequest);
requestConfiguration.maxRedirects(jerseyRequest);
this.redirectController = requestConfiguration.redirectController(jerseyRequest);
this.redirectController.init(requestConfiguration);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
notifyResponse(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
// assert: no-op, if channel is closed after LastHttpContent has been consumed
if (readTimedOut) {
responseDone.completeExceptionally(new TimeoutException("Stream closed: read timeout"));
} else if (jerseyRequest.isCancelled()) {
responseDone.completeExceptionally(new CancellationException());
} else {
responseDone.completeExceptionally(new IOException("Stream closed"));
}
}
protected void notifyResponse(ChannelHandlerContext ctx) {
if (jerseyResponse != null) {
ClientResponse cr = jerseyResponse;
jerseyResponse = null;
int responseStatus = cr.getStatus();
if (Boolean.TRUE.equals(requestConfiguration.followRedirects())
&& (responseStatus == ResponseStatus.Redirect3xx.MOVED_PERMANENTLY_301.getStatusCode()
|| responseStatus == ResponseStatus.Redirect3xx.FOUND_302.getStatusCode()
|| responseStatus == ResponseStatus.Redirect3xx.SEE_OTHER_303.getStatusCode()
|| responseStatus == ResponseStatus.Redirect3xx.TEMPORARY_REDIRECT_307.getStatusCode()
|| responseStatus == ResponseStatus.Redirect3xx.PERMANENT_REDIRECT_308.getStatusCode())) {
String location = cr.getHeaderString(HttpHeaders.LOCATION);
if (location == null || location.isEmpty()) {
responseAvailable.completeExceptionally(new RedirectException(LocalizationMessages.REDIRECT_NO_LOCATION()));
} else {
try {
URI newUri = URI.create(location);
if (!newUri.isAbsolute()) {
final URI originalUri = jerseyRequest.getUri();
newUri = new JerseyUriBuilder()
.scheme(originalUri.getScheme())
.userInfo(originalUri.getUserInfo())
.host(originalUri.getHost())
.port(originalUri.getPort())
.uri(location)
.build();
}
boolean alreadyRequested = !redirectUriHistory.add(newUri);
if (alreadyRequested) {
// infinite loop detection
responseAvailable.completeExceptionally(
new RedirectException(LocalizationMessages.REDIRECT_INFINITE_LOOP()));
} else if (redirectUriHistory.size() > requestConfiguration.maxRedirects.get()) {
// maximal number of redirection
responseAvailable.completeExceptionally(new RedirectException(
LocalizationMessages.REDIRECT_LIMIT_REACHED(requestConfiguration.maxRedirects.get())));
} else {
ClientRequest newReq = new ClientRequest(jerseyRequest);
newReq.setUri(newUri);
ctx.close();
if (redirectController.prepareRedirect(newReq, cr)) {
final NettyConnector newConnector =
new NettyConnector(newReq.getClient(), connector.clientConfiguration);
newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
@Override
public boolean complete(ClientResponse value) {
newConnector.close();
return responseAvailable.complete(value);
}
@Override
public boolean completeExceptionally(Throwable ex) {
newConnector.close();
return responseAvailable.completeExceptionally(ex);
}
});
} else {
responseAvailable.complete(cr);
}
}
} catch (IllegalArgumentException e) {
responseAvailable.completeExceptionally(
new RedirectException(LocalizationMessages.REDIRECT_ERROR_DETERMINING_LOCATION(location)));
}
}
} else {
responseAvailable.complete(cr);
}
}
}
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
if (jerseyRequest.isCancelled()) {
responseAvailable.completeExceptionally(new CancellationException());
return;
}
if (msg instanceof HttpResponse) {
if (msg.decoderResult().isFailure()) {
Throwable cause = msg.decoderResult().cause();
ResponseProcessingException ex = new ResponseProcessingException(null, cause);
responseAvailable.completeExceptionally(ex);
return;
} else {
final HttpResponse response = (HttpResponse) msg;
jerseyResponse = new ClientResponse(new Response.StatusType() {
@Override
public int getStatusCode() {
return response.status().code();
}
@Override
public Response.Status.Family getFamily() {
return Response.Status.Family.familyOf(response.status().code());
}
@Override
public String getReasonPhrase() {
return response.status().reasonPhrase();
}
}, jerseyRequest);
for (Map.Entry<String, String> entry : response.headers().entries()) {
jerseyResponse.getHeaders().add(entry.getKey(), entry.getValue());
}
// request entity handling.
nis = new NettyInputStream();
responseDone.whenComplete((_r, th) -> nis.complete(th));
jerseyResponse.setEntityStream(nis);
}
}
if (msg instanceof HttpContent) {
HttpContent httpContent = (HttpContent) msg;
ByteBuf content = httpContent.content();
if (content.isReadable()) {
content.retain();
if (nis == null) {
nis = new NettyInputStream();
}
nis.publish(content);
}
if (msg instanceof LastHttpContent) {
responseDone.complete(null);
notifyResponse(ctx);
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, final Throwable cause) {
responseDone.completeExceptionally(cause);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
readTimedOut = true;
ctx.close();
} else {
super.userEventTriggered(ctx, evt);
}
}
/* package */ static class ProxyHeaders implements Predicate<String> {
static final ProxyHeaders INSTANCE = new ProxyHeaders();
private static final String HOST = HttpHeaders.HOST.toLowerCase(Locale.ROOT);
private static final String FORWARDED = HttpHeaders.FORWARDED.toLowerCase(Locale.ROOT);
@Override
public boolean test(String headerName) {
String lowName = headerName.toLowerCase(Locale.ROOT);
return lowName.startsWith("proxy-") || lowName.equals(HOST) || lowName.equals(FORWARDED);
}
}
}