Sub-scope 02 (inbound-web) error + validation + envelope revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5 generatedAt=2026-08-30T07:15:49+00:00 === OWNED FILES === 61c06223ed4dfac5cb94b1788de2afb46b9b3e8e main/java/dev/caskeleton/adapter/inbound/web/envelope/EnvelopeBodyAdvice.java 50 fa09a2bb8f0f1146659853da743de1a3d88943ab main/java/dev/caskeleton/adapter/inbound/web/error/BudgetProblemMapper.java 76 75873269a7a0555d28ba6eabe7cac86180ede801 main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeErrorMessages.java 65 9507703d56db4f1b45f62a26341180b775dd7c3c main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeValidationDetails.java 130 542bcb7ab58d02e8450aa83cd9e01be3b7bcbb29 main/java/dev/caskeleton/adapter/inbound/web/error/ErrorResponseFactory.java 35 4d032215718fc9c3dc0c34038cbd6902e95efad4 main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java 417 90a8aaa0bdfa27b86bb8f6833a046e9da3ef0190 main/java/dev/caskeleton/adapter/inbound/web/error/ProblemCatalog.java 183 56418a5e7509a0c0fa20d2be7f407b6eae4f67ef main/java/dev/caskeleton/adapter/inbound/web/error/ProblemCode.java 81 5b23ad283fe8c74e61aa0b79a46a8f4534bdca15 main/java/dev/caskeleton/adapter/inbound/web/error/ProblemDefinition.java 34 3a36c4f3abb4b1e797382ed2de037964b489888f main/java/dev/caskeleton/adapter/inbound/web/error/ProblemStatusMismatchException.java 28 f9b319637d88bcff57ea77fc5c06b8115b142c15 main/java/dev/caskeleton/adapter/inbound/web/error/SafeProblemDetailExtensions.java 53 c0c84b9e9af4caa715721bf77cd264fe68df8874 main/java/dev/caskeleton/adapter/inbound/web/error/ThrottleProblemWriter.java 78 1ee96422b2920792abc8d29b7777480f03c87b63 main/java/dev/caskeleton/adapter/inbound/web/error/ValidationIssue.java 33 b7df48bdb01fa677b72255fb33a98fd6da310c16 main/java/dev/caskeleton/adapter/inbound/web/error/WebProblem.java 55 37259f3aa1744258e00e0bedf7891dbfcfb877bf main/java/dev/caskeleton/adapter/inbound/web/error/WebProblemFactory.java 79 2ae0b9baf462bca6b25628e4bbcc9714d6910df5 main/java/dev/caskeleton/adapter/inbound/web/error/WebProblemSanitizer.java 85 d356abf4681c11dbd0280679ed6cf9463304fd7f main/java/dev/caskeleton/adapter/inbound/web/error/WebResourceNotVisibleException.java 37 740cd79b90c855d61f6ebab68161465d10f59849 main/java/dev/caskeleton/adapter/inbound/web/mvc/error/WebMvcProblemExceptionHandler.java 182 65ab7a8a369cd766d71c6f9acaf47dddc0b2056e main/java/dev/caskeleton/adapter/inbound/web/validation/TransportValidationException.java 48 dc30f22ba2988307d72587897843c544606ffe1f main/java/dev/caskeleton/adapter/inbound/web/validation/WebInputPointer.java 66 cdefe58978d3814bc33f85a57e7f82c459224457 main/java/dev/caskeleton/adapter/inbound/web/validation/WebValidationExceptionMapper.java 78 2ca258903c2a3a1916ebbde7617065164695774f main/java/dev/caskeleton/adapter/inbound/web/validation/WebValidationIssueMapper.java 95 e62332952fed5d25d76d901c628784201ffe8711 main/java/dev/caskeleton/adapter/inbound/web/webflux/error/WebFluxProblemExceptionHandler.java 173 ce2655c041e98d80f91e2fc75e3e34462b8539fa test/java/dev/caskeleton/adapter/inbound/web/envelope/EnvelopeBodyAdviceTest.java 88 629d85d8af89581560ce05a76ed770d6f6ed2e6d test/java/dev/caskeleton/adapter/inbound/web/envelope/EnvelopeMetaIntegrationTest.java 73 d6102747d9f6db20895d14d228e9189df77f21c5 test/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandlerTest.java 628 d1b1169de7e8083a792ab84b7f1f979b2e217963 test/java/dev/caskeleton/adapter/inbound/web/error/IdempotencyExceptionMappingTest.java 54 e27f1e3d6cc396448b9e762d9ceeaf6a8133f51c test/java/dev/caskeleton/adapter/inbound/web/error/NoResourceFoundErrorHandlingTest.java 62 515d315fd2c6d86837f5fe3a65a160adadf4434b test/java/dev/caskeleton/adapter/inbound/web/error/ProblemCatalogTest.java 99 40616be170c3b0afcc9703234471ece3bf2feb0c test/java/dev/caskeleton/adapter/inbound/web/error/SpanErrorRecorderHookTest.java 102 17860c0f5b0640a11f2011d4612df32fb72396a9 test/java/dev/caskeleton/adapter/inbound/web/error/TransportErrorHandlingTest.java 149 58e1e3056f24a8a3a7fb8e9d20443c0a7f32d0c2 test/java/dev/caskeleton/adapter/inbound/web/error/WebProblemFactoryTest.java 122 7cca8a05a6864b353af491e6860deac8d76f82df test/java/dev/caskeleton/adapter/inbound/web/validation/WebValidationExceptionMapperTest.java 125 file_count=33 === 8.1 두 에러 계약이 한 컨텍스트에 함께 등록되는가 === $ grep -n 'RestControllerAdvice' -B 2 -A 12 adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java | head -20 38-import org.springframework.web.bind.MethodArgumentNotValidException; 39-import org.springframework.web.bind.annotation.ExceptionHandler; 40:import org.springframework.web.bind.annotation.RestControllerAdvice; 41-import org.springframework.web.context.request.WebRequest; 42-import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 43-import org.springframework.web.multipart.MaxUploadSizeExceededException; 44-import org.springframework.web.servlet.NoHandlerFoundException; 45-import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 46-import org.springframework.web.servlet.resource.NoResourceFoundException; 47- 48-/** 49- * Skeleton-wide base error → {@link Envelope} converter. Handles operational, transport, and 50- * security exceptions only; domain exceptions are handled by a separate 51: * {@code @RestControllerAdvice} in the consuming module. See README for the design rationale. 52- */ 53:@RestControllerAdvice 54-public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { 55- 56- private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); 57- exit=0 $ grep -n '@RestControllerAdvice\|@Order\|@ConditionalOn' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/error/WebMvcProblemExceptionHandler.java 41:@RestControllerAdvice 46:@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 51:// Not @ConditionalOnBean: that condition is only reliable inside an auto-configuration class, where 55:@ConditionalOnProperty( 60:@Order(Ordered.HIGHEST_PRECEDENCE + 10) exit=0 $ grep -c '@ExceptionHandler' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java 17 exit=0 $ grep -n 'ExceptionHandler(' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/error/WebMvcProblemExceptionHandler.java 71: public WebMvcProblemExceptionHandler(WebProblemFactory problems) { 77: @ExceptionHandler(MethodArgumentNotValidException.class) 99: @ExceptionHandler(HttpMessageNotReadableException.class) 110: @ExceptionHandler(HttpMediaTypeNotSupportedException.class) 121: @ExceptionHandler(HttpRequestMethodNotSupportedException.class) 147: @ExceptionHandler(WebResourceNotVisibleException.class) 158: @ExceptionHandler(NoResourceFoundException.class) exit=0 $ grep -n 'protected ResponseEntity handle' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java 285: protected ResponseEntity handleMethodArgumentNotValid( 301: protected ResponseEntity handleHttpMessageNotReadable( 315: protected ResponseEntity handleHttpRequestMethodNotSupported( 341: protected ResponseEntity handleHttpMediaTypeNotSupported( 362: protected ResponseEntity handleMaxUploadSizeExceededException( 377: protected ResponseEntity handleHttpMediaTypeNotAcceptable( 398: protected ResponseEntity handleNoHandlerFoundException( 404: protected ResponseEntity handleNoResourceFoundException( exit=0 === 8.2 겹치는 예외 타입에서 어느 쪽이 이기는가 === $ grep -rn 'class GlobalExceptionHandler' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java 54:public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { exit=0 $ grep -rn 'implements Ordered\|@Order' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java | wc -l 0 exit=0 === 8.3 문서가 무엇을 선언하는가 === $ grep -n 'D5' adapter/inbound/web/README.md 168:- **D5: RFC 7807 `ProblemDetail` 표현은 거부**하고 자체 `Envelope` 형식을 쓴다. 245:- **W3C `traceparent`(D5/D7/D4).** 유효한 인바운드 traceparent 가 있으면 채택해 그 `traceId`→MDC `trace_id`, exit=0 $ grep -c 'problem+json\|RFC 9457\|ProblemCode' adapter/inbound/web/README.md adapter/inbound/web/CLAUDE.md adapter/inbound/web/README.md:0 adapter/inbound/web/CLAUDE.md:0 exit=1 $ sed -n '92,94p' adapter/inbound/web/CLAUDE.md Domain `@RestControllerAdvice` in a consuming module must be annotated `@Order(Ordered.HIGHEST_PRECEDENCE)` (or otherwise ordered ahead of this module's base `GlobalExceptionHandler`), because the base handler's catch-all exit=0 === 8.4 테스트가 두 advice 를 함께 세우는가 === $ grep -rln 'WebMvcProblemExceptionHandler' adapter/inbound/web/src/test adapter/inbound/web/src/testkit adapter/inbound/web/src/testkit/java/dev/caskeleton/webtestkit/ContractFixtureApplication.java exit=0 $ grep -n 'Import\|jsonPath' adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/error/NoResourceFoundErrorHandlingTest.java | head -14 7:import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 18:import org.springframework.context.annotation.Import; 28:@Import({ 43: .andExpect(jsonPath("$.success").value(false)) 44: .andExpect(jsonPath("$.error.code").value("ROUTE_NOT_FOUND")) 45: .andExpect(jsonPath("$.error.category").value("NOT_FOUND")) 46: .andExpect(jsonPath("$.error.retryable").value(false)) 47: .andExpect(jsonPath("$.error.message").value("Requested route was not found")) exit=0 === 8.4b 미도달 유틸 === $ grep -rn 'alreadySafe' --include=*.java adapter/inbound/web/src | wc -l 1 exit=0 $ grep -rn 'SafeProblemDetailExtensions' --include=*.java adapter/inbound/web/src/main | grep -v 'SafeProblemDetailExtensions.java' | wc -l 0 exit=0 $ grep -rn 'requireStatusAgreement' --include=*.java adapter/inbound/web/src/main | grep -v 'WebProblemFactory.java' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/BudgetProblemMapper.java:16: * with its own body would be caught by {@code requireStatusAgreement} only after shipping. adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/BudgetProblemMapper.java:73: problems.requireStatusAgreement(statusFor(violation), problem); adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/budget/WebBudgetOutcome.java:8: * something different for two of them, and {@code requireStatusAgreement} refused the response — adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/idempotency/IdempotentResponseWriter.java:68: problems.requireStatusAgreement(plan.status(), problem); adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/webflux/idempotency/WebFluxIdempotentInvoker.java:154: problems.requireStatusAgreement(plan.status(), problem); exit=0