• 29.08.2026 09:07:04
  • Admin Admin

Spring REST API hata yanıtlarını RFC 9457 ProblemDetail ile kararlı, güvenli ve test edilebilir hale getirin. Spring MVC, Spring Security ve gateway katmanlarında hata türlerinin nasıl korunacağını uygulamalı olarak inceleyin.

Spring REST API'de ProblemDetail ile Sürümlemeli Hata Sözleşmesi

Spring REST API için RFC 9457 tabanlı hata sözleşmesi

Bir spring rest api için HTTP durum kodu tek başına istemcinin karar vermesine yetmez. İstemcinin programatik olarak ayırt edebileceği kararlı bir type URI'si, tekrar deneme kararında kullanılacak code alanı ve istek bağlamı için correlationId tanımlayın. RFC 9457'nin ProblemDetail modeli, Spring Framework içinde doğrudan desteklenir; ancak varsayılan hata gövdesini istemci sözleşmesi kabul etmek risklidir. Alan adlarını, type URI'lerini ve hangi hataların dışarı sızacağını uygulama seviyesinde sabitleyin.

@RestControllerAdvice
class ApiExceptionHandler {

  @ExceptionHandler(MethodArgumentNotValidException.class)
  ResponseEntity<ProblemDetail> validation(
      MethodArgumentNotValidException ex,
      HttpServletRequest request) {

    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.UNPROCESSABLE_ENTITY,
        "Request validation failed");

    problem.setType(URI.create("https://api.example.com/problems/validation"));
    problem.setTitle("Validation error");
    problem.setInstance(URI.create(request.getRequestURI()));
    problem.setProperty("code", "VALIDATION_FAILED");
    problem.setProperty("fields", ex.getBindingResult().getFieldErrors().stream()
        .map(error -> Map.of(
            "name", error.getField(),
            "code", error.getCode(),
            "rejectedValue", String.valueOf(error.getRejectedValue())))
        .toList());

    return ResponseEntity.unprocessableEntity()
        .contentType(MediaType.APPLICATION_PROBLEM_JSON)
        .body(problem);
  }
}

Buradaki önemli sınır, Java exception sınıfını dış sözleşmeye çevirmemektir. Örneğin Hibernate ConstraintViolationException, JDBC sürücüsü değiştiğinde veya persistence katmanı yeniden düzenlendiğinde değişebilir; buna karşılık VALIDATION_FAILED istemci için kalıcı bir uygulama kodudur. rejectedValue alanını sadece güvenli tipler için üretin: parola, erişim belirteci veya büyük bir JSON gövdesi bu alana taşınmamalıdır. Bu kontrolü merkezi hale getirmek için alan tipine göre maskeleme yapan bir FieldErrorMapper yazın.

Spring MVC doğrulaması ve hata tiplerinin doğru sınıflandırılması

spring mvc katmanında yanlış sınıflandırılan hatalar, istemcinin hatalı retry davranışına yol açar. @RequestBody doğrulama hatası MethodArgumentNotValidException iken, path variable veya request parameter üzerindeki doğrulama ConstraintViolationException üretir. JSON ayrıştırma hatası ise çoğunlukla HttpMessageNotReadableException olarak gelir. Bu üç durumu aynı VALIDATION_FAILED koduna toplamak mümkündür, fakat type URI'lerini validation, malformed-json ve invalid-parameter olarak ayırmak, mobil istemcinin kullanıcıya göstereceği mesajı doğru seçmesini sağlar.

@RestController
@RequestMapping("/v1/transfers")
class TransferController {

  @PostMapping
  ResponseEntity<TransferResponse> create(
      @Valid @RequestBody CreateTransferRequest request) {
    return ResponseEntity.status(HttpStatus.CREATED).body(service.create(request));
  }
}

record CreateTransferRequest(
    @NotBlank @Size(max = 36) String clientRequestId,
    @NotNull @Positive BigDecimal amount,
    @NotBlank @Pattern(regexp = "[A-Z]{3}") String currency) {
}

İstemci sözleşmesini sıkı tutmak istiyorsanız bilinmeyen JSON alanlarını CI ortamında görünür hata yapın. Aksi halde "amunt" gibi bir yazım hatası Jackson tarafından yoksayılabilir ve iş akışı sıfır veya null varsayımlarıyla devam edebilir. Uygulama yapılandırmasına aşağıdaki ayarı ekleyin; bu değişiklik geriye uyumluluk kararı gerektirdiği için önce tüketicilerin contract testlerini çalıştırın.

