com.nike.riposte.server.handler.RequestContentValidationHandler 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.riposte.server.channelpipeline.ChannelAttributes;
import com.nike.riposte.server.error.exception.MissingRequiredContentException;
import com.nike.riposte.server.error.validation.RequestValidator;
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.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Function;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.LastHttpContent;
import static com.nike.riposte.util.AsyncNettyHelper.runnableWithTracingAndMdc;
/**
* Looks at the current channel state's {@link HttpProcessingState#getEndpointForExecution()} and {@link
* HttpProcessingState#getRequestInfo()} to see if (1) the endpoint wants the incoming request content validated, and
* (2) the incoming request's {@link RequestInfo#getContent()} is populated. If both those things are true then {@link
* #validationService} will be run on the request's content, which will throw an appropriate exception with validation
* violation details. If {@link #validationService} is null then no validation will be performed.
*
* NOTE: Both the payload deserialization and validation may be done asynchronously if {@link
* Endpoint#shouldValidateAsynchronously(RequestInfo)} returns true. If asynchronous processing is requested then the
* deserialization and validation will be done via {@link CompletableFuture} running in a separate {@link Executor}.
* That future is added to {@link HttpProcessingState#addPreEndpointExecutionWorkChainSegment(Function)} so that the
* endpoint handler can make sure the future completes successfully before the endpoint execution happens. If the future
* throws an exception then the endpoint should not be executed.
*
* This must come after {@link com.nike.riposte.server.handler.RequestContentDeserializerHandler} in the pipeline to
* make sure that the {@link com.nike.riposte.server.http.RequestInfo#getContent()} has had a chance to be populated.
*
* @author Nic Munroe
*/
public class RequestContentValidationHandler extends BaseInboundHandlerWithTracingAndMdcSupport {
private static final Executor ASYNC_VALIDATION_EXECUTOR =
Executors.newWorkStealingPool(Runtime.getRuntime().availableProcessors() * 2);
private final RequestValidator validationService;
public RequestContentValidationHandler(RequestValidator validationService) {
if (validationService == null) {
throw new IllegalArgumentException(
"validationService cannot be null. If you don't have a validationService to pass in, don't register "
+ "this handler in the pipeline"
);
}
this.validationService = validationService;
}
@Override
public PipelineContinuationBehavior doChannelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof LastHttpContent) {
HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get();
Endpoint> endpoint = state.getEndpointForExecution();
RequestInfo> requestInfo = state.getRequestInfo();
if (endpoint != null && requestInfo.isCompleteRequestWithAllChunks()) {
if (endpoint.isRequireRequestContent() && requestInfo.getRawContentLengthInBytes() == 0) {
throw new MissingRequiredContentException(requestInfo, endpoint);
}
if (endpoint.isValidateRequestContent(requestInfo)
//TODO: Is this actually necessary? Does false indicate a misconfigured endpoint?
&& requestInfo.isContentDeserializerSetup() // Content deserialization must be possible
) {
if (endpoint.shouldValidateAsynchronously(requestInfo)) {
// The endpoint has requested asynchronous validation, so split it off into the
// pre-endpoint-execution-work-chain.
state.addPreEndpointExecutionWorkChainSegment(aVoid -> CompletableFuture.runAsync(
() -> executeValidation(requestInfo, endpoint, ctx),
ASYNC_VALIDATION_EXECUTOR)
);
}
else {
// This request can be validated synchronously, so do it now.
executeValidation(requestInfo, endpoint, ctx);
}
}
}
}
return PipelineContinuationBehavior.CONTINUE;
}
@Override
protected boolean argsAreEligibleForLinkingAndUnlinkingDistributedTracingInfo(
HandlerMethodToExecute methodToExecute, ChannelHandlerContext ctx, Object msgOrEvt, Throwable cause
) {
// To save on extraneous linking/unlinking, we'll do it as-necessary in this class.
return false;
}
@SuppressWarnings("WeakerAccess")
protected void executeValidation(RequestInfo> requestInfo, Endpoint> endpoint, ChannelHandlerContext ctx) {
runnableWithTracingAndMdc(() -> {
// NOTE: The requestInfo.getContent() call should be here in this method because this is the first time
// getContent() is called and will deserialize the payload here. For very large payloads it may take a
// while to deserialize, and if we're validating asynchronously we should be deserializing
// asynchronously as well.
// TODO: Is null check required here before validation now that we are doing a missing content check?
if (requestInfo.getContent() != null) {
// We have an endpoint and validation is requested and the content has been deserialized.
// Perform the validation.
Class>[] validationGroups = endpoint.validationGroups(requestInfo);
if (validationGroups == null)
validationService.validateRequestContent(requestInfo);
else
validationService.validateRequestContent(requestInfo, validationGroups);
}
}, ctx).run();
}
}