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

Suggested change to fix logging of handled exceptions #166

Closed
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
Expand Up @@ -2,24 +2,20 @@

import com.bugsnag.HandledState.SeverityReasonType;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import java.util.Collections;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Reports uncaught exceptions thrown from handler mapping or execution to Bugsnag
* and then passes the exception to the next handler in the chain.
*
* <p>
* Set to highest precedence so that it should be called before other exception
* resolvers.
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
class BugsnagMvcExceptionHandler implements HandlerExceptionResolver {

private final Bugsnag bugsnag;
Expand Down Expand Up @@ -48,3 +44,4 @@ public ModelAndView resolveException(HttpServletRequest request,
return null;
}
}

34 changes: 23 additions & 11 deletions bugsnag-spring/src/main/java/com/bugsnag/MvcConfiguration.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,48 @@
package com.bugsnag;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;
import javax.annotation.PostConstruct;

/**
* If spring-webmvc is loaded, add configuration for reporting unhandled exceptions.
*/
@Configuration
@Conditional(SpringWebMvcLoadedCondition.class)
class MvcConfiguration {
class MvcConfiguration extends WebMvcConfigurerAdapter {

@Autowired
private Bugsnag bugsnag;

/**
* Register an exception resolver to send unhandled reports to Bugsnag
* for uncaught exceptions thrown from request handlers.
*/
@Bean
BugsnagMvcExceptionHandler bugsnagHandlerExceptionResolver() {
return new BugsnagMvcExceptionHandler(bugsnag);
}

/**
* Add a callback to assign specified severities for some Spring exceptions.
*/
@PostConstruct
void addExceptionClassCallback() {
bugsnag.addCallback(new ExceptionClassCallback());
}

/**
* Normally, the exceptionResolvers contain the following resolvers in this order:
* - {@link org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver}
* - {@link org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver}
* - {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver}
* <p>
* The first two handlers handle exceptions in an application-specific manner.
* (either by @{@link org.springframework.web.bind.annotation.ExceptionHandler}
* or @{@link org.springframework.web.bind.annotation.ResponseStatus})
* <p>
* Therefore, exceptions that are handled by these handlers should not be handled by Bugsnag.
* Only unhandled exceptions shall be sent to Bugsnag.
*/
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
final int position = exceptionResolvers.isEmpty() ? 0 : exceptionResolvers.size() - 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell the default exception resolvers are populated here.

My concern with this approach is that we don't have any control over the contents of this list. If the order or content of the default exception handlers is changed in the spring libraries then it could affect the way that the Bugsnag library works. If it does then we might end up in the situation of trying to support multiple versions of this mechanism as we can't guarantee what versions of the spring framework users are using.

Although there are problems with the current implementation, I believe that it is more resilient to change in the framework, and it's still possible to exclude unwanted exceptions using the beforeNotify callback, or by specifying "Discard by error class" in the project settings.

exceptionResolvers.add(position, new BugsnagMvcExceptionHandler(bugsnag));
}
}
24 changes: 24 additions & 0 deletions bugsnag-spring/src/test/java/com/bugsnag/SpringMvcTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,20 @@ public void springVersionSetCorrectly() {
assertEquals(SpringBootVersion.getVersion(), runtimeVersions.get("springBoot"));
}

@Test
public void noBugsnagNotifyOnResponseStatusException() {
callResponseStatusExceptionEndpoint();

verifyNoReport();
}

@Test
public void noBugsnagNotifyOnExceptionHandledByExceptionHandlerException() {
callResponseStatusExceptionEndpoint();

verifyNoReport();
}

@Test
public void unhandledTypeMismatchExceptionSeverityInfo() {
callUnhandledTypeMismatchExceptionEndpoint();
Expand Down Expand Up @@ -242,6 +256,16 @@ private void callUnhandledTypeMismatchExceptionEndpoint() {
"/throw-type-mismatch-exception", String.class);
}

private void callResponseStatusExceptionEndpoint() {
this.restTemplate.getForEntity(
"/throw-response-status-exception", String.class);
}

private void callCustomExceptionEndpoint() {
this.restTemplate.getForEntity(
"/throw-custom-exception", String.class);
}

private void callHandledTypeMismatchExceptionUserSeverityEndpoint() {
this.restTemplate.getForEntity(
"/handled-type-mismatch-exception-user-severity", String.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ public void throwTypeMismatchException() {
throw new TypeMismatchException("Test", String.class);
}

/**
* Throw an exception with @ResponseStatus
*/
@RequestMapping("/throw-response-status-exception")
public void throwResponseStatusException() {
throw new TestResponseStatusException();
}

/**
* Throw an exception that is handled by @ExceptionHandler
*/
@RequestMapping("/throw-custom-exception")
public void throwCustomException() {
throw new TestCustomException();
}

/**
* Report a handled exception where the severity reason is exceptionClass
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.bugsnag.testapp.springboot;

public class TestCustomException extends RuntimeException {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.bugsnag.testapp.springboot;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class TestExceptionHandler {
@ExceptionHandler(TestCustomException.class)
public ResponseEntity handleTestCustomException(TestCustomException ignored) {
return ResponseEntity.ok(TestCustomException.class.getSimpleName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.bugsnag.testapp.springboot;

import static org.springframework.http.HttpStatus.I_AM_A_TEAPOT;

import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(I_AM_A_TEAPOT)
public class TestResponseStatusException extends RuntimeException {
}