As a principal engineer, discuss non-obvious HttpMessageConverter behaviors: the StringHttpMessageConverter charset default, converter reuse across RestTemplate, and how the write-side producible-type computation can surprise you.
answer
- String converter default = ISO-8859-1 (fix to UTF-8)
- same SPI powers RestTemplate (separate list)
- WebClient uses codecs, not HttpMessageConverter
- */* converters (String/byte[]) skew producible set
- classpath XML converter silently expands negotiation
basics
~20 sKey gotchas: StringHttpMessageConverter defaults to ISO-8859-1 when no charset is given (garbling UTF-8 text); the same converter SPI is reused by RestTemplate on the client side; and because String and byte[] converters advertise /, they can quietly pre-empt Jackson depending on ordering and Accept.
solid answer
~50 sSeveral subtle points. First, StringHttpMessageConverter's default charset is ISO-8859-1, so returning non-ASCII text with no charset in the media type can corrupt output; set the converter's defaultCharset to UTF-8 or ensure the media type carries charset=UTF-8. Second, the exact same HttpMessageConverter SPI powers RestTemplate — the client serializes request bodies and deserializes responses with converters, so a custom converter registered only in MVC won't help RestTemplate and vice versa. Third, the write path computes producible media types by asking every converter canWrite(type, null); converters that support */* (String, byte[]) can appear producible for many types, so with an absent or wildcard Accept, ordering decides and Jackson may not win. Finally, adding an XML converter to the classpath silently expands content negotiation, which can change existing endpoints' responses for clients sending Accept: application/xml. These are all consequences of the same order-plus-media-type selection model.
code
java · 22 lines@Configuration
class ConverterHardening implements WebMvcConfigurer {
// Fix the ISO-8859-1 default so text/plain responses are UTF-8.
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.replaceAll(c ->
c instanceof StringHttpMessageConverter
? new StringHttpMessageConverter(StandardCharsets.UTF_8)
: c);
}
}
// The SAME SPI on the client side — MVC config does NOT reach here.
class ClientConfig {
RestTemplate restTemplate() {
RestTemplate rt = new RestTemplate();
// Client-side converters are independent; customize explicitly.
rt.getMessageConverters().add(0, new MappingJackson2HttpMessageConverter());
return rt;
}
}go deeper
Awareness that text encoding and client-side serialization exist as separate concerns.
Know the ISO-8859-1 default and that RestTemplate has its own converters.
Explain the producible-type computation, / pre-emption, and classpath-driven negotiation drift.
Reason holistically about the shared SPI across server/client, content-negotiation governance, ordering-precedence risks, and the full exception-to-status mapping for robust API contracts.
## 1. The StringHttpMessageConverter charset trap `StringHttpMessageConverter` has historically used **ISO-8859-1** as its default charset (`DEFAULT_CHARSET`). When a response media type carries no explicit `charset` parameter, the converter encodes the String with ISO-8859-1, which mangles multi-byte UTF-8 characters (accents, CJK, emoji). Mitigations: - Ensure the negotiated media type includes `charset=UTF-8` (e.g., `produces = "text/plain;charset=UTF-8"`). - Replace/adjust the converter with a UTF-8 default: `new StringHttpMessageConverter(StandardCharsets.UTF_8)` and register it via `extendMessageConverters`. Note JSON is unaffected because `MappingJackson2HttpMessageConverter` writes UTF-8 by default and JSON is defined as UTF-8. This bites specifically on raw-`String`/`text/plain` endpoints. ## 2. The converter SPI is shared with the client (RestTemplate) `HttpMessageConverter` is not MVC-only. `RestTemplate` holds its **own** `List<HttpMessageConverter<?>>` and uses them to serialize request bodies (`postForObject` with a POJO) and deserialize responses (`getForObject(url, MyDto.class)`). Consequences: - A converter you register through `WebMvcConfigurer` affects **server** endpoints only; a `RestTemplate` you `new` up gets the default converter list, not your MVC customizations. - To customize a `RestTemplate`, set its converters explicitly (`restTemplate.getMessageConverters()` / `setMessageConverters`) or use `RestTemplateBuilder` in Boot. - `WebClient` (reactive) uses a different but analogous abstraction (`HttpMessageReader`/`HttpMessageWriter` via codecs), not `HttpMessageConverter`. ## 3. Producible-type computation surprises During writing, `AbstractMessageConverterMethodProcessor` builds the set of **producible** media types by iterating converters and, for each that `canWrite(returnType, null)`, adding its `getSupportedMediaTypes()`. Because `StringHttpMessageConverter` and `ByteArrayHttpMessageConverter` advertise `*/*`, and because converters are lenient in `canWrite(type, null)`, the producible set can be broader than you'd expect. With a wildcard or absent `Accept`, the **first** producible converter for that media type wins — so ordering, not 'JSON is obviously right', decides. This is why returning a `String` yields `text/plain` and returning a `byte[]` yields `application/octet-stream` even in a JSON-centric API. ## 4. Classpath-driven negotiation drift Adding `jackson-dataformat-xml` (perhaps transitively) causes `MappingJackson2XmlHttpMessageConverter` to be registered. Now the same handler can produce XML for `Accept: application/xml`. An API you thought was JSON-only can start emitting XML to clients that ask for it — a behavior change with no code edit. Lock this down with explicit `produces` constraints or a restricted `ContentNegotiationManager` if it matters. ## 5. Ordering / precedence maintenance Because selection is first-match-wins, inserting a converter with `add(0, ...)` gives it precedence for its media types. But it must correctly implement `canWrite`/`canRead` narrowly, or it will greedily capture requests meant for others. A converter that returns `true` from `canWrite` too liberally and sits early in the list is a classic cause of 'my JSON started coming back as something else'. ## 6. Exception mapping recap (for completeness) - Unreadable request media type → `HttpMediaTypeNotSupportedException` → 415. - Malformed body for a matched converter → `HttpMessageNotReadableException` → 400. - No acceptable writable converter → `HttpMediaTypeNotAcceptableException` → 406. - Serialization failure → `HttpMessageNotWritableException` → 500. Understanding these lets you build robust `@ExceptionHandler` / `ResponseEntityExceptionHandler` behavior for content-negotiation errors.
- Why doesn't a converter registered via WebMvcConfigurer affect a hand-created RestTemplate?Because RestTemplate maintains its own independent converter list. The WebMvcConfigurer hooks configure the server-side MVC list only. You must set converters on the RestTemplate (or use RestTemplateBuilder in Boot) to change client-side serialization.
- A UTF-8 API suddenly returns garbled text on a /export endpoint returning a String. What's the likely cause and fix?StringHttpMessageConverter defaulted to ISO-8859-1 because the media type carried no charset. Fix by returning produces = text/plain;charset=UTF-8, or registering a StringHttpMessageConverter constructed with StandardCharsets.UTF_8.
- How would you prevent an endpoint from ever emitting XML even if jackson-dataformat-xml lands on the classpath?Constrain it with produces = application/json (or MediaType.APPLICATION_JSON_VALUE), and/or configure the ContentNegotiationManager to restrict producible types. That way Accept: application/xml yields 406 rather than XML.
saying these in an interview costs you the question
- Assuming StringHttpMessageConverter defaults to UTF-8.
- Thinking MVC converter customizations automatically apply to RestTemplate/WebClient.
- Claiming WebClient uses HttpMessageConverter (it uses HttpMessageReader/Writer codecs).