Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.micronaut.http.server.RouteExecutor Maven / Gradle / Ivy
/*
* Copyright 2017-2021 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.BeanContext;
import io.micronaut.context.exceptions.BeanCreationException;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.async.propagation.ReactivePropagation;
import io.micronaut.core.async.propagation.ReactorPropagation;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.execution.CompletableFutureExecutionFlow;
import io.micronaut.core.execution.ExecutionFlow;
import io.micronaut.core.io.buffer.ReferenceCounted;
import io.micronaut.core.propagation.PropagatedContext;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.ReturnType;
import io.micronaut.http.BasicHttpAttributes;
import io.micronaut.http.HttpHeaders;
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.MutableHttpHeaders;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.bind.binders.ContinuationArgumentBinder;
import io.micronaut.http.body.MessageBodyWriter;
import io.micronaut.http.codec.CodecException;
import io.micronaut.http.context.ServerHttpRequestContext;
import io.micronaut.http.context.ServerRequestContext;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.http.reactive.execution.ReactiveExecutionFlow;
import io.micronaut.http.server.binding.RequestArgumentSatisfier;
import io.micronaut.http.server.exceptions.response.ErrorContext;
import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor;
import io.micronaut.inject.BeanType;
import io.micronaut.inject.MethodReference;
import io.micronaut.scheduling.executor.ExecutorSelector;
import io.micronaut.scheduling.instrument.InstrumentedExecutorService;
import io.micronaut.scheduling.instrument.InstrumentedScheduledExecutorService;
import io.micronaut.web.router.DefaultRouteInfo;
import io.micronaut.web.router.MethodBasedRouteInfo;
import io.micronaut.web.router.RouteAttributes;
import io.micronaut.web.router.RouteInfo;
import io.micronaut.web.router.RouteMatch;
import io.micronaut.web.router.Router;
import io.micronaut.web.router.UriRouteMatch;
import io.micronaut.web.router.exceptions.UnsatisfiedRouteException;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.CorePublisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import static io.micronaut.core.util.KotlinUtils.isKotlinCoroutineSuspended;
import static io.micronaut.inject.beans.KotlinExecutableMethodUtils.isKotlinFunctionReturnTypeUnit;
/**
* A class responsible for executing routes.
*
* @author James Kleeh
* @since 3.0.0
*/
@Singleton
public final class RouteExecutor {
private static final Logger LOG = LoggerFactory.getLogger(RouteExecutor.class);
/**
* Also present in netty RoutingInBoundHandler.
*/
private static final Pattern IGNORABLE_ERROR_MESSAGE = Pattern.compile(
"^.*(?:connection (?:reset|closed|abort|broken)|broken pipe).*$", Pattern.CASE_INSENSITIVE);
final Router router;
final BeanContext beanContext;
final RequestArgumentSatisfier requestArgumentSatisfier;
final HttpServerConfiguration serverConfiguration;
final ErrorResponseProcessor> errorResponseProcessor;
private final ExecutorSelector executorSelector;
private final Optional coroutineHelper;
private final ConversionService conversionService;
/**
* Default constructor.
*
* @param router The router
* @param beanContext The bean context
* @param requestArgumentSatisfier The request argument satisfier
* @param serverConfiguration The server configuration
* @param errorResponseProcessor The error response processor
* @param executorSelector The executor selector
*/
public RouteExecutor(Router router,
BeanContext beanContext,
RequestArgumentSatisfier requestArgumentSatisfier,
HttpServerConfiguration serverConfiguration,
ErrorResponseProcessor> errorResponseProcessor,
ExecutorSelector executorSelector) {
this.router = router;
this.beanContext = beanContext;
this.requestArgumentSatisfier = requestArgumentSatisfier;
this.serverConfiguration = serverConfiguration;
this.errorResponseProcessor = errorResponseProcessor;
this.executorSelector = executorSelector;
this.coroutineHelper = beanContext.findBean(CoroutineHelper.class);
this.conversionService = beanContext.getConversionService();
}
/**
* @return The router
*/
public @NonNull Router getRouter() {
return router;
}
/**
* @return The request argument satisfier
*/
@Internal
public @NonNull RequestArgumentSatisfier getRequestArgumentSatisfier() {
return requestArgumentSatisfier;
}
/**
* @return The error response processor
*/
public @NonNull ErrorResponseProcessor> getErrorResponseProcessor() {
return errorResponseProcessor;
}
/**
* @return The executor selector
*/
public @NonNull ExecutorSelector getExecutorSelector() {
return executorSelector;
}
/**
* @return The kotlin coroutine helper
*/
public Optional getCoroutineHelper() {
return coroutineHelper;
}
@Nullable
UriRouteMatch findRouteMatch(HttpRequest> httpRequest) {
return router.findClosest(httpRequest);
}
static void setRouteAttributes(HttpRequest> request, UriRouteMatch route) {
setRouteAttributes(request, (RouteMatch>) route);
BasicHttpAttributes.setUriTemplate(request, route.getRouteInfo().getUriMatchTemplate().toString());
}
static void setRouteAttributes(HttpRequest> request, RouteMatch> route) {
RouteAttributes.setRouteMatch(request, route);
RouteAttributes.setRouteInfo(request, route.getRouteInfo());
}
/**
* Creates a default error response. Should be used when a response could not be retrieved
* from any other method.
*
* @param httpRequest The request that case the exception
* @param cause The exception that occurred
* @return A response to represent the exception
*/
public MutableHttpResponse> createDefaultErrorResponse(HttpRequest> httpRequest,
Throwable cause) {
logException(cause);
MutableHttpResponse> mutableHttpResponse = HttpResponse.serverError();
RouteAttributes.setException(mutableHttpResponse, cause);
RouteAttributes.setRouteInfo(mutableHttpResponse, new DefaultRouteInfo<>(
ReturnType.of(MutableHttpResponse.class, Argument.OBJECT_ARGUMENT),
Object.class,
true,
false));
try {
mutableHttpResponse = errorResponseProcessor.processResponse(
ErrorContext.builder(httpRequest)
.cause(cause)
.errorMessage("Internal Server Error: " + cause.getMessage())
.build(), mutableHttpResponse);
} catch (Exception e) {
logException(e);
}
applyConfiguredHeaders(mutableHttpResponse.getHeaders());
if (mutableHttpResponse.getContentType().isEmpty() && httpRequest.getMethod() != HttpMethod.HEAD) {
return mutableHttpResponse.contentType(MediaType.APPLICATION_JSON_TYPE);
}
return mutableHttpResponse;
}
/**
* @param request The request
* @param finalRoute The route
* @return The default content type declared on the route
*/
public MediaType resolveDefaultResponseContentType(HttpRequest> request, RouteInfo> finalRoute) {
final List producesList = finalRoute.getProduces();
if (request != null) {
final Iterator i = request.accept().iterator();
if (i.hasNext()) {
final MediaType mt = i.next();
if (producesList.contains(mt)) {
return mt;
}
}
}
MediaType defaultResponseMediaType;
final Iterator produces = producesList.iterator();
if (produces.hasNext()) {
defaultResponseMediaType = produces.next();
} else {
defaultResponseMediaType = MediaType.APPLICATION_JSON_TYPE;
}
return defaultResponseMediaType;
}
private MutableHttpResponse> notFoundErrorResponse(HttpRequest> request) {
MutableHttpResponse> response = errorResponseProcessor.processResponse(
ErrorContext.builder(request)
.errorMessage("Page Not Found")
.build(), HttpResponse.notFound());
if (response.getContentType().isEmpty() && request.getMethod() != HttpMethod.HEAD) {
return response.contentType(MediaType.APPLICATION_JSON_TYPE);
}
return response;
}
void logException(Throwable cause) {
//handling connection reset by peer exceptions
if (isIgnorable(cause)) {
logIgnoredException(cause);
} else {
if (LOG.isErrorEnabled()) {
LOG.error("Unexpected error occurred: {}", cause.getMessage(), cause);
}
}
}
static boolean isIgnorable(Throwable cause) {
if (cause instanceof ClosedChannelException) {
return true;
}
String message = cause.getMessage();
return cause instanceof IOException && message != null && IGNORABLE_ERROR_MESSAGE.matcher(message).matches();
}
static void logIgnoredException(Throwable cause) {
if (LOG.isDebugEnabled()) {
LOG.debug("Swallowed an IOException caused by client connectivity: {}", cause.getMessage(), cause);
}
}
RouteMatch> findErrorRoute(Throwable cause,
Class> declaringType,
HttpRequest> httpRequest) {
RouteMatch> errorRoute = null;
if (cause instanceof BeanCreationException beanCreationException && declaringType != null) {
// If the controller could not be instantiated, don't look for a local error route
Optional> rootBeanType = beanCreationException.getRootBeanType().map(BeanType::getBeanType);
if (rootBeanType.isPresent() && declaringType == rootBeanType.get()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Failed to instantiate [{}]. Skipping lookup of a local error route", declaringType.getName());
}
declaringType = null;
}
}
// First try to find an error route by the exception
if (declaringType != null) {
// handle error with a method that is non-global with exception
errorRoute = router.findErrorRoute(declaringType, cause, httpRequest).orElse(null);
}
if (errorRoute == null) {
// handle error with a method that is global with exception
errorRoute = router.findErrorRoute(cause, httpRequest).orElse(null);
}
if (errorRoute == null) {
// Second try is by status route if the status is known
HttpStatus errorStatus = null;
if (cause instanceof UnsatisfiedRouteException || cause instanceof CodecException) {
// when arguments do not match, then there is UnsatisfiedRouteException, we can handle this with a routed bad request
// or when incoming request body is not in the expected format
errorStatus = HttpStatus.BAD_REQUEST;
} else if (cause instanceof HttpStatusException statusException) {
errorStatus = statusException.getStatus();
}
if (errorStatus != null) {
if (declaringType != null) {
// handle error with a method that is non-global with bad request
errorRoute = router.findStatusRoute(declaringType, errorStatus, httpRequest).orElse(null);
}
if (errorRoute == null) {
// handle error with a method that is global with bad request
errorRoute = router.findStatusRoute(errorStatus, httpRequest).orElse(null);
}
}
}
if (errorRoute != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Found matching exception handler for exception [{}]: {}", cause.getMessage(), errorRoute);
}
setRouteAttributes(httpRequest, errorRoute);
requestArgumentSatisfier.fulfillArgumentRequirementsBeforeFilters(errorRoute, httpRequest);
}
return errorRoute;
}
RouteMatch findStatusRoute(HttpRequest> incomingRequest, int status, RouteInfo> finalRoute) {
Class> declaringType = finalRoute.getDeclaringType();
// handle re-mapping of errors
RouteMatch statusRoute = null;
// if declaringType is not null, this means it's a locally marked method handler
if (declaringType != null) {
statusRoute = router.findStatusRoute(declaringType, status, incomingRequest)
.orElseGet(() -> router.findStatusRoute(status, incomingRequest).orElse(null));
}
return statusRoute;
}
ExecutorService findExecutor(RouteInfo> routeInfo) {
// Select the most appropriate Executor
ExecutorService executor;
if (routeInfo instanceof MethodReference, ?> methodReference) {
executor = executorSelector.select(methodReference, serverConfiguration.getThreadSelection()).orElse(null);
} else if (routeInfo instanceof MethodBasedRouteInfo, ?> methodBasedRouteInfo) {
executor = executorSelector.select(methodBasedRouteInfo.getTargetMethod().getExecutableMethod(), serverConfiguration.getThreadSelection()).orElse(null);
} else {
executor = null;
}
return executor;
}
private Flux applyExecutorToPublisher(Publisher publisher, @Nullable ExecutorService executor, PropagatedContext propagatedContext) {
if (executor == null) {
return Flux.from(publisher).subscribeOn(Schedulers.fromExecutor(command -> propagatedContext.wrap(command).run()));
}
if (executor instanceof InstrumentedExecutorService instrumentedExecutorService) {
executor = instrumentedExecutorService.getTarget();
}
if (executor instanceof ScheduledExecutorService scheduledExecutorService) {
executor = new InstrumentedScheduledExecutorService() {
@Override
public ScheduledExecutorService getTarget() {
return scheduledExecutorService;
}
@Override
public Callable instrument(Callable task) {
return propagatedContext.wrap(task);
}
@Override
public Runnable instrument(Runnable command) {
return propagatedContext.wrap(command);
}
};
} else {
ExecutorService finalExecutor = executor;
executor = new InstrumentedExecutorService() {
@Override
public ExecutorService getTarget() {
return finalExecutor;
}
@Override
public Callable instrument(Callable task) {
return propagatedContext.wrap(task);
}
@Override
public Runnable instrument(Runnable command) {
return propagatedContext.wrap(command);
}
};
}
final Scheduler scheduler = Schedulers.fromExecutorService(executor);
return Flux.from(publisher)
.subscribeOn(scheduler)
.publishOn(scheduler);
}
private ExecutionFlow> fromImperativeExecute(PropagatedContext propagatedContext,
HttpRequest> request,
RouteInfo> routeInfo,
@Nullable Object body) {
// performance optimization: check for common body types
boolean shortCircuit = body instanceof String || body instanceof byte[];
// this is a bit messy to avoid type pollution performance issues
MutableHttpResponse> outgoingResponse;
if (!shortCircuit && body instanceof MutableHttpResponse> mut) {
outgoingResponse = mut;
} else if (!shortCircuit && body instanceof HttpResponse> httpResponse) {
outgoingResponse = httpResponse.toMutableResponse();
} else {
MutableHttpResponse mutableHttpResponse = forStatus(routeInfo, null);
if (body != null) {
mutableHttpResponse = mutableHttpResponse.body(body);
}
return ExecutionFlow.just(mutableHttpResponse);
}
final Argument> bodyArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
if (bodyArgument.isAsyncOrReactive()) {
return fromPublisher(
processPublisherBody(propagatedContext, request, outgoingResponse, routeInfo)
);
}
return ExecutionFlow.just(outgoingResponse);
}
ExecutionFlow> callRoute(PropagatedContext propagatedContext, RouteMatch> routeMatch, HttpRequest> request) {
RouteInfo> routeInfo = routeMatch.getRouteInfo();
ExecutorService executorService = routeInfo.getExecutor(serverConfiguration.getThreadSelection());
ExecutionFlow> executeMethodResponseFlow;
if (executorService != null) {
if (routeInfo.isSuspended()) {
executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> {
coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(request, contextView, propagatedContext));
return Mono.from(
ReactiveExecutionFlow.fromFlow(executeRouteAndConvertBody(propagatedContext, routeMatch, request)).toPublisher()
);
}));
} else if (routeInfo.isReactive()) {
executeMethodResponseFlow = ReactiveExecutionFlow.async(executorService, () -> executeRouteAndConvertBody(propagatedContext, routeMatch, request));
} else {
executeMethodResponseFlow = ExecutionFlow.async(executorService, () -> executeRouteAndConvertBody(propagatedContext, routeMatch, request));
}
} else {
if (routeInfo.isSuspended()) {
executeMethodResponseFlow = ReactiveExecutionFlow.fromPublisher(Mono.deferContextual(contextView -> {
coroutineHelper.ifPresent(helper -> helper.setupCoroutineContext(request, contextView, propagatedContext));
return Mono.from(
ReactiveExecutionFlow.fromFlow(executeRouteAndConvertBody(propagatedContext, routeMatch, request)).toPublisher()
);
}));
} else if (routeInfo.isReactive()) {
executeMethodResponseFlow = ReactiveExecutionFlow.fromFlow(executeRouteAndConvertBody(propagatedContext, routeMatch, request));
} else {
executeMethodResponseFlow = executeRouteAndConvertBody(propagatedContext, routeMatch, request);
}
}
return executeMethodResponseFlow;
}
private ExecutionFlow> executeRouteAndConvertBody(PropagatedContext propagatedContext, RouteMatch> routeMatch, HttpRequest> httpRequest) {
try (PropagatedContext.Scope ignore = propagatedContext.plus(new ServerHttpRequestContext(httpRequest)).propagate()) {
try {
requestArgumentSatisfier.fulfillArgumentRequirementsAfterFilters(routeMatch, httpRequest);
Object body = routeMatch.execute();
if (body instanceof Optional optional) {
body = optional.orElse(null);
}
return createResponseForBody(propagatedContext, httpRequest, body, routeMatch.getRouteInfo(), routeMatch);
} catch (Throwable e) {
return ExecutionFlow.error(e);
}
}
}
ExecutionFlow> createResponseForBody(PropagatedContext propagatedContext,
HttpRequest> request,
Object body,
RouteInfo> routeInfo,
@Nullable
RouteMatch> routeMatch) {
ExecutionFlow> outgoingResponse;
MutableHttpResponse> response = null;
if (body == null) {
if (routeInfo.isVoid()) {
response = voidResponse(routeInfo);
} else if (serverConfiguration.isNotFoundOnMissingBody()) {
response = notFoundErrorResponse(request);
} else {
response = noContentResponse(routeInfo);
}
} else if (body instanceof String) {
// Micro-optimization for String values
response = forStatus(routeInfo, null).body(body);
} else if (body instanceof HttpStatus httpStatus) {
response = HttpResponse.status(httpStatus);
}
if (response != null) {
return ExecutionFlow.just(finaliseResponse(request, routeInfo, routeMatch, response));
}
if (routeInfo.isImperative()) {
outgoingResponse = fromImperativeExecute(propagatedContext, request, routeInfo, body);
} else {
if (routeInfo.isAsync() && body != null) {
outgoingResponse = CompletableFutureExecutionFlow.just(
fromCompletionStage(request, (CompletionStage) body, routeInfo)
);
} else {
// special case HttpResponse because FullNettyClientHttpResponse implements Completable...
boolean isReactive = routeInfo.isReactive() || (Publishers.isConvertibleToPublisher(body) && !(body instanceof HttpResponse>));
if (isReactive && body != null) {
Publisher publisher = Publishers.convertToPublisher(conversionService, body);
outgoingResponse = ReactiveExecutionFlow.fromPublisher(
ReactivePropagation.propagate(
propagatedContext,
fromReactiveExecute(propagatedContext, request, publisher, routeInfo)
)
);
} else {
if (routeInfo.isSuspended()) {
outgoingResponse = fromKotlinCoroutineExecute(propagatedContext, request, body, routeInfo);
} else {
outgoingResponse = fromImperativeExecute(propagatedContext, request, routeInfo, body);
}
}
}
}
response = outgoingResponse.tryCompleteValue();
if (response != null) {
return ExecutionFlow.just(finaliseResponse(request, routeInfo, routeMatch, response));
}
return outgoingResponse.map(res -> finaliseResponse(request, routeInfo, routeMatch, res));
}
private MutableHttpResponse> finaliseResponse(HttpRequest> request, RouteInfo> routeInfo, RouteMatch> routeMatch, MutableHttpResponse> response) {
// for head request we never emit the body
if (request != null && request.getMethod().equals(HttpMethod.HEAD)) {
final Object o = response.getBody().orElse(null);
if (o instanceof ReferenceCounted referenceCounted) {
referenceCounted.release();
}
response.body(null);
if (o != null) {
RouteAttributes.setHeadBody(response, o);
}
}
applyConfiguredHeaders(response.getHeaders());
if (routeMatch != null) {
RouteAttributes.setRouteMatch(response, routeMatch);
}
RouteAttributes.setRouteInfo(response, routeInfo);
response.bodyWriter((MessageBodyWriter) routeInfo.getMessageBodyWriter());
return response;
}
private ExecutionFlow> fromKotlinCoroutineExecute(PropagatedContext propagatedContext, HttpRequest> request, Object body, RouteInfo> routeInfo) {
boolean isKotlinFunctionReturnTypeUnit =
routeInfo instanceof MethodBasedRouteInfo, ?> mbri &&
isKotlinFunctionReturnTypeUnit(mbri.getTargetMethod().getExecutableMethod());
final Supplier> supplier = ContinuationArgumentBinder.extractContinuationCompletableFutureSupplier(request);
if (isKotlinCoroutineSuspended(body)) {
Mono> responsePublisher = Mono.fromCompletionStage(supplier)
.flatMap(obj -> {
MutableHttpResponse> response;
if (obj instanceof HttpResponse> httpResponse) {
response = httpResponse.toMutableResponse();
final Argument> bodyArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
if (bodyArgument.isAsyncOrReactive()) {
return processPublisherBody(propagatedContext, request, response, routeInfo);
}
} else {
response = forStatus(routeInfo, null);
if (!isKotlinFunctionReturnTypeUnit) {
response = response.body(obj);
}
}
return Mono.just(response);
});
if (serverConfiguration.isNotFoundOnMissingBody()) {
responsePublisher = responsePublisher
.switchIfEmpty(Mono.fromCallable(() -> notFoundErrorResponse(request)));
}
return ReactiveExecutionFlow.fromPublisher(responsePublisher);
}
Object suspendedBody = isKotlinFunctionReturnTypeUnit ? null : body;
return fromImperativeExecute(propagatedContext, request, routeInfo, suspendedBody);
}
private CorePublisher> fromReactiveExecute(PropagatedContext propagatedContext,
HttpRequest> request,
Publisher publisher,
RouteInfo> routeInfo) {
boolean isSingle = routeInfo.isSpecifiedSingle() || routeInfo.isReactive() && routeInfo.isSingleResult() || Publishers.isSingle(publisher.getClass());
boolean isCompletable = !isSingle && routeInfo.isVoid() && routeInfo.isCompletable();
if (isSingle || isCompletable) {
// full response case
return Flux.from(publisher)
.flatMap(o -> {
if (o instanceof Optional> optional) {
if (optional.isPresent()) {
o = optional.get();
} else {
return Mono.empty();
}
}
MutableHttpResponse> singleResponse;
if (o instanceof HttpResponse> httpResponse) {
singleResponse = httpResponse.toMutableResponse();
final Argument> bodyArgument = routeInfo.getReturnType() //Mono
.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT) //HttpResponse
.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); //Mono
if (bodyArgument.isAsyncOrReactive()) {
return processPublisherBody(propagatedContext, request, singleResponse, routeInfo);
}
} else if (o instanceof HttpStatus status) {
singleResponse = forStatus(routeInfo, status);
} else {
singleResponse = forStatus(routeInfo, null)
.body(o);
}
return Flux.just(singleResponse);
})
.switchIfEmpty(Mono.fromSupplier(() -> {
MutableHttpResponse> singleResponse;
if (isCompletable || routeInfo.isVoid()) {
singleResponse = voidResponse(routeInfo);
} else if (serverConfiguration.isNotFoundOnMissingBody()) {
singleResponse = notFoundErrorResponse(request);
} else {
singleResponse = noContentResponse(routeInfo);
}
return singleResponse;
}))
.contextWrite(context -> ReactorPropagation.addPropagatedContext(context, propagatedContext).put(ServerRequestContext.KEY, request));
}
// streaming case
Argument> typeArgument = routeInfo.getReturnType().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
if (HttpResponse.class.isAssignableFrom(typeArgument.getType())) {
// a response stream
Publisher> bodyPublisher = (Publisher) publisher;
Flux> response = Flux.from(bodyPublisher)
.map(HttpResponse::toMutableResponse);
Argument> bodyArgument = typeArgument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
if (bodyArgument.isAsyncOrReactive()) {
return response.flatMap(resp ->
processPublisherBody(propagatedContext, request, resp, routeInfo));
}
return response.contextWrite(context -> ReactorPropagation.addPropagatedContext(context, propagatedContext).put(ServerRequestContext.KEY, request));
}
return processPublisherBody(propagatedContext, request, forStatus(routeInfo, null), false, publisher, routeInfo);
}
private MutableHttpResponse voidResponse(RouteInfo> routeInfo) {
return forStatus(routeInfo, HttpStatus.OK)
.header(HttpHeaders.CONTENT_LENGTH, "0");
}
private MutableHttpResponse noContentResponse(RouteInfo> routeInfo) {
return forStatus(routeInfo, HttpStatus.NO_CONTENT)
.header(HttpHeaders.CONTENT_LENGTH, "0");
}
@NonNull
private CompletionStage> fromCompletionStage(@NonNull HttpRequest> request,
@NonNull CompletionStage completionStage,
@NonNull RouteInfo> routeInfo) {
return completionStage.thenCompose(asyncBody -> {
MutableHttpResponse> mutableResponse;
if (asyncBody instanceof Optional> optional) {
if (optional.isPresent()) {
asyncBody = optional.get();
} else if (serverConfiguration.isNotFoundOnMissingBody()) {
return CompletableFuture.completedStage(notFoundErrorResponse(request));
} else {
return CompletableFuture.completedStage(noContentResponse(routeInfo));
}
}
boolean explicitResponse = false;
if (asyncBody instanceof HttpResponse> httpResponse) {
mutableResponse = httpResponse.toMutableResponse();
final Argument> bodyArgument = routeInfo.getReturnType() // CompletionStage
.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT) // HttpResponse
.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); // CompletionStage
if (bodyArgument.isAsync()) {
CompletionStage inner = (CompletionStage) mutableResponse.body();
return inner.thenApply(innerBody -> {
if (innerBody == null) {
return notFoundErrorResponse(request);
}
return mutableResponse.body(innerBody);
});
}
explicitResponse = true;
} else if (asyncBody instanceof HttpStatus status) {
mutableResponse = forStatus(routeInfo, status);
} else {
mutableResponse = forStatus(routeInfo, null)
.body(asyncBody);
}
if (mutableResponse.body() == null && !explicitResponse) {
if (routeInfo.isVoid()) {
return CompletableFuture.completedStage(voidResponse(routeInfo));
} else if (serverConfiguration.isNotFoundOnMissingBody()) {
return CompletableFuture.completedStage(notFoundErrorResponse(request));
} else {
return CompletableFuture.completedStage(noContentResponse(routeInfo));
}
}
return CompletableFuture.completedStage(mutableResponse);
});
}
private Mono> processPublisherBody(PropagatedContext propagatedContext,
HttpRequest> request,
MutableHttpResponse> response,
RouteInfo> routeInfo) {
Object body = response.body();
if (body == null) {
return Mono.just(response);
}
Publisher bodyPublisher = Publishers.convertToPublisher(conversionService, body);
return processPublisherBody(propagatedContext, request, response, Publishers.isSingle(body.getClass()), bodyPublisher, routeInfo);
}
private Mono> processPublisherBody(PropagatedContext propagatedContext,
HttpRequest> request,
MutableHttpResponse> response,
boolean isSinglePublisher,
Publisher bodyPublisher,
RouteInfo> routeInfo) {
if (isSinglePublisher) {
return Mono.from(bodyPublisher).map(b -> {
response.body(b);
return response;
});
}
MediaType mediaType = response.getContentType().orElseGet(() -> resolveDefaultResponseContentType(request, routeInfo));
bodyPublisher = applyExecutorToPublisher(
bodyPublisher,
findExecutor(routeInfo),
propagatedContext
).contextWrite(cv -> ReactorPropagation.addPropagatedContext(cv, propagatedContext).put(ServerRequestContext.KEY, request));
return Mono.>just(response
.header(HttpHeaders.CONTENT_TYPE, mediaType)
.body(ReactivePropagation.propagate(propagatedContext, bodyPublisher)))
.contextWrite(context -> ReactorPropagation.addPropagatedContext(context, propagatedContext).put(ServerRequestContext.KEY, request));
}
private void applyConfiguredHeaders(MutableHttpHeaders headers) {
if (serverConfiguration.isDateHeader() && !headers.contains(HttpHeaders.DATE)) {
headers.date(LocalDateTime.now());
}
if (headers.get(HttpHeaders.SERVER) == null) {
serverConfiguration.getServerHeader()
.ifPresent(header -> headers.add(HttpHeaders.SERVER, header));
}
}
private MutableHttpResponse forStatus(RouteInfo> routeMatch) {
return forStatus(routeMatch, HttpStatus.OK);
}
private MutableHttpResponse forStatus(RouteInfo> routeMatch, HttpStatus defaultStatus) {
HttpStatus status = routeMatch.findStatus(defaultStatus);
return HttpResponse.status(status);
}
static ExecutionFlow fromPublisher(Publisher publisher) {
return ReactiveExecutionFlow.fromPublisher(publisher);
}
}