컨트롤러에서 에러를 날리면
ErrorReportValve 클래스로
public void invoke(Request request, Response response) throws IOException, ServletException {
this.getNext().invoke(request, response);
if (response.isCommitted()) {
if (response.setErrorReported()) {
AtomicBoolean ioAllowed = new AtomicBoolean(true);
respo
StandardHostValve 클래스에서 invoke 메소드 탐
final class StandardHostValve extends ValveBase {
private static final Log log = LogFactory.getLog(StandardHostValve.class);
private static final StringManager sm = StringManager.getManager(StandardHostValve.class);
private static final ClassLoader MY_CLASSLOADER = StandardHostValve.class.getClassLoader();
StandardHostValve() {
super(true);
}
public void invoke(Request request, Response response) throws IOException, ServletException {
Context context = request.getContext();
if (context == null) {
이 메소드에서
try {
context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
if (asyncAtStart || context.fireRequestInitEvent(request.getRequest())) {
try {
// 지금 response가 오류가 난 상태인지 본다
if (!response.isErrorReportRequired()) {
context.getPipeline().getFirst().invoke(request, response);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
가보면 Response 클래스에서
public boolean isErrorReportRequired() {
return this.getCoyoteResponse().isErrorReportRequired();
}
// 나는 에러를 던져서 errorState.get()값이 0 이다 그래서 false
public boolean isErrorReportRequired() {
return this.errorState.get() == 1;
}
그래서 if문 내부가 실행되고
if (!response.isErrorReportRequired()) {
context.getPipeline().getFirst().invoke(request, response);
}
DispatcherServlet에서 doDispatch 함수
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
그리고 handler 찾아서 컨트롤러 실행
@GetMapping("/exception")
public String exception() {
throw new IllegalArgumentException("IllegalArgumentException error");
}
다시 DispatcherServlet의 doDispatch 로 돌아가서
// 응답을 처리하는 과정에서 예외 발생해서
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception ex) {
// 여기로 이동
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
private static void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {
if (mappedHandler != null) {
// interceptor가 있으면 그쪽에서 예외처리 mappedHandler.triggerAfterCompletion(request, response, ex);
}
// 계속 예외를 던짐
throw ex;
}
FramworkServlet 클래스에서 processRequest 함수
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = this.buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
this.initContextHolders(request, localeContext, requestAttributes);
try {
this.doService(request, response);
} catch (IOException | ServletException ex) {
failureCause = ex;
throw ex;
} catch (Throwable ex) {
failureCause = ex;
// 여기서 예외 받아서 다시 던짐
throw new ServletException("Request processing failed: " + ex, ex);
스프링을 지나서
StandardWrapperValve 클래스로 감
해당 클래스에서 invoke 메소드 타고
private void exception(Request request, Response response, Throwable exception) {
this.exception(request, response, exception, 500);
}
500 상태코드 던짐
Request에 exception 저장
private void exception(Request request, Response response, Throwable exception, int errorCode) {
request.setAttribute("jakarta.servlet.error.exception", exception);
response.setStatus(errorCode);
response.setError();
}
StandardHostValve 클래스에서 오류를 꺼냄
Throwable t = (Throwable)request.getAttribute("jakarta.servlet.error.exception");
// 이번엔 true 다
if (response.isErrorReportRequired()) {
// 예외가 존재하므로 이 함수탐
if (t != null) {
this.throwable(request, response, t);
}
보면 에러페이지 존재
protected void throwable(Request request, Response response, Throwable throwable) {
Context context = request.getContext();
if (context != null) {
Throwable realError = throwable;
if (throwable instanceof ServletException) {
realError = ((ServletException)throwable).getRootCause();
if (realError == null) {
realError = throwable;
}
}
if (realError instanceof ClientAbortException) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardHost.clientAbort", new Object[]{realError.getCause().getMessage()}));
}
} else {
ErrorPage errorPage = context.findErrorPage(throwable);
if (errorPage == null && realError != throwable) {
errorPage = context.findErrorPage(realError);
}
계속 진행
else {
if (response.getStatus() < 400) {
response.setStatus(500);
}
response.setError();
// 예외 정보로는 오류 페이지를 찾지 못해서 예외 상태로 시도
this.status(request, response);
}
private void status(Request request, Response response) {
int statusCode = response.getStatus();
Context context = request.getContext();
if (context != null) {
if (response.isError()) {
ErrorPage errorPage = context.findErrorPage(statusCode);
if (errorPage == null) {
errorPage = context.findErrorPage(0);
없으면 기본 에러페이지를 얻음
if (errorPage == null) {
errorPage = context.findErrorPage(0);
}

if (errorPage != null && response.isErrorReportRequired()) {
// 아직 response 완료되지 않았음을 의미
response.setAppCommitted(false);
그리고
이 메소드로 오류 관련 정보를 전부 저장
setRequestErrorAttributes
톰캣이 준비한 에러페이지도 있지만 커스텀하게 만들었으면 그 페이지 노출하는 함수
if (this.custom(request, response, errorPage)) {
response.setErrorReported();
try {
response.finishResponse();
} catch (ClientAbortException var7) {
} catch (IOException e) {
this.container.getLogger().warn(sm.getString("standardHostValve.exception", new Object[]{errorPage}), e);
}
}
custom 함수를 보면
RequestDispatcher rd = servletContext.getRequestDispatcher(errorPage.getLocation());
// 포워딩하는데 스프링이 받음 (BasicErrorController)
// 스프링에서 view를 못 찾으면 다시 에러 던짐
rd.forward(request.getRequest(), response.getResponse());
ErrorReportValve 클래스로 가서 report 함수 실행
protected void report(Request request, Response response, Throwable throwable) {
int statusCode = response.getStatus();
if (statusCode >= 400 && response.getContentWritten() <= 0L && response.setErrorReported()) {
AtomicBoolean result = new AtomicBoolean(false);
response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
if (result.get()) {
ErrorPage errorPage = this.findErrorPage(statusCode, throwable);
if (errorPage == null || !this.sendErrorPage(errorPage.getLocation(), response)) {
String message = Escape.htmlElementContent(response.getMessage());
if (message == null) {
if (throwable != null) {
String exceptionMessage = throwable.getMessage();
if (exceptionMessage != null && exceptionMessage.length() > 0) {
try (Scanner scanner = new Scanner(exceptionMessage)) {
message = Escape.htmlElementContent(scanner.nextLine());
}
}
}
if (message == null) {
message = "";
}
}
String reason = null;
String description = null;
StringManager smClient = StringManager.getManager("org.apache.catalina.valves", request.getLocales());
response.setLocale(smClient.getLocale());
try {
reason = smClient.getString("http." + statusCode + ".reason");
description = smClient.getString("http." + statusCode + ".desc");
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
if (reason == null || description == null) {
if (message.isEmpty()) {
return;
}
reason = smClient.getString("errorReportValve.unknownReason");
description = smClient.getString("errorReportValve.noDescription");
}
StringBuilder sb = new StringBuilder();
sb.append("<!doctype html><html lang=\"");
sb.append(smClient.getLocale().getLanguage()).append("\">");
sb.append("<head>");
sb.append("<title>");
sb.append(smClient.getString("errorReportValve.s
이건 톰캣이 제공하는 것이고
톰캣 제공하는 화면 보려면
# 이렇게 설정
server.error.whitelabel.enabled=false
스프링에서 제공하는것은
ErrorMvcAutoConfiguration 클래스에서 render 메소드를 탄다
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (response.isCommitted()) {
String message = this.getMessage(model);
logger.error(message);
} else {
response.setContentType(TEXT_HTML_UTF8.toString())