
io.micronaut.http.server.RequestLifecycle Maven / Gradle / Ivy
/*
* Copyright 2017-2022 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.server;
import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.exceptions.ConversionErrorException;
import io.micronaut.core.execution.ExecutionFlow;
import io.micronaut.core.propagation.PropagatedContext;
import io.micronaut.core.type.ReturnType;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.http.HttpAttributes;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.body.MessageBodyHandlerRegistry;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.http.filter.FilterRunner;
import io.micronaut.http.filter.GenericHttpFilter;
import io.micronaut.http.server.exceptions.*;
import io.micronaut.http.server.exceptions.response.ErrorContext;
import io.micronaut.http.server.types.files.FileCustomizableResponseType;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.json.JsonSyntaxException;
import io.micronaut.web.router.DefaultRouteInfo;
import io.micronaut.web.router.DefaultUriRouteMatch;
import io.micronaut.web.router.RouteInfo;
import io.micronaut.web.router.RouteMatch;
import io.micronaut.web.router.UriRouteMatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.function.BiFunction;
import java.util.function.Supplier;
/**
* This class handles the full route processing lifecycle for a request.
*
* @author Jonas Konrad
* @since 4.0.0
*/
@Internal
public class RequestLifecycle {
private static final Logger LOG = LoggerFactory.getLogger(RequestLifecycle.class);
private final RouteExecutor routeExecutor;
private final boolean multipartEnabled;
private HttpRequest> request;
/**
* @param routeExecutor The route executor to use for route resolution
*/
protected RequestLifecycle(RouteExecutor routeExecutor) {
this.routeExecutor = Objects.requireNonNull(routeExecutor, "routeExecutor");
Optional isMultiPartEnabled = routeExecutor.serverConfiguration.getMultipart().getEnabled();
this.multipartEnabled = isMultiPartEnabled.isEmpty() || isMultiPartEnabled.get();
}
/**
* @param routeExecutor The route executor to use for route resolution
* @param request The request
* @deprecated Will be removed after 4.3.0
*/
@Deprecated(forRemoval = true, since = "4.3.0")
protected RequestLifecycle(RouteExecutor routeExecutor, HttpRequest> request) {
this(routeExecutor);
this.request = request;
}
/**
* Execute this request normally.
*
* @return The response to the request.
* @deprecated Will be removed after 4.3.0
*/
@Deprecated(forRemoval = true, since = "4.3.0")
protected final ExecutionFlow> normalFlow() {
return normalFlow(request);
}
/**
* The request for this lifecycle. This may be changed by filters.
*
* @return The current request
* @deprecated Will be removed after 4.3.0
*/
@Deprecated(forRemoval = true, since = "4.3.0")
protected final HttpRequest> request() {
return request;
}
/**
* Try to find a static file for this request. If there is a file, filters will still run, but
* only after the call to this method.
*
* @return The file at this path, or {@code null} if none is found
* @deprecated Will be removed after 4.3.0
*/
@Deprecated(forRemoval = true, since = "4.3.0")
@Nullable
protected FileCustomizableResponseType findFile() {
return null;
}
/**
* Execute this request normally.
*
* @param request The request
* @return The response to the request.
*/
protected final ExecutionFlow> normalFlow(HttpRequest> request) {
try {
Objects.requireNonNull(request, "request");
if (!multipartEnabled) {
MediaType contentType = request.getContentType().orElse(null);
if (contentType != null &&
contentType.equals(MediaType.MULTIPART_FORM_DATA_TYPE)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Multipart uploads have been disabled via configuration. Rejected request for URI {}, method {}, and content type {}", request.getUri(),
request.getMethodName(), contentType);
}
return onStatusError(
request,
HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE),
"Content Type [" + contentType + "] not allowed"
);
}
}
return runServerFilters(request);
} catch (Throwable t) {
return onError(request, t);
}
}
private ExecutionFlow> executeRoute(HttpRequest> request,
PropagatedContext propagatedContext,
RouteMatch> routeMatch) {
ExecutionFlow> routeMatchFlow = fulfillArguments(routeMatch, request);
ExecutionFlow> responseFlow = callRoute(routeMatchFlow, request, propagatedContext);
responseFlow = handleStatusException(responseFlow, request, routeMatch, propagatedContext);
return onErrorNoFilter(responseFlow, request, propagatedContext);
}
private ExecutionFlow> callRoute(ExecutionFlow> flux,
HttpRequest> filteredRequest,
PropagatedContext propagatedContext) {
Object o = ((ExecutionFlow>) flux).tryCompleteValue();
// usually this is a DefaultUriRouteMatch, avoid scalability issues here
RouteMatch> routeMatch = o instanceof DefaultUriRouteMatch, ?> urm ? urm : (RouteMatch>) o;
if (routeMatch != null) {
return routeExecutor.callRoute(propagatedContext, routeMatch, filteredRequest);
}
return flux.flatMap(rm -> routeExecutor.callRoute(propagatedContext, rm, filteredRequest));
}
private ExecutionFlow> handleStatusException(ExecutionFlow> flux,
HttpRequest> request,
RouteMatch> routeMatch,
PropagatedContext propagatedContext) {
Object o = ((ExecutionFlow>) flux).tryCompleteValue();
// usually this is a MutableHttpResponse, avoid scalability issues here
HttpResponse> response = o instanceof MutableHttpResponse> mut ? mut : (HttpResponse>) o;
if (response != null) {
return handleStatusException(request, response, routeMatch, propagatedContext);
}
return flux.flatMap(res -> handleStatusException(request, res, routeMatch, propagatedContext));
}
private ExecutionFlow> onErrorNoFilter(ExecutionFlow> flux,
HttpRequest> request,
PropagatedContext propagatedContext) {
if (flux.tryCompleteValue() != null) {
return flux;
}
Throwable throwable = flux.tryCompleteError();
if (throwable != null) {
return onErrorNoFilter(request, throwable, propagatedContext);
}
return flux.onErrorResume(exp -> onErrorNoFilter(request, exp, propagatedContext));
}
/**
* Handle an error in this request. It also runs filters for the error handling.
*
* @param request The request
* @param throwable The error
* @return The response for the error
*/
protected final ExecutionFlow> onError(HttpRequest> request, Throwable throwable) {
try {
return runWithFilters(request, (filteredRequest, propagatedContext) -> onErrorNoFilter(filteredRequest, throwable, propagatedContext))
.onErrorResume(t -> createDefaultErrorResponseFlow(request, t));
} catch (Throwable e) {
return createDefaultErrorResponseFlow(request, e);
}
}
private ExecutionFlow> onErrorNoFilter(HttpRequest> request, Throwable t, PropagatedContext propagatedContext) {
if ((t instanceof CompletionException || t instanceof ExecutionException) && t.getCause() != null) {
// top level exceptions returned by CompletableFutures. These always wrap the real exception thrown.
t = t.getCause();
}
if (t instanceof ConversionErrorException cee && cee.getCause() instanceof JsonSyntaxException jse) {
// with delayed parsing, json syntax errors show up as conversion errors
t = jse;
}
final Throwable cause = t;
RouteMatch> errorRoute = routeExecutor.findErrorRoute(cause, findDeclaringType(request), request);
if (errorRoute != null) {
return handleErrorRoute(request, propagatedContext, errorRoute, cause);
} else {
Optional> optionalDefinition = routeExecutor.beanContext.findBeanDefinition(ExceptionHandler.class, Qualifiers.byTypeArgumentsClosest(cause.getClass(), Object.class));
if (optionalDefinition.isPresent()) {
BeanDefinition handlerDefinition = optionalDefinition.get();
return handlerExceptionHandler(request, propagatedContext, handlerDefinition, cause);
}
if (RouteExecutor.isIgnorable(cause)) {
RouteExecutor.logIgnoredException(cause);
return ExecutionFlow.empty();
}
return createDefaultErrorResponseFlow(request, cause);
}
}
private Class> findDeclaringType(HttpRequest> request) {
// find the origination of the route
Optional previousRequestRouteInfo = request.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class);
return previousRequestRouteInfo.map(RouteInfo::getDeclaringType).orElse(null);
}
private ExecutionFlow> handleErrorRoute(HttpRequest> request, PropagatedContext propagatedContext, RouteMatch> errorRoute, Throwable cause) {
RouteExecutor.setRouteAttributes(request, errorRoute);
if (routeExecutor.serverConfiguration.isLogHandledExceptions()) {
routeExecutor.logException(cause);
}
try {
return ExecutionFlow.just(errorRoute)
.flatMap(routeMatch -> routeExecutor.callRoute(propagatedContext, routeMatch, request)
.flatMap(res -> handleStatusException(request, res, routeMatch, propagatedContext))
)
.onErrorResume(u -> createDefaultErrorResponseFlow(request, u))
.>map(response -> {
response.setAttribute(HttpAttributes.EXCEPTION, cause);
return response;
})
.onErrorResume(throwable -> createDefaultErrorResponseFlow(request, throwable));
} catch (Throwable e) {
return createDefaultErrorResponseFlow(request, e);
}
}
private ExecutionFlow> handlerExceptionHandler(HttpRequest> request, PropagatedContext propagatedContext, BeanDefinition handlerDefinition, Throwable cause) {
final Optional> optionalMethod = handlerDefinition.findPossibleMethods("handle").findFirst();
RouteInfo
© 2015 - 2025 Weber Informatics LLC | Privacy Policy