com.nike.riposte.server.handler.RoutingHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of riposte-core Show documentation
Show all versions of riposte-core Show documentation
Riposte module riposte-core
package com.nike.riposte.server.handler;
import com.nike.internal.util.Pair;
import com.nike.riposte.server.channelpipeline.ChannelAttributes;
import com.nike.riposte.server.error.exception.MethodNotAllowed405Exception;
import com.nike.riposte.server.error.exception.MultipleMatchingEndpointsException;
import com.nike.riposte.server.error.exception.PathNotFound404Exception;
import com.nike.riposte.server.error.exception.RequestTooBigException;
import com.nike.riposte.server.handler.base.BaseInboundHandlerWithTracingAndMdcSupport;
import com.nike.riposte.server.handler.base.PipelineContinuationBehavior;
import com.nike.riposte.server.http.Endpoint;
import com.nike.riposte.server.http.HttpProcessingState;
import com.nike.riposte.server.http.RequestInfo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import static com.nike.riposte.util.HttpUtils.getConfiguredMaxRequestSize;
import static com.nike.riposte.util.HttpUtils.isMaxRequestSizeValidationDisabled;
/**
* Handles the logic of determining which {@link Endpoint} matches the {@link RequestInfo} in the channel's current
* state ({@link com.nike.riposte.server.http.HttpProcessingState#getRequestInfo()}). It also performs some error
* checking to make sure exactly 1 endpoint matches. See {@link #findSingleEndpointForExecution(RequestInfo)} for more
* information on the error checking.
*
* This must come before {@link SmartHttpContentDecompressor} in the pipeline so that it can turn off auto-decompression
* for endpoints that aren't supposed to do auto-decompression (e.g. {@link
* com.nike.riposte.server.http.ProxyRouterEndpoint}s). Consequently it must also come before {@link
* RequestInfoSetterHandler}, which means we have to do the creation of the {@link RequestInfo} from the incoming
* Netty {@link HttpRequest} message and set it on {@link HttpProcessingState} if the state didn't already have a
* {@link RequestInfo}.
*
* @author Nic Munroe
*/
public class RoutingHandler extends BaseInboundHandlerWithTracingAndMdcSupport {
protected final RiposteHandlerInternalUtil handlerUtils = RiposteHandlerInternalUtil.DEFAULT_IMPL;
protected final Collection> endpoints;
protected final int globalConfiguredMaxRequestSizeInBytes;
public RoutingHandler(Collection> endpoints, int globalMaxRequestSizeInBytes) {
if (endpoints == null || endpoints.isEmpty())
throw new IllegalArgumentException("endpoints cannot be empty");
this.endpoints = endpoints;
this.globalConfiguredMaxRequestSizeInBytes = globalMaxRequestSizeInBytes;
}
/**
* @return The single {@link Endpoint} that matches and wants to handle the given request (and the path pattern that
* the endpoint used when deciding it wanted to handle the request). This will throw a {@link
* PathNotFound404Exception} if there are no matching endpoints. It will throw a {@link
* MethodNotAllowed405Exception} if there's an endpoint that matches the path but not the HTTP method of the
* request, and this will throw a {@link MultipleMatchingEndpointsException} if there are multiple endpoints that
* fully match the path and HTTP method.
*/
@SuppressWarnings("WeakerAccess")
protected Pair, String> findSingleEndpointForExecution(RequestInfo requestInfo) {
boolean hasPathMatch = false;
List> fullyMatchingEndpoints = new ArrayList<>(1);
String matchingPattern = "";
for (Endpoint> endpoint : endpoints) {
Optional pattern = endpoint.requestMatcher().matchesPath(requestInfo);
if (pattern.isPresent()) {
hasPathMatch = true;
if (endpoint.requestMatcher().matchesMethod(requestInfo)) {
fullyMatchingEndpoints.add(endpoint);
matchingPattern = pattern.get();
}
}
}
// If there's no endpoint that even matches the path then this is a 404 situation.
if (!hasPathMatch) {
throw new PathNotFound404Exception(
"No matching endpoint found. requested_uri_path=" + requestInfo.getPath() + ", requested_method="
+ requestInfo.getMethod());
}
// We have at least one path match. fullyMatchingEndpoints will now tell us how many matched both path
// *and* HTTP method.
// Do error checking.
if (fullyMatchingEndpoints.isEmpty()) {
// Not a 404 because we did have at least one endpoint that matched the path, but none matched both path and
// HTTP method so we throw a 405.
throw new MethodNotAllowed405Exception(
"Found path match for incoming request, but no endpoint matched both path and HTTP method",
requestInfo.getPath(), String.valueOf(requestInfo.getMethod()));
}
if (fullyMatchingEndpoints.size() > 1) {
// More than 1 endpoint matched. Also not ok.
throw new MultipleMatchingEndpointsException(
"Found multiple endpoints that matched the incoming request's path and HTTP method. This is not "
+ "allowed - your endpoints must be structured so that only one endpoint can match any given request",
fullyMatchingEndpoints, requestInfo.getPath(), String.valueOf(requestInfo.getMethod())
);
}
// At this point we know there's exactly 1 fully matching endpoint, so go ahead and return it.
return Pair.of(fullyMatchingEndpoints.get(0), matchingPattern);
}
@Override
public PipelineContinuationBehavior doChannelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HttpRequest) {
HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get();
RequestInfo request = handlerUtils.createRequestInfoFromNettyHttpRequestAndHandleStateSetupIfNecessary(
(HttpRequest)msg,
state
);
Pair, String> endpointForExecution = findSingleEndpointForExecution(request);
request.setPathParamsBasedOnPathTemplate(endpointForExecution.getRight());
state.setEndpointForExecution(endpointForExecution.getLeft(), endpointForExecution.getRight());
throwExceptionIfContentLengthHeaderIsLargerThanConfiguredMaxRequestSize(
(HttpRequest) msg, endpointForExecution.getLeft()
);
}
return PipelineContinuationBehavior.CONTINUE;
}
private void throwExceptionIfContentLengthHeaderIsLargerThanConfiguredMaxRequestSize(HttpRequest msg, Endpoint> endpoint) {
int configuredMaxRequestSize = getConfiguredMaxRequestSize(endpoint, globalConfiguredMaxRequestSizeInBytes);
if (!isMaxRequestSizeValidationDisabled(configuredMaxRequestSize)
&& HttpHeaders.isContentLengthSet(msg)
&& HttpHeaders.getContentLength(msg) > configuredMaxRequestSize) {
throw new RequestTooBigException(
"Content-Length header value exceeded configured max request size of " + configuredMaxRequestSize
);
}
}
@Override
protected boolean argsAreEligibleForLinkingAndUnlinkingDistributedTracingInfo(
HandlerMethodToExecute methodToExecute, ChannelHandlerContext ctx, Object msgOrEvt, Throwable cause
) {
// This class does not log, and nothing that happens in this class should cause logging to happen elsewhere.
// Therefore we should never bother with linking/unlinking tracing info to save on the extra processing.
return false;
}
}