spring:
  jackson:
    deserialization:
      fail-on-unknown-properties: true

İncelik şudur: field error içindeki rejectedValue değerini doğrudan String.valueOf ile loglamak, byte dizileri veya iç içe DTO'larda hem gereksiz tahsis hem de veri sızıntısı oluşturur. En fazla 128 karakterlik, allowlist tabanlı bir temsil kullanın ve sadece String, Number, Boolean gibi skaler tipleri hata cevabına alın. Nesne ve koleksiyonlar için rejectedValue yerine "omitted" yazmak daha güvenli bir sözleşmedir.

Spring Security filtre zincirinde ProblemDetail üretmek

spring security kaynaklı 401 ve 403 hataları çoğu zaman @RestControllerAdvice tarafından yakalanmaz. AuthenticationEntryPoint ve AccessDeniedHandler, DispatcherServlet'e ulaşmadan önce SecurityFilterChain içinde çalışır. Bu nedenle API'nin geri kalanında application/problem+json dönüp kimlik doğrulama hatalarında HTML veya boş gövde dönmesi gibi tutarsızlıkları, handler'ları doğrudan filtre zincirine bağlayarak engelleyin.

@Bean
SecurityFilterChain apiSecurity(HttpSecurity http, ObjectMapper objectMapper)
    throws Exception {

  AuthenticationEntryPoint entryPoint = (request, response, ex) -> {
    ProblemDetail body = ProblemDetail.forStatus(HttpStatus.UNAUTHORIZED);
    body.setType(URI.create("https://api.example.com/problems/unauthenticated"));
    body.setTitle("Authentication required");
    body.setProperty("code", "UNAUTHENTICATED");
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
    objectMapper.writeValue(response.getOutputStream(), body);
  };

  AccessDeniedHandler deniedHandler = (request, response, ex) -> {
    ProblemDetail body = ProblemDetail.forStatus(HttpStatus.FORBIDDEN);
    body.setType(URI.create("https://api.example.com/problems/forbidden"));
    body.setTitle("Permission denied");
    body.setProperty("code", "FORBIDDEN");
    response.setStatus(HttpStatus.FORBIDDEN.value());
    response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
    objectMapper.writeValue(response.getOutputStream(), body);
  };

  return http
      .csrf(csrf -> csrf.disable())
      .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
      .exceptionHandling(errors -> errors
          .authenticationEntryPoint(entryPoint)
          .accessDeniedHandler(deniedHandler))
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
      .build();
}

CSRF'yi yalnızca bearer token ile çalışan stateless API zincirinde devre dışı bırakın. Aynı uygulamada cookie tabanlı tarayıcı oturumu varsa ayrı bir SecurityFilterChain ve requestMatcher kullanın; aksi halde cookie ile kimlik doğrulanmış yazma endpointleri cross-site request riskine açılır. Ayrıca 403 cevabında gerekli rol, kaynak sahibi veya policy sonucu gibi ayrıntıları dönmeyin: bu bilgiler yetkisiz kullanıcıya kaynak keşfi sağlar.

Microservices mimarisi ve Spring Cloud Gateway'de hata bağlamı

microservices mimarisi içinde gateway'in görevi downstream servislerin domain hata type URI'lerini yeniden yazmak değildir. Gateway sadece ağ seviyesindeki 502, 503 ve timeout gibi kendi ürettiği hatalara gateway-unavailable veya upstream-timeout type'ı vermelidir. spring cloud Gateway üzerinden geçen her isteğe correlation ID eklemek ise istemci hatası ile downstream log kaydını birleştirmenin somut yoludur.

@Component
class CorrelationIdFilter implements GlobalFilter, Ordered {

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    String correlationId = Optional.ofNullable(
        exchange.getRequest().getHeaders().getFirst("X-Correlation-Id"))
        .filter(value -> value.matches("[a-zA-Z0-9-]{8,64}"))
        .orElseGet(() -> UUID.randomUUID().toString());

    ServerWebExchange mutated = exchange.mutate()
        .request(request -> request.headers(headers ->
            headers.set("X-Correlation-Id", correlationId)))
        .build();

    mutated.getResponse().getHeaders().set("X-Correlation-Id", correlationId);
    return chain.filter(mutated);
  }

  @Override
  public int getOrder() {
    return Ordered.HIGHEST_PRECEDENCE;
  }
}

İstemciden gelen correlation ID'yi doğrulamadan kabul etmek yaygın bir operatör hatasıdır. Çok uzun bir değer log satırlarını bozabilir, kontrol karakteri içerebilir veya log tabanlı sorguları kirletebilir. Örnekteki regex hem uzunluğu sınırlar hem de kabul edilen karakter kümesini daraltır. Downstream Spring Boot servisinde aynı header'ı MDC'ye almak için OncePerRequestFilter kullanın ve log pattern'ine %X{correlationId} ekleyin; gateway ile servis logları bu alan üzerinden sorgulanabilir.

Spring Boot eğitimi için sözleşme testi ve geriye uyumluluk kontrolü

Bir spring boot eğitimi veya java spring eğitimi kapsamında hata modeli anlatılırken en değerli pratik, ProblemDetail gövdesini MockMvc ile alan alan doğrulamaktır. Sadece status().isUnprocessableEntity() kontrolü type URI'sinin veya code alanının yanlışlıkla değişmesini yakalayamaz. Aşağıdaki test, istemcinin bağımlı olduğu üç sözleşme alanını açıkça kilitler.

@WebMvcTest(TransferController.class)
@Import(ApiExceptionHandler.class)
class TransferControllerTest {

  @Autowired MockMvc mvc;

  @Test
  void invalidAmountReturnsStableProblemContract() throws Exception {
    mvc.perform(post("/v1/transfers")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"clientRequestId\":\"c-1\",\"amount\":0,\"currency\":\"TRY\"}"))
        .andExpect(status().isUnprocessableEntity())
        .andExpect(content().contentTypeCompatibleWith(
            MediaType.APPLICATION_PROBLEM_JSON))
        .andExpect(jsonPath("$.type").value(
            "https://api.example.com/problems/validation"))
        .andExpect(jsonPath("$.code").value("VALIDATION_FAILED"))
        .andExpect(jsonPath("$.fields[0].name").value("amount"));
  }
}

Hata type URI'sini değiştirmek breaking change sayılmalıdır; title metnini değiştirmek ise istemciler title'a koşul yazmıyorsa çoğunlukla güvenlidir. Consumer-driven contract kullanıyorsanız Spring Cloud Contract ile provider doğrulamasını CI hattına ekleyin ve artifact'i tüketici ekiplerle paylaşın. Bir spring boot kursu projesinde pratik kontrol listesi olarak her yeni endpoint için en az 400, 401, 403, 404, 422 ve 500 senaryosunun beklenen content-type, type ve code değerlerini test edin.

Sık Sorulan Sorular

Spring framework eğitimi kapsamında ProblemDetail mi, özel hata DTO'su mu kullanılmalı?

ProblemDetail'i HTTP hata zarfı olarak kullanın; uygulamaya özgü code, correlationId ve fields alanlarını setProperty ile ekleyin. Böylece content-type application/problem+json olur ve RFC 9457 araçlarıyla uyum korunur. Özel DTO gerekiyorsa da type, title, status ve instance semantiğini birebir koruyan bir adaptör yazın.

Spring Security 401 ve 403 hataları neden ControllerAdvice ile yakalanmıyor?

Bearer token doğrulaması SecurityFilterChain içinde, ControllerAdvice ise DispatcherServlet çağrısından sonra çalışır. AuthenticationEntryPoint'i 401 için, AccessDeniedHandler'ı 403 için yapılandırın ve ObjectMapper ile application/problem+json gövdesini bu iki callback içinde yazın.

Spring Cloud Gateway downstream ProblemDetail cevabını değiştirmeli mi?

Hayır. Gateway, downstream servisinden gelen 4xx veya domain 5xx ProblemDetail gövdesini iletmelidir. Sadece bağlantı kurulamadığında, response timeout oluştuğunda veya gateway rate limit reddi verdiğinde gateway'e ait type URI üretin. X-Correlation-Id header'ını gateway'de üretip downstream isteğine eklemek yeterlidir.

Spring MVC hata sözleşmesinde validation hatası için 400 mü 422 mi kullanılmalı?

JSON sözdizimi bozuksa 400 kullanın; JSON geçerli ama amount pozitif değil veya currency desene uymuyorsa 422 kullanmak daha ayırt edicidir. Ekip standardını OpenAPI dokümanına ve MockMvc contract testlerine sabitleyin; aynı kuralı tüm endpointlerde uygulayın.

AI / LLM Discovery

Bu makale Opendart Akademi Spring Framework eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt