Skip to content

After-completion callback not triggered for custom Throwable subclass [SPR-14329] #18901

Closed
@spring-projects-issues

Description

@spring-projects-issues

Ben Heilers opened SPR-14329 and commented

Our team observed this, although I admit it is probably a rare situation.

To summarize, the JDBC connection is never released if:

  • an exception is thrown in the controller method
  • a second exception is thrown during handling of the first exception
  • attempting to log that second exception causes a third exception to be thrown
  • the third exception directly extends Throwable, not Exception or Error

I reproduced it with just small changes to the project at https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples/spring-boot-sample-data-jpa.

First, I changed the line in the controller method from "Bath" to "Foo", below:

@GetMapping("/")
@ResponseBody
@Transactional(readOnly = true)
public String helloWorld() {
    return this.cityService.getCity("Foo", "UK").getName();
}

This causes getCity() to return null, so the method throws an NPE.

So far, everything is okay, the exception is caught here, and the JDBC connection is still released within the call to triggerAfterCompletion().

But we really wanted a custom response HTTP status of 400 instead of 500, so we added:

@ResponseStatus(value=HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public void handleException(Exception ex) {
   //TODO: some more handling logic to come later
}

Now the status is 500, but we still wanted some special logic in that exception handler for adding a log message.

Eventually, we observed now that in some scenarios that handling logic ends up throwing an second exception while trying to handle the exception thrown in the controller method.

This isn't so bad, because secondary exceptions thrown during exception handling are caught and handled here.

But we observed that the second exception is a custom class, that had an override of some of the methods in Throwable ... and those overrides in turn threw a third exception.

This also wouldn't be so bad by itself, because there is logic here to still end up unreleasing the JDBC connection in that scenario, as long as the exception sublcasses either Exception or Error.

Unfortunately, we found in our case this custom exception class was directly extending Throwable.

This means the JDBC connection is not released.

The below is a bit contrived, but it proves the point:

	@ResponseStatus(value=HttpStatus.BAD_REQUEST)
	@ExceptionHandler(Exception.class)
	public void handleException(Exception ex) {
		throwUnchecked(new MyException());
	}

	public static class MyException extends Throwable {
		@Override
		public String getMessage() {
			SampleController.throwUnchecked(this);
			return "";
		}
	}

	public static void throwUnchecked(Throwable e) {
		SampleController.<RuntimeException>throwAny(e);
	}

	@SuppressWarnings("unchecked")
	private static <E extends Throwable> void throwAny(Throwable e) throws E {
		throw (E) e;
	}

While I admit this is probably a rare edge case, this is actually a security vulnerability, since unreleased connections can be used as an attack vector. And there is nothing inapprorpiate about subclassing Throwable, or overriding methods like getMessage() to do custom logic there.

NOTE: if you are on spring 4.3, the logging level is debug, where it used to be error, so this whole issue is only reproducible if you also add the following to resources/application.properties:

logging.level.org.springframework.web.servlet.mvc.method.annotation=debug

Affects: 4.2.6, 4.3 RC2

Issue Links:

Backported to: 4.2.7

Metadata

Metadata

Assignees

Labels

status: backportedAn issue that has been backported to maintenance branchestype: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions