← Назад к вопросам

Как скрыть вывод ошибки пользователю

2.0 Middle🔥 121 комментариев
#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Скрытие ошибок пользователю: Best Practices

Показывать полные stack trace и внутренние ошибки пользователям — критическая ошибка безопасности и плохого UX. Это может выдать информацию о архитектуре приложения, версиях библиотек и создать дыры для атак. Правильное скрытие ошибок требует многоуровневого подхода.

Основной принцип

Правило 1: Пользователь видит дружественное сообщение Правило 2: Разработчик видит полную информацию в логах Правило 3: Never expose stack traces to clients

Решение 1: Global Exception Handler в Spring

Используй @ControllerAdvice для централизованной обработки ошибок:

// Пользовательские исключения
public class UserFacingException extends RuntimeException {
    private String userMessage;
    private int httpStatus;
    
    public UserFacingException(String userMessage, int httpStatus) {
        this.userMessage = userMessage;
        this.httpStatus = httpStatus;
    }
    
    public String getUserMessage() {
        return userMessage;
    }
    
    public int getHttpStatus() {
        return httpStatus;
    }
}

public class InternalException extends RuntimeException {
    // Разработчикам видна полная информация
    // Пользователям видно только дружественное сообщение
}

// Global Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    // 1. Обработка expected ошибок
    @ExceptionHandler(UserFacingException.class)
    public ResponseEntity<ErrorResponse> handleUserFacingException(
            UserFacingException ex) {
        return ResponseEntity
            .status(ex.getHttpStatus())
            .body(new ErrorResponse(
                ex.getUserMessage(),
                null  // Без stack trace для пользователя!
            ));
    }
    
    // 2. Обработка validation ошибок
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        String userMessage = "Ошибка валидации. Проверьте входные данные.";
        
        // Логируем полную информацию
        logger.warn("Validation error: {}", ex.getBindingResult().getAllErrors());
        
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse(
                userMessage,
                null
            ));
    }
    
    // 3. Обработка неожиданных ошибок
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
            Exception ex) {
        String errorId = UUID.randomUUID().toString();
        
        // ВСЕГДА логируй полный stack trace для разработчиков
        logger.error("Unexpected error [{}]", errorId, ex);
        
        // Пользователю показываем дружественное сообщение
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse(
                "Произошла ошибка на сервере. Попробуйте позже.",
                errorId  // Для связи с логами
            ));
    }
}

// DTO для ответа
public class ErrorResponse {
    private String message;
    private String errorId;  // Для трейсинга в логах
    
    public ErrorResponse(String message, String errorId) {
        this.message = message;
        this.errorId = errorId;
    }
    
    public String getMessage() { return message; }
    public String getErrorId() { return errorId; }
}

Решение 2: Разделение типов исключений

// Исключения, которые показываем пользователю
public class ValidationException extends Exception {
    public ValidationException(String message) {
        super(message);  // Пользователь видит это
    }
}

public class ResourceNotFoundException extends Exception {
    public ResourceNotFoundException(String resourceType, String id) {
        super("" + resourceType + " с ID " + id + " не найден");
    }
}

// Исключения, которые скрываем
public class DatabaseException extends Exception {
    // Пользователь НЕ должен видеть детали БД
}

public class ExternalApiException extends Exception {
    // Пользователь НЕ должен видеть детали внешних сервисов
}

// Обработка
@RestController
public class UserController {
    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable String id) {
        try {
            return ResponseEntity.ok(userService.findById(id));
        } catch (ValidationException ex) {
            // Показываем пользователю
            return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(new ErrorResponse(ex.getMessage(), null));
        } catch (ResourceNotFoundException ex) {
            // Показываем пользователю
            return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse(ex.getMessage(), null));
        } catch (DatabaseException ex) {
            // Логируем, но НЕ показываем
            logger.error("Database error:", ex);
            return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse(
                    "Произошла ошибка. Попробуйте позже.",
                    null
                ));
        }
    }
}

Решение 3: Логирование с разными уровнями

@Service
public class UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
    
    public User findById(String id) {
        try {
            // Логика
            validateId(id);
            return userRepository.findById(id)
                .orElseThrow(() -> 
                    new ResourceNotFoundException("User", id)
                );
        } catch (DatabaseException ex) {
            // ERROR: с полным stack trace для разработчиков
            logger.error("Failed to fetch user from database for id: {}", id, ex);
            
            // Но выбрасываем дружественное исключение
            throw new InternalException("Database error");
        } catch (ValidationException ex) {
            // WARN: это expected case
            logger.warn("Invalid user ID: {}", id);
            throw ex;
        }
    }
    
    private void validateId(String id) {
        if (id == null || id.isEmpty()) {
            throw new ValidationException("ID не может быть пустым");
        }
    }
}

Решение 4: Чувствительная информация в логах

@Service
public class PaymentService {
    private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
    
    public void processPayment(PaymentRequest request) {
        try {
            // НЕ логируй полные данные карты!
            String maskedCard = maskCardNumber(request.getCardNumber());
            logger.info("Processing payment for card: {}", maskedCard);
            
            // Обработка платежа
            externalPaymentGateway.charge(request);
        } catch (PaymentGatewayException ex) {
            // Логируем для разработчиков, но маскируем чувствительное
            logger.error(
                "Payment failed for card: {}, error code: {}",
                maskCardNumber(request.getCardNumber()),
                ex.getErrorCode(),
                ex  // Полный stack trace
            );
            
            // Пользователю показываем дружественное сообщение
            throw new UserFacingException(
                "Платёж не прошёл. Проверьте данные карты.",
                400
            );
        }
    }
    
    private String maskCardNumber(String cardNumber) {
        // Показываем только последние 4 цифры
        return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
    }
}

Решение 5: Environment-specific error responses

@Configuration
public class ErrorHandlingConfig {
    @Value("${app.debug-mode:false}")
    private boolean debugMode;
    
    @Bean
    public ErrorResponseFactory errorResponseFactory() {
        return new ErrorResponseFactory(debugMode);
    }
}

public class ErrorResponseFactory {
    private boolean debugMode;
    
    public ErrorResponseFactory(boolean debugMode) {
        this.debugMode = debugMode;
    }
    
    public ErrorResponse createErrorResponse(
            Exception ex,
            String userFriendlyMessage) {
        if (debugMode) {
            // DEV: показываем всё
            return new ErrorResponse(
                ex.getMessage(),
                ex.getStackTrace().toString()
            );
        } else {
            // PROD: скрываем детали
            return new ErrorResponse(
                userFriendlyMessage,
                null
            );
        }
    }
}

// application-prod.yml
app:
  debug-mode: false

// application-dev.yml
app:
  debug-mode: true

Решение 6: Security headers для минимизации информации

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Скрывай информацию о сервере
            .headers(h -> h
                .contentSecurityPolicy("default-src self")
                .xssProtection()
                .frameOptions()
            )
            // Не показывай подробности аутентификации
            .authenticationEntryPoint((request, response, authException) -> {
                response.setStatus(401);
                response.setContentType("application/json");
                response.getWriter().write("{\"error\": \"Unauthorized\"}");
            });
        
        return http.build();
    }
}

Чеклист безопасности

  • Никогда не выводи stack trace в ответе пользователю
  • Всегда логируй полную информацию для разработчиков
  • Используй errorId для связи UI логов и server логов
  • Не выводи информацию о версиях библиотек
  • Маскируй чувствительные данные (карты, пароли, токены)
  • Используй разные сообщения для разных типов ошибок
  • В PROD отключай debug mode
  • Регулярно проверяй логи на утечку информации
  • Используй централизованный Exception Handler
  • Валидируй входные данные и показывай понятные ошибки

Вывод

Скрытие ошибок от пользователя — это баланс между:

  • Безопасностью: не выдавать архитектурные детали
  • UX: показывать понятные сообщения
  • Дебаггингом: сохранять полную информацию в логах

Используй GlobalExceptionHandler + разумное логирование и твоё приложение будет безопаснее и удобнее.

Как скрыть вывод ошибки пользователю | PrepBro