Speedy provides a built-in DefaultExceptionMapper that translates exceptions to HTTP status codes. You can override
default mappings or add custom mappings using @SpeedyControllerAdvice and @SpeedyExceptionHandler annotations.
When an exception is thrown during request processing, Speedy resolves the HTTP status in this order:
@SpeedyExceptionHandler methods (highest priority)SpeedyHttpException hierarchy — maps BadRequestException (400), NotFoundException (404),
PreconditionFailedException (412, see Conditional Requests / ETags),
InternalServerError (500)ConstraintViolationException / DataException → 400JsonProcessingException → 400IllegalArgumentException → 400"Internal Server Error".Create a class annotated with @SpeedyControllerAdvice. Add methods annotated with @SpeedyExceptionHandler:
import com.github.silent.samurai.speedy.annotations.SpeedyControllerAdvice;
import com.github.silent.samurai.speedy.annotations.SpeedyExceptionHandler;
import org.springframework.stereotype.Component;
@Component
@SpeedyControllerAdvice
public class MyExceptionAdvice {
@SpeedyExceptionHandler(value = IllegalArgumentException.class, status = 400)
public String handleIllegalArgument(IllegalArgumentException e) {
return "Invalid argument: " + e.getMessage();
}
@SpeedyExceptionHandler(value = RuntimeException.class, status = 500)
public String handleRuntime() {
return "Unexpected error occurred";
}
}
Handler methods can optionally accept the thrown exception as a parameter. If the parameter is present, use it to craft a dynamic message.
Register the advice bean in your ISpeedyConfiguration.register() method:
@Configuration
public class SpeedyConfig implements ISpeedyConfiguration {
@Autowired
MyExceptionAdvice myExceptionAdvice;
// ...
@Override
public void register(ISpeedyRegistry registry) {
registry.registerControllerAdvice(myExceptionAdvice);
registry.registerEventHandler(entityEvents);
registry.registerValidator(speedyValidation);
}
}
Custom exception handlers resolve matches using these rules:
| Rule | Description |
|---|---|
| Exact match | @SpeedyExceptionHandler(value = NotFoundException.class) matches exactly NotFoundException |
| Superclass hierarchy | If no exact match, Speedy walks up the superclass chain. A handler for RuntimeException catches all runtime subclasses. |
| Cause chain | If the top-level exception has no match, Speedy walks the getCause() chain. A handler for the root cause will be used. |
Example:
@SpeedyControllerAdvice
public class GlobalAdvice {
@SpeedyExceptionHandler(value = RuntimeException.class, status = 500)
public String handleRuntime(RuntimeException e) {
return "Server error: " + e.getMessage();
}
@SpeedyExceptionHandler(value = IllegalArgumentException.class, status = 400)
public String handleIllegalArg(IllegalArgumentException e) {
return "Bad request: " + e.getMessage();
}
@SpeedyExceptionHandler(value = NotFoundException.class, status = 404)
public String handleNotFound(NotFoundException e) {
return "Entity not found: " + e.getMessage();
}
}
// Matches IllegalArgumentException → 400
throw new IllegalArgumentException("field email is invalid");
// CustomException extends RuntimeException → matches via superclass → 500
throw new CustomException("something went wrong");
// Wrapped exception → walks cause chain to IllegalArgumentException → 400
Exception e = new Exception("wrapper", new IllegalArgumentException("root cause"));
All exceptions produce a JSON error response:
{
"status": 400,
"message": "Bad request: field email is invalid",
"timestamp": "2026-05-28T13:45:00"
}
Messages for 5xx responses are always masked:
{
"status": 500,
"message": "Internal Server Error",
"timestamp": "2026-05-28T13:45:00"
}