fix(http-client): set application/json on body + use JdkClientHttpRequestFactory for PATCH (v3.0.1)#4
Merged
Conversation
…uestFactory for PATCH
Two real bugs in core's HTTP client utilities, both surfaced by a fresh
end-to-end consumer (devslab-examples api-log-{jpa,mybatis,r2dbc}-demo)
calling postSyncTyped against a real @RequestBody-annotated controller:
1. Content-Type missing on POST/PUT/PATCH bodies. RestClient.body(String) /
WebClient.bodyValue(String) routed through StringHttpMessageConverter
and wrote text/plain;charset=ISO-8859-1. Downstream @RequestBody Foo
rejected the call as Unsupported Media Type -> 415 -> 500. Fix sets
application/json explicitly in both exchange() methods.
2. patchSync* / patchAsync* completely broken end-to-end. The autoconfig
wired SimpleClientHttpRequestFactory whose setRequestMethod throws
ProtocolException: Invalid HTTP method: PATCH (JDK HttpURLConnection
limitation). Swapped for JdkClientHttpRequestFactory (java.net.http,
Java 11+) which supports all five verbs. Read-timeout property
preserved; connect-timeout default left to HttpClient (any consumer
who needs a tighter one swaps the bean -- it's @ConditionalOnMissingBean).
Why these slipped past CI before: the existing RestApiClientUtilRoutingTest
/ ReactiveApiClientUtilRoutingTest are subclass-recording mocks that never
hit a socket. Bug (1) is invisible at the routing layer; bug (2) needs to
hit the request factory's setRequestMethod. Both are wire-level concerns.
Added end-to-end integration test coverage (65 cases across 4 classes):
RestApiClientUtilWireIT - MockWebServer-driven, wire-level:
ReactiveApiClientUtilWireIT Content-Type per verb, exact body bytes,
UTF-8 (Korean + emoji), large bodies
(32 KB), HTTP method propagation,
internal-field non-leakage
RestApiClientUtilSpringE2EIT - @SpringBootTest with real @RestController
ReactiveApiClientUtilSpringE2EIT on @RequestBody Foo. servlet + reactive,
each pinning spring.main.web-application-type
because the test classpath has both
starters. Round-trips all five verbs
plus 4xx/5xx propagation plus Unicode
plus nested objects plus null fields.
The Spring E2E ITs also surface a constraint nice to lock in: the core's
ApiEventListener requires an ApiLogWriter SPI bean to wire up, but the
implementations live in :jpa / :r2dbc / :mybatis. Each E2E test registers
a no-op ApiLogWriter for context startup. Documented in the test class.
No public API changes. Compatibility note: any caller passing a non-JSON
String body to a body-carrying method used to get text/plain; that body
now goes as application/json. api-log's HTTP wrappers are explicitly
JSON-only by design (the library is JSON + JSONB-centric); callers needing
other content types should use RestClient / WebClient directly.
VERSION 3.0.0 -> 3.0.1; CHANGELOG entry in root + docs/changelog{,ko}.md
covers fix + migration note.
jlc488
added a commit
that referenced
this pull request
May 23, 2026
PR #4 fixed two real HTTP-client bugs (Content-Type + PATCH); v3.0.1 is the recommended upgrade. Update every copy-paste install snippet so users land on the fixed version, but keep the historical prose ("v3.0.0 splits the starter into four artifacts...", "that's the v3.0.0 promise...") since those describe when the multi-module shape was introduced, which didn't change in 3.0.1. Sed-replaced patterns: <version>3.0.0</version> -> 3.0.1 :3.0.0" (Kotlin DSL) -> :3.0.1" :3.0.0' (Groovy DSL) -> :3.0.1' `3.0.0` (the "Replace `X` with..." hint) -> `3.0.1` Touched 10 files (README + docs/getting-started/installation + docs/guides/ {jpa,mybatis,r2dbc}-backend, each en + ko). CHANGELOG entries unchanged (historical records).
jlc488
added a commit
to devslab-kr/devslab-examples
that referenced
this pull request
May 23, 2026
… PATCH fixes) api-log v3.0.1 published to Maven Central (devslab-kr/api-log#4 + #5 merged, tag pushed, release workflow succeeded at 16:39Z, all four artifacts indexed). 3.0.1 fixes two bugs in RestApiClientUtil / ReactiveApiClientUtil that the previous PR #62 CI runs surfaced: 1. Content-Type missing on POST/PUT/PATCH body -> upstream returned 415/500 (the postBodyIsPreservedInPayloadColumn failures across all three demos). 2. PATCH method unsupported because SimpleClientHttpRequestFactory wraps java.net.HttpURLConnection (which rejects PATCH). Bump scope: one line per demo build.gradle.kts (well, two — core + backend), three demos = 6 lines. No source changes needed — the bugs were entirely in the starter, the demo code was already calling the right APIs. Expecting all 15 IT tests (5 per demo) to pass on this run now that the starter is fixed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two real bugs in core's HTTP client utilities — both surfaced by the first end-to-end consumer (devslab-examples `api-log-{jpa,mybatis,r2dbc}-demo` set) calling `postSyncTyped` against a real `@RequestBody`-annotated controller. Plus 65 new end-to-end test cases (4 new IT classes) so any future regression of this class is caught immediately.
Bugs fixed
1. `Content-Type` missing on POST/PUT/PATCH bodies
`RestClient.body(String)` / `WebClient.bodyValue(String)` route the body through `StringHttpMessageConverter`, which writes `Content-Type: text/plain;charset=ISO-8859-1` by default. Any downstream service binding with `@RequestBody Foo` rejected the call as Unsupported Media Type → 415 → propagated as 500 to the test client.
Fix: both `exchange()` methods explicitly set `MediaType.APPLICATION_JSON` when a payload is present.
```diff
```
2. `patchSync*` / `patchAsync*` broken end-to-end
`RestApiClientAutoConfiguration` wired `SimpleClientHttpRequestFactory` (backed by `java.net.HttpURLConnection`), whose `setRequestMethod` throws:
```
java.net.ProtocolException: Invalid HTTP method: PATCH
at java.base/java.net.HttpURLConnection.setRequestMethod(...)
```
A long-standing JDK limitation. `patchSync` was a published API that could never actually run.
Fix: swap to `JdkClientHttpRequestFactory` (`java.net.http.HttpClient` backed, Java 11+). All five verbs supported natively; `read-timeout` preserved; `connect-timeout` default left to `HttpClient` (consumers who need tighter swap the bean — it's `@ConditionalOnMissingBean`).
Why these escaped CI before
`RestApiClientUtilRoutingTest` / `ReactiveApiClientUtilRoutingTest` are subclass-recording mocks that intercept `send` / `sendTyped` before `exchange()` runs. They verify "POST was called with these args" but never reach the HTTP layer. Bug 1 is invisible above `exchange()`; bug 2 needs `setRequestMethod` to be called.
The gap was: no test that hit an actual socket or actual `@RequestBody`.
Test coverage added (65 cases, 4 classes)
Both E2E ITs pin `spring.main.web-application-type` explicitly because the test classpath has both `spring-boot-starter-web` and `spring-boot-starter-webflux` and the default would pick servlet for the reactive one too.
The E2E ITs also register a no-op `ApiLogWriter` bean — `ApiEventListener` requires the SPI to wire up, but writer implementations live in `:jpa` / `:r2dbc` / `:mybatis`. Documented inline in each test class.
Compatibility
Upgrading from 3.0.0
```diff
```
Recommended for anyone on 3.0.0 — every consumer calling a body-carrying method against a real Spring service is affected.
Test plan
Downstream actions after merge + v3.0.1 tag