This guide covers the breaking changes introduced in #3360 and explains how to update your code.
Target release:
v14
feign.Request.Body has been redesigned from a byte[]-backed concrete class into a streaming-ready interface.
Request bodies are no longer eagerly buffered in memory unless you explicitly use the byte[]/String factory methods.
For most users, regular Feign usage via interface annotations and Feign.builder().target(...) is unchanged.
The breaking changes primarily affect code that interacts directly with request body internals, including:
- Custom
Encoderimplementations - Custom
Clientimplementations - Any code that directly reads
Request.body(),Request.length(),Request.charset(), orRequestTemplate.body()/RequestTemplate.requestBody()
DefaultEncoder streaming support (non-breaking) (#3396)
DefaultEncoder now additionally supports File, Path, InputStream, and Request.Body as request body types.
This is an additive, non-breaking change. Existing DefaultEncoder users do not need to modify code; users can now opt
into streaming by passing these types.
Before:
// Body was a concrete class with public fields/methods
Request.Body body = Request.Body.create("hello", StandardCharsets.UTF_8);
byte[] bytes = body.asBytes();
String str = body.asString();
int len = body.length();
Optional<Charset> charset = body.getEncoding();
boolean binary = body.isBinary();After:
// Body is now an interface — use the factory methods
Request.Body body = Request.Body.of("hello", StandardCharsets.UTF_8);
// To read content, write it to a stream (note: these methods throw checked IOException):
byte[] bytes = body.writeToByteArray();
String str = body.writeToString(StandardCharsets.UTF_8);
long len = body.contentLength(); // -1 if unknown/streaming
boolean repeatable = body.isRepeatable();Removed methods on Request.Body:
| Removed | Replacement |
|---|---|
Body.create(String) |
Body.of(String) |
Body.create(String, Charset) |
Body.of(String, Charset) |
Body.create(byte[]) |
Body.of(byte[]) |
Body.create(byte[], Charset) |
Body.of(byte[], Charset) |
Body.encoded(byte[], Charset) |
Body.of(byte[], Charset) |
Body.empty() |
Pass null for no body |
body.asBytes() |
body.writeToByteArray() |
body.asString() |
body.writeToString(charset) |
body.length() → int |
body.contentLength() → long (returns -1 if unknown) |
body.getEncoding() |
Read charset from Content-Type header |
body.isBinary() |
No direct replacement; body.isRepeatable() may be useful depending on your use case |
Before:
byte[] body = request.body(); // nullable byte[]
if (body != null) {
out.write(body);
}After:
// writeTo(OutputStream) throws checked IOException, so a plain Consumer lambda
// (as used in Optional.ifPresent) cannot propagate it. Use an explicit if-block instead.
Optional<Request.Body> body = request.body();
if (body.isPresent()) {
body.get().writeTo(out);
}Before:
int length = request.length();After:
// contentLength() does not throw, so Optional.map is safe here.
long length = request.body()
.map(Request.Body::contentLength)
.orElse(0L);Before:
Charset charset = request.charset();After: Read the charset from the Content-Type request header. There is no longer a charset field on Request
itself.
Before:
boolean binary = request.isBinary();After:
// There is no direct replacement for request.isBinary().
// Depending on why you were checking it, request body repeatability may be useful:
boolean repeatable = request.body()
.map(Request.Body::isRepeatable)
.orElse(false);The byte[] + Charset-based Request.create(...) overloads have been removed.
Before:
Request.create(HttpMethod.GET, url, headers, bodyBytes, charset);
Request.create(HttpMethod.GET, url, headers, bodyBytes, charset, requestTemplate);
// Deprecated String-based variant:
Request.create("GET", url, headers, bodyBytes, charset);After:
// With a body:
Request.create(HttpMethod.GET, url, headers, Request.Body.of(bodyBytes), requestTemplate);
// Without a body:
Request.create(HttpMethod.GET, url, headers, null, null);Before:
template.body("hello world");After:
template.body(Request.Body.of("hello world"));Before:
template.body(bytes, StandardCharsets.UTF_8);After:
template.body(Request.Body.of(bytes, StandardCharsets.UTF_8));
// or, if charset is irrelevant for your use case:
template.body(Request.Body.of(bytes));Before:
byte[] body = template.body();After:
// writeToByteArray() throws checked IOException, so a plain Function lambda
// (as used in Optional.map) cannot propagate it. Use an explicit if-block instead.
byte[] body = null;
Optional<Request.Body> requestBody = template.requestBody();
if (requestBody.isPresent()) {
body = requestBody.get().writeToByteArray();
}The method now returns Optional<Request.Body> and is the primary accessor.
Before:
Charset charset = template.requestCharset();After: Read charset from the Content-Type header. There is no longer a charset tracked on the template itself.
If you implement a custom Encoder, update calls to template.body(...):
Before:
template.body(serialized.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
// or
template.body(serialized);After:
template.body(Request.Body.of(serialized, StandardCharsets.UTF_8));
// or, for UTF-8 strings:
template.body(Request.Body.of(serialized));If you implement a custom Client, update how you write the request body:
Before:
byte[] body = request.body();
if (body != null) {
outputStream.write(body);
}After:
// writeTo(OutputStream) throws checked IOException, so a plain Consumer lambda
// (as used in Optional.ifPresent) cannot propagate it. Use an explicit if-block instead.
Optional<Request.Body> body = request.body();
if (body.isPresent()) {
body.get().writeTo(outputStream);
}For retry-capable clients, check body.isRepeatable() before attempting a retry — non-repeatable (streaming) bodies
cannot be re-sent.
FeignException no longer captures the request body when a read error occurs, because the body may be a non-repeatable
stream. Code asserting exception.contentUTF8() returns the request body must be updated:
Before:
assertThat(exception.contentUTF8()).isEqualTo("Request body");After:
assertThat(exception.contentUTF8()).isEmpty();Before:
RequestKey.builder(...).charset(StandardCharsets.UTF_8).build();After: The charset(Charset) method has been removed from RequestKey.Builder. Remove it from your mock setup
code.
feign.Request.Body previously implemented java.io.Serializable. This has been removed.
If you were serializing Request.Body objects (e.g., for caching or distributed tracing), you will need an alternative
serialization strategy.
Before:
VertxFeign.builder()
.webClient(webClient)
.target(MyApi.class, url);After:
VertxFeign.builder()
.vertx(vertx) // required — NPE with descriptive message if missing
.webClient(webClient)
.target(MyApi.class, url);14. Encoder.encode() now returns boolean (#3485)
Encoder.encode() now returns boolean instead of void. Return true when the encoder
handles the object, false otherwise. This replaces the separate canEncode() method.
Before:
public class MyEncoder implements Encoder {
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
template.body(Request.Body.of(serialize(object)));
}
}After:
public class MyEncoder implements Encoder {
@Override
public boolean encode(Object object, Type bodyType, RequestTemplate template) {
template.body(Request.Body.of(serialize(object)));
return true; // or return false if the encoder does not handle this type
}
}Built-in encoders (DefaultEncoder, FormEncoder, MeteredEncoder, GraphqlEncoder, etc.) already return boolean
from encode(). If your encoder returns false, the MultiEncoder (see section 19) will try the next encoder. If
no encoder returns true, an EncodeException is thrown.
feign.codec.Encoder has been relocated from the feign-core module to the new feign-api
module. The package name (feign.codec) is unchanged. If you have a direct dependency on
feign-core without feign-api, you need to add feign-api to your classpath.
The deprecated inner class Encoder.Default (which extended DefaultEncoder) has been removed.
Before:
new Encoder.Default()After:
new feign.core.codec.DefaultEncoder()17. Composing multiple encoders with MultiEncoder.of() (#3485)
Use MultiEncoder.of(...) to compose multiple encoders into a single MultiEncoder, which
delegates to the first encoder whose encode() returns true.
Before:
Feign.builder()
.encoder(new JacksonEncoder())
.target(MyApi.class, "https://api.example.com");After (multiple encoders):
Feign.builder()
.encoder(MultiEncoder.of(
new FormEncoder(),
new JacksonEncoder(),
new JAXBEncoder(factory)
))
.target(MyApi.class, "https://api.example.com");After (single encoder is unchanged):
Feign.builder()
.encoder(new JacksonEncoder())
.target(MyApi.class, "https://api.example.com");The MultiEncoder.of() factory returns a MultiEncoder, which tries
each encoder's encode() and uses the first one that returns true.
If you want to stream a body (e.g., from a file or InputStream), implement Request.Body directly. Because
writeTo(OutputStream) itself declares throws IOException, the lambda can propagate it freely — the restriction
only applies to standard functional interfaces (Consumer, Function, etc.) that don't
declare checked exceptions.
// One-shot InputStream — non-repeatable (isRepeatable() defaults to false)
Request.Body streamingBody = outputStream -> {
try (InputStream in = Files.newInputStream(path)) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
// or, with Java 9+:
// in.transferTo(outputStream);
}
// IOException propagates naturally — no try-catch needed here
};
template.body(streamingBody);For a repeatable streaming body (e.g., backed by a file that can be re-read):
public class FileBody implements Request.Body {
private final Path path;
public FileBody(Path path) {
this.path = path;
}
@Override
public void writeTo(OutputStream out) throws IOException {
try (InputStream in = Files.newInputStream(path)) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
// or, with Java 9+:
// in.transferTo(out);
}
}
@Override
public boolean isRepeatable() {
return true;
}
@Override
public long contentLength() {
try {
return Files.size(path);
} catch (IOException e) {
return Request.Body.super.contentLength(); // returns -1
}
}
}TODO: Once we have specialized EncoderPredicate factory methods (i.e. for xml content types), update this example to use those factory methods
The new PredicateEncoder and EncoderPredicate classes can be used in conjunction with MultiEncoder to fine tune which Encoder
handles different types of encode requests.
Feign.builder()
.encoder(MultiEncoder.of(
new DefaultEncoder(),
new PredicateEncoder((obj, type, templ) -> templ.headers().get("Content-Type").contains("application/xml"), new JAXBEncoder(factory), // handle xml requests
new JacksonEncoder() // handle everything else
))
.target(MyApi.class, "https://api.example.com");RequestTemplate#body(byte[], Charset) is kept @Deprecated for backward compatibility with
spring-cloud-openfeign-core. Spring Cloud OpenFeign users are not required to make any changes immediately, but should
migrate to body(Request.Body) once the Spring team provides an updated release.