The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
11 KiB
Plaintext
157 lines
11 KiB
Plaintext
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<Object> handle' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java
|
|
285: protected ResponseEntity<Object> handleMethodArgumentNotValid(
|
|
301: protected ResponseEntity<Object> handleHttpMessageNotReadable(
|
|
315: protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
|
|
341: protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
|
|
362: protected ResponseEntity<Object> handleMaxUploadSizeExceededException(
|
|
377: protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(
|
|
398: protected ResponseEntity<Object> handleNoHandlerFoundException(
|
|
404: protected ResponseEntity<Object> 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
|
|
|