Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow concurrent Exception to be unwrapped for the ExceptionMapper #4525

Merged
merged 1 commit into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2020 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
Expand Down Expand Up @@ -28,7 +28,9 @@
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -489,14 +491,12 @@ private ContainerResponse convertResponse(final Response exceptionResponse) {
private Response mapException(final Throwable originalThrowable) throws Throwable {
LOGGER.log(Level.FINER, LocalizationMessages.EXCEPTION_MAPPING_START(), originalThrowable);

Throwable throwable = originalThrowable;
boolean inMappable = false;
boolean mappingNotFound = false;
final ThrowableWrap wrap = new ThrowableWrap(originalThrowable);
wrap.tryMappableException();

do {
if (throwable instanceof MappableException) {
inMappable = true;
} else if (inMappable || throwable instanceof WebApplicationException) {
final Throwable throwable = wrap.getCurrent();
if (wrap.isInMappable() || throwable instanceof WebApplicationException) {
// in case ServerProperties.PROCESSING_RESPONSE_ERRORS_ENABLED is true, allow
// wrapped MessageBodyProviderNotFoundException to propagate
if (runtime.processResponseErrors && throwable instanceof InternalServerErrorException
Expand Down Expand Up @@ -568,8 +568,6 @@ private Response mapException(final Throwable originalThrowable) throws Throwabl

return waeResponse;
}

mappingNotFound = true;
}
// internal mapping
if (throwable instanceof HeaderValueException) {
Expand All @@ -578,18 +576,17 @@ private Response mapException(final Throwable originalThrowable) throws Throwabl
}
}

if (!inMappable || mappingNotFound) {
if (!wrap.isInMappable() || !wrap.isWrapped()) {
// user failures (thrown from Resource methods or provider methods)

// spec: Unchecked exceptions and errors that have not been mapped MUST be re-thrown and allowed to
// propagate to the underlying container.

// not logged on this level.
throw throwable;
throw wrap.getWrappedOrCurrent();
}

throwable = throwable.getCause();
} while (throwable != null);
} while (wrap.unwrap() != null);
// jersey failures (not thrown from Resource methods or provider methods) -> rethrow
throw originalThrowable;
}
Expand Down Expand Up @@ -1181,4 +1178,91 @@ public void invoke(final ConnectionCallback callback) {
});
}
}

/**
* The structure that holds original {@link Throwable}, top most wrapped {@link Throwable} for the cases where the
* exception is to be tried to be mapped but is wrapped in a known wrapping {@link Throwable}, and the current unwrapped
* {@link Throwable}. For instance, the original is {@link MappableException}, the wrapped is {@link CompletionException},
* and the current is {@code IllegalStateException}.
*/
private static class ThrowableWrap {
private final Throwable original;
private Throwable wrapped = null;
private Throwable current;
private boolean inMappable = false;

private ThrowableWrap(Throwable original) {
this.original = original;
this.current = original;
}

/**
* Gets the original {@link Throwable} to be mapped to an {@link ExceptionMapper}.
* @return the original Throwable.
*/
private Throwable getOriginal() {
return original;
}

/**
* Some exceptions can be unwrapped. If an {@link ExceptionMapper} is not found for them, the original wrapping
* {@link Throwable} is to be returned. If the exception was not wrapped, return current.
* @return the wrapped or current {@link Throwable}.
*/
private Throwable getWrappedOrCurrent() {
return wrapped != null ? wrapped : current;
}

/**
* Get current unwrapped {@link Throwable}.
* @return current {@link Throwable}.
*/
private Throwable getCurrent() {
return current;
}

/**
* Check whether the current is a known wrapping exception.
* @return true if the current is a known wrapping exception.
*/
private boolean isWrapped() {
final boolean isConcurrentWrap =
CompletionException.class.isInstance(current) || ExecutionException.class.isInstance(current);

return isConcurrentWrap;
}

/**
* Store the top most wrap exception and return the cause.
* @return the cause of the current {@link Throwable}.
*/
private Throwable unwrap() {
if (wrapped == null) {
wrapped = current;
}
current = current.getCause();
return current;
}

/**
* Set flag that the original {@link Throwable} is {@link MappableException} and unwrap the nested {@link Throwable}.
* @return true if the original {@link Throwable} is {@link MappableException}.
*/
private boolean tryMappableException() {
if (MappableException.class.isInstance(original)) {
inMappable = true;
current = original.getCause();
return true;
}
return false;
}

/**
* Return the flag that original {@link Throwable} is {@link MappableException}.
* @return true if the original {@link Throwable} is {@link MappableException}.
*/
private boolean isInMappable() {
return inMappable;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2017, 2020 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
Expand Down Expand Up @@ -127,11 +127,29 @@ public void testGetCustomAsync() {
assertThat(response.readEntity(String.class), is(ENTITY));
}

@Test
public void test4463() {
Response response = target("cs/exceptionally").request().get();

assertThat(response.getStatus(), is(406));
}

@Path("/cs")
public static class CompletionStageResource {

private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();

@GET
@Path("exceptionally")
public CompletionStage<String> failAsyncLater() {
CompletableFuture<String> fail = new CompletableFuture<>();
fail.completeExceptionally(new IllegalStateException("Uh-oh"));

return fail.exceptionally(ex -> {
throw new WebApplicationException("OOPS", Response.Status.NOT_ACCEPTABLE.getStatusCode());
});
}

@GET
@Path("/completed")
public CompletionStage<String> getCompleted() {
Expand Down