Files
llm-wiki/raw/errors/mockmvc-406-produces-accept-double-fault-2026-06-02.md
T

3.8 KiB

title, source_type, status, related_branches, related_projects, tags, created, status_label
title source_type status related_branches related_projects tags created status_label
error / mockmvc-406-produces-accept-double-fault-2026-06-02 error-note raw
feature-api-contract-baseline
ca-skeleton
error
ca-skeleton
spring-mvc
content-negotiation
406
mockmvc
testing
2026-06-02 resolved

error: mockmvc-406-produces-accept-double-fault-2026-06-02

Layer: raw/errors/ — 실제 발생한 오류 / 막힘 / 트러블슈팅의 원석.

Parent / 부모

증상

406 Not Acceptable 핸들러(D9)를 검증하려고, produces = APPLICATION_JSON 인 핸들러에 Accept: application/xml 로 요청해 HttpMediaTypeNotAcceptableException 을 유발하는 테스트를 작성. status().isNotAcceptable() 단언이 AssertionError, 그 원인은 IllegalArgumentException 이었고 응답이 406 envelope 으로 떨어지지 않았다.

근본 원인 / Root cause

이중 실패(double-fault)였다. 컨텐츠 협상 실패로 406 이 발생하면 GlobalExceptionHandler.handleHttpMediaTypeNotAcceptable 가 envelope ResponseEntity<Envelope> 를 반환한다. 그런데 이 에러 응답 본문 자체 도 클라이언트의 Accept: application/xml 에 맞춰 직렬화돼야 하는데, 그 미디어 타입을 만족하는 메시지 컨버터가 없어(JSON 만 등록) 응답 작성 단계에서 다시 협상 실패가 난다. 실 서버(full Spring)에서는 에러 경로가 JSON 으로 강제 작성되지만, standalone MockMvc 의 최소 컨버터 구성에서는 이 2차 실패가 그대로 표면화된다.

즉 "produces/Accept 불일치" 시나리오는 406 핸들러 로직 이 아니라 테스트 하네스의 컨버터 협상 을 시험하게 되어, 정작 검증하려는 핸들러 매핑을 못 본다.

해결 / Resolution

테스트를 협상 경로 대신 예외를 직접 던지는 probe 로 전환:

@GetMapping("/t/not-acceptable")
Map<String,String> notAcceptable() throws HttpMediaTypeNotAcceptableException {
    throw new HttpMediaTypeNotAcceptableException(List.of(MediaType.APPLICATION_JSON));
}

요청은 기본 Accept(/) 라 406 envelope 이 JSON 으로 정상 직렬화되고, ResponseEntityExceptionHandler 우산 → handleHttpMediaTypeNotAcceptable override 가 실제로 타는지 결정적으로 검증된다. 415(handleHttpMediaTypeNotSupported)는 요청 본문 Content-Type 으로 자연스럽게 유발 가능하므로 그대로 두고, 406 만 직접 throw 로 분리.

회고 / Lessons

  • 406 의 본질: 응답 표현 협상 실패. 그 에러 응답을 거부된 미디어 타입으로 다시 쓰려 하면 무한히 협상 실패한다 — 실서버는 fallback 으로 해결하지만 테스트 하네스는 다를 수 있다.
  • 핸들러 매핑/분류 를 검증할 때는 협상 경로를 통하기보다 해당 예외를 직접 던지는 게 결정적이고 하네스-독립적. (협상 자체의 동작은 별도 통합 테스트에서.)
  • 415(요청 본문) 와 406(응답 표현) 는 RFC 9110 상 의미가 다르고 유발 경로도 다르다 — 테스트도 분리해야 한다. 이 분리 자체가 D9 가 "둘을 같은 코드로 뭉개지 말라" 고 한 이유의 실증.

재발 가능성

  • 향후 XML/기타 표현 협상을 지원하면 produces/Accept 경로의 통합 테스트가 필요해지고, 그때는 컨버터를 갖춘 full-context 테스트로 가야 한다.

Sources / 근거

  • src/adapter-web/src/main/java/dev/caskeleton/adapter/web/error/GlobalExceptionHandler.javahandleHttpMediaTypeNotAcceptable / handleHttpMediaTypeNotSupported
  • src/adapter-web/src/test/java/dev/caskeleton/adapter/web/error/TransportErrorHandlingTest.java