New posts
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# groups the HttpServiceProxyRegistry knows about
|
||||
group apikey -> [ApiKeyClient]
|
||||
group bearer -> [SecureClient]
|
||||
group flaky -> [FlakyClient]
|
||||
group github -> [GitHubClient]
|
||||
group oauth -> [OAuthClient]
|
||||
group posts -> [ParamsClient, PostClient]
|
||||
group resilient -> [ResilientClient]
|
||||
PostClient bean is a JDK proxy: true
|
||||
registry.getClient("posts", PostClient.class) is the injected bean: true
|
||||
|
||||
# CRUD against the stub (every call is a real HTTP request)
|
||||
findAll() -> 3 posts
|
||||
findById(1) -> Post[id=1, userId=1, title=First post, body=Body of the first post]
|
||||
findByUser(2) -> [3]
|
||||
findEntity(2) -> status 200 OK, Content-Type application/json
|
||||
create(...) -> id 101
|
||||
update(1, ...) -> title 'Updated Title'
|
||||
delete(1) -> void, no exception
|
||||
|
||||
# argument kinds (ParamsClient)
|
||||
@RequestHeader -> upstream saw X-Correlation-Id=abc-123
|
||||
@RequestParam(required=false), null -> 3 posts
|
||||
@RequestParam(defaultValue="2") -> posts [3]
|
||||
URI argument -> Second post
|
||||
|
||||
# what the stub actually received
|
||||
GET /posts
|
||||
GET /posts/1
|
||||
GET /posts?userId=2
|
||||
GET /posts/2
|
||||
POST /posts
|
||||
PUT /posts/1
|
||||
DELETE /posts/1
|
||||
GET /echo/headers
|
||||
GET /posts
|
||||
GET /posts?userId=2
|
||||
GET /posts/2
|
||||
@@ -0,0 +1,15 @@
|
||||
# A. the keys the article's first version used (spring.http.client.service.group.<g>.base-url)
|
||||
findAll() -> java.lang.IllegalArgumentException: URI with undefined scheme
|
||||
|
||||
# B. the keys Spring Boot 4.1.2 binds (spring.http.serviceclient.<g>.base-url)
|
||||
findAll() -> OK -> 3
|
||||
|
||||
# C. no base URL at all
|
||||
findAll() -> java.lang.IllegalArgumentException: URI with undefined scheme
|
||||
|
||||
# D. a return type that does not match the JSON (array decoded as a single Post)
|
||||
list() -> org.springframework.web.client.RestClientException: Error while extracting response for type [com.ankurm.hsc.model.Post] and content type [application/json]
|
||||
root cause class: tools.jackson.databind.exc.MismatchedInputException
|
||||
|
||||
# E. @GetMapping on a client interface instead of @GetExchange
|
||||
createClient(UsesRequestMapping.class).list() -> java.lang.IllegalStateException: Unexpected method invocation: public abstract java.lang.String com.ankurm.hsc.Step02PropertiesAndPitfallsTest$UsesRequestMapping.list()
|
||||
@@ -0,0 +1,30 @@
|
||||
# 1. framework defaults (a plain RestClient, no status handler)
|
||||
findById(99) -> HttpClientErrorException$NotFound
|
||||
status : 404 NOT_FOUND
|
||||
message : 404 Not Found: "{"type":"https://stub.example/problems/post-not-found","title":"Post not found","status":404,"detail":"No post with id 99","instance":"/posts/99"}"
|
||||
content-type: application/problem+json
|
||||
getResponseBodyAs(ProblemDetail): title='Post not found' detail='No post with id 99' type=https://stub.example/problems/post-not-found
|
||||
alwaysDown() -> HttpServerErrorException.ServiceUnavailable, status 503 SERVICE_UNAVAILABLE
|
||||
|
||||
# 2. a response that never comes: read timeout is 1500ms on the flaky group, /slow takes 3000ms
|
||||
slow() -> ResourceAccessException, cause java.net.http.HttpTimeoutException
|
||||
message: I/O error on GET request for "http://127.0.0.1:<port>/slow": Request cancelled
|
||||
|
||||
# 3. nobody listening
|
||||
findAll() -> ResourceAccessException, cause java.net.ConnectException
|
||||
|
||||
# 4. Optional<Post> and ResponseEntity<Post> on a 404 with the default handler
|
||||
findOptional(99) -> NotFound
|
||||
findEntity(99) -> NotFound
|
||||
|
||||
# 5. NotFoundRestClientAdapterDecorator (spring-web 7.0.10) turns a 404 into an empty result
|
||||
findById(99) -> null
|
||||
findOptional(99) -> Optional.empty
|
||||
findEntity(99) -> status 404 NOT_FOUND, body null
|
||||
|
||||
# 6. our own defaultStatusHandler on the "posts" group (UpstreamProblemStatusHandler)
|
||||
findById(99) -> UpstreamNotFoundException: Upstream says not found: No post with id 99
|
||||
problem.type=https://stub.example/problems/post-not-found instance=/posts/99
|
||||
create(blank title) -> UpstreamValidationException: Upstream rejected the request: Validation failed
|
||||
extension 'errors' = [{field=title, message=must not be blank}]
|
||||
alwaysDown() -> UpstreamUnavailableException: Upstream unavailable (503): Upstream is down for maintenance
|
||||
@@ -0,0 +1,6 @@
|
||||
# our service (MockMvc, real HTTP to the stub behind it)
|
||||
GET /api/posts/1 -> 200 {"id":1,"userId":1,"title":"First post","body":"Body of the first post"}
|
||||
GET /api/posts/99 -> 404 {"detail":"No post with id 99","instance":"/api/posts/99","status":404,"title":"Not Found"}
|
||||
POST blank title -> 400 {"detail":"Upstream rejected the request","instance":"/api/posts","status":400,"title":"Bad Request","errors":[{"field":"title","message":"must not be blank"}]}
|
||||
GET /api/down -> 502 {"detail":"Upstream service error","instance":"/api/down","status":502,"title":"Bad Gateway"}
|
||||
GET /api/slow -> 504 {"detail":"Upstream service timed out","instance":"/api/slow","status":504,"title":"Gateway Timeout"}
|
||||
@@ -0,0 +1,24 @@
|
||||
# 1. RetryTemplate around the call (org.springframework.core.retry, spring-core 7.0.10)
|
||||
get("tpl", failures=2) -> 'Recovered on call 3'
|
||||
stub saw 3 calls; listener events: [beforeRetry, onRetryFailure(ServiceUnavailable), beforeRetry, onRetrySuccess]
|
||||
maxRetries(2) against a dead upstream -> HttpServerErrorException$ServiceUnavailable / 503 Service Unavailable: "{"type":"https://stub.example/problems/service-unavailable","title":"Service Unavailable","status":503,"detail":"Flaky upstream, call 3","instance":"/flaky/exhaust"}"
|
||||
stub saw 3 calls
|
||||
execute() instead of invoke() -> RetryException, retryCount=2, exceptions=3, last=ServiceUnavailable
|
||||
|
||||
# 2. @Retryable on the @HttpExchange interface method (bean from the "flaky" group)
|
||||
get("ann", failures=2) -> 'Recovered on call 3'; stub saw 3 calls
|
||||
get("ann-dead", failures=10) -> HttpServerErrorException$ServiceUnavailable / 503 Service Unavailable: "{"type":"https://stub.example/problems/service-unavailable","title":"Service Unavailable","status":503,"detail":"Flaky upstream, call 4","instance":"/flaky/ann-dead"}"
|
||||
stub saw 4 calls (maxRetries = 3)
|
||||
|
||||
# 3. the same retry as a ClientHttpRequestInterceptor on the "resilient" group (idempotent methods only)
|
||||
get("icpt", failures=2) -> 'Recovered on call 3'; stub saw 3 calls; interceptor attempts so far: 3
|
||||
POST createUnstable -> HttpServerErrorException$ServiceUnavailable / 503 Service Unavailable: "{"type":"https://stub.example/problems/service-unavailable","title":"Service Unavailable","status":503,"detail":"Created, but the response was lost","instance":"/posts/unstable"}"
|
||||
stub created the resource 1 time(s): the interceptor did not retry a POST
|
||||
|
||||
# 4. the trap: @Retryable on a POST whose response was lost after the server did the work
|
||||
createUnstable() -> id 102, body 'created 2 times so far'
|
||||
stub created the resource 2 times for ONE logical call
|
||||
|
||||
# 5. Resilience4j 2.4.0 around the same plain client
|
||||
maxAttempts(3), failures=2 -> 'Recovered on call 3'; stub saw 3 calls
|
||||
maxAttempts(3) against a dead upstream -> ServiceUnavailable; stub saw 3 calls
|
||||
@@ -0,0 +1,13 @@
|
||||
# 1. no credentials (a plain RestClient)
|
||||
whoAmI() -> 401 Unauthorized, WWW-Authenticate: Bearer realm="stub"
|
||||
|
||||
# 2. bearer token added by a ClientHttpRequestInterceptor on the "bearer" group
|
||||
whoAmI() -> {caller=demo-user, tokenKind=static}
|
||||
headers the upstream saw: Authorization=Bearer <redacted>, X-Correlation-Id present=true (8 chars)
|
||||
|
||||
# 3. API key from spring.http.serviceclient.apikey.default-header.X-Api-Key (no Java at all)
|
||||
whoAmI() -> {caller=demo-service, auth=api-key}
|
||||
headers the upstream saw: X-Api-Key=demo-key-123, X-Correlation-Id present=true
|
||||
|
||||
# 4. the interceptor is per group: the "apikey" group never sends Authorization
|
||||
Authorization header on the apikey group: null
|
||||
@@ -0,0 +1,14 @@
|
||||
# @ClientRegistrationId("stub") on the interface + OAuth2RestClientHttpServiceGroupConfigurer
|
||||
call 1: whoAmI() -> {caller=demo-user, tokenKind=oauth2}
|
||||
call 2: whoAmI() -> {caller=demo-user, tokenKind=oauth2}
|
||||
call 3: whoAmI() -> {caller=demo-user, tokenKind=oauth2}
|
||||
|
||||
# what the stub saw, in order
|
||||
POST /oauth/token
|
||||
GET /secure/bearer
|
||||
GET /secure/bearer
|
||||
GET /secure/bearer
|
||||
token requests: 1
|
||||
|
||||
# the same interface WITHOUT the group configurer: the annotation is silently ignored
|
||||
whoAmI() -> 401 UNAUTHORIZED
|
||||
@@ -0,0 +1,14 @@
|
||||
# 1. GET: URL, method and the decoded record
|
||||
findById(1) -> Post[id=1, userId=1, title=Test Title, body=Test body]
|
||||
|
||||
# 2. POST: the request body the interface sent
|
||||
create(...) -> id 101 (request matched: path, method, Content-Type, JSON body)
|
||||
|
||||
# 3. a ProblemDetail 404 goes through the real status handler
|
||||
findById(9) -> UpstreamNotFoundException (handler decoded the body: 'No post with id 9')
|
||||
|
||||
# 4. a 500 with no body: the handler falls back instead of crashing
|
||||
findAll() -> UpstreamUnavailableException: Upstream unavailable (500): (no problem+json body)
|
||||
|
||||
# 5. what MockRestServiceServer cannot tell you
|
||||
timeouts, connection resets, TLS and real serialisation quirks never happen here; see 03 and 05 for the real server.
|
||||
@@ -0,0 +1,2 @@
|
||||
# a proxy built from the slice's own RestClient.Builder, which the mock server IS bound to
|
||||
findById(1) -> Post[id=1, userId=1, title=From the mock, body=b]
|
||||
@@ -0,0 +1,2 @@
|
||||
@RestClientTest(PostClient.class), then @Autowired PostClient -> no such bean (null)
|
||||
MockRestServiceServer bean present -> true
|
||||
@@ -0,0 +1,2 @@
|
||||
PostClient from the @ImportHttpServices group is injected: true
|
||||
server.expect(...) -> IllegalStateException: Unable to use auto-configured MockRestServiceServer since a mock server customizer has not been bound to a RestTemplate or RestClient
|
||||
Reference in New Issue
Block a user