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 |
|
|
|
2026-06-02 | resolved |
error: mockmvc-406-produces-accept-double-fault-2026-06-02
Layer:
raw/errors/— 실제 발생한 오류 / 막힘 / 트러블슈팅의 원석.
Parent / 부모
- raw/branch-notes/feature-api-contract-baseline — D9 (406 vs 415 distinct) 계약 테스트 작성 중 발생.
증상
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.java—handleHttpMediaTypeNotAcceptable/handleHttpMediaTypeNotSupportedsrc/adapter-web/src/test/java/dev/caskeleton/adapter/web/error/TransportErrorHandlingTest.java