Skip to content

feat!: make feign.Request.Body streaming-ready#3360

Open
yvasyliev wants to merge 4 commits into
OpenFeign:masterfrom
yvasyliev:feature/request-body-streaming
Open

feat!: make feign.Request.Body streaming-ready#3360
yvasyliev wants to merge 4 commits into
OpenFeign:masterfrom
yvasyliev:feature/request-body-streaming

Conversation

@yvasyliev
Copy link
Copy Markdown
Contributor

@yvasyliev yvasyliev commented May 18, 2026

Co-authored-by: trumpetinc 6618744+trumpetinc@users.noreply.github.com

Summary

This PR is the first step toward resolving #2734 — enabling request body streaming in Feign. It builds on the foundational work from @trumpetinc in #2754, which had become too outdated to merge directly.

The core idea is to change feign.Request.Body from a byte[]-backed structure to a streaming-ready abstraction. This means request bodies are no longer cached in memory unless explicitly needed — enabling Feign to send large files and streams without buffering.

Important

This change does not introduce public streaming API in Feign. It only makes the Feign client streaming-ready. Public streaming API is planned to be implemented in a follow-up PR.

Warning

This is a breaking change and is intended to be released as v14.beta.1.


What changed

Core changes

  • feign.Request.Body is now an interface with:
    • writeTo(OutputStream) — the primary write mechanism
    • contentLength() — returns -1 for unknown/streaming bodies
    • isRepeatable()false for non-repeatable bodies
    • writeToByteArray() and writeToString(Charset) — helper methods for repeatable bodies (use with caution)
    • Body.of(...) — factory methods for String, byte[] inputs
  • feign.Request.BodyImpl is the default implementation for repeatable, non-streaming bodies. It overrides toString() for human-readable logging.
  • feign.RequestTemplate now exposes a single body(Request.Body) setter. The old body(byte[], Charset) overload is @Deprecated for backward compatibility with spring-cloud-openfeign-core.
  • feign.Request.body() now returns Optional<Request.Body> instead of byte[].
  • feign.Request#length() removed — use Request.Body#contentLength() instead.
  • feign.RequestTemplate#requestBody() returns Optional<Request.Body>.
  • Removed feign.RequestTemplate#charset instance variable — charset should be read from Content-Type headers.
  • Added feign.utils.ThrowingConsumer<T, E> functional interface.

HTTP client updates

All feign.Client implementations updated to stream bodies via Request.Body#writeTo:

Client Change
DefaultClient body.get().writeTo(out)
ApacheHttp5Client New FeignBodyEntity wrapping Request.Body
AsyncApacheHttp5Client Migrated to ClassicRequestBuilder + ClassicToAsyncRequestProducer for true streaming
OkHttpClient New anonymous RequestBody delegating to feign.Request.Body
Http2Client BodyPublishers.ofInputStream via PipedInputStream/PipedOutputStream
GoogleHttpClient New FeignBodyContent inner class
JAXRSClient Uses StreamingOutput
VertxHttpClient Uses new OutputToReadStream bridge (see below)

Encoder updates

All encoder implementations updated to call template.body(Request.Body.of(...)):

  • GsonEncoder, Jackson3Encoder, JAXBEncoder, JacksonJaxbJsonEncoder, Fastjson2Encoder, MoshiEncoder, SOAPEncoder, DefaultEncoder, JsonEncoder, Fastjson2Encoder

Vert.x streaming bridge

A new feign.vertx.OutputToReadStream class bridges Java blocking OutputStream to Vert.x ReadStream<Buffer>. It is adapted from io.cloudonix:vertx-java.io (MIT License) with a local compatibility fix to support both Vert.x 4 and Vert.x 5 (using onComplete instead of andThen to avoid a runtime linkage to io.vertx.core.Completable absent in Vert.x 4).

An issue has been filed upstream: cloudonix/vertx-java.io#8. Once fixed upstream, OutputToReadStream can be removed from this repo.

VertxFeign.Builder now requires .vertx(Vertx) in addition to .webClient(WebClient). A NullPointerException with a descriptive message is thrown if either is missing.

FeignException changes

  • FeignException.errorReading(...) now passes null as the body (instead of request.body()), since the request body may be a non-repeatable stream and should not be cached.
  • Related test assertions updated to expect isEmpty().

Logging

  • Logger updated to use Request.Body#toString() (provided by BodyImpl) for repeatable bodies.

Metrics

  • dropwizard-metrics4/5 MeteredEncoder: uses body.contentLength().
  • micrometer MeteredEncoder: reads Content-Length header for recording.

mock module

  • RequestKey no longer stores or compares Charset.
  • RequestKey.Builder#charset(Charset) removed.

Known limitations / future work

  1. spring-cloud-openfeign-core compatibility: RequestTemplate#body(byte[], Charset) kept as @Deprecated. Once the Spring team migrates to body(Request.Body), this can be removed.
  2. Charset extraction: There is no trivial vanilla Java way to parse charset from Content-Type headers. UTF-8 is used as a fallback in writeToString. A separate issue/PR may be needed.
  3. FeignException design: Response bodies are still cached as byte[] in FeignException. Reconsidering this is deferred to a future PR.
  4. All tests pass, but the build should be triggered with -Djapicmp.skip=true property provided.

Credits

Special thanks to @trumpetinc whose original #2754 laid the groundwork for this change.


Related

yvasyliev and others added 4 commits May 18, 2026 07:14
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
jacksonJaxbJsonProvider.writeTo(
object, bodyType.getClass(), null, null, APPLICATION_JSON_TYPE, null, outputStream);
template.body(outputStream.toByteArray(), Charset.defaultCharset());
template.body(Request.Body.of(outputStream.toByteArray()));
Copy link
Copy Markdown

@trumpetinc trumpetinc May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a temporary byte array, could we do something like this?

template.body( Request.Body.of(os -> jacksonJaxbJsonProvider.writeTo(
          object, bodyType.getClass(), null, null, APPLICATION_JSON_TYPE, null, os) ) );

This would require a Body.of method that would take an interface that can write body content to an arbitrary stream, so I'm thinking that maybe what I'm suggesting would be some future improvement? If so, then I understand why it is done this way for now.

() -> {
try (outputStream) {
body.writeTo(outputStream);
} catch (IOException ignored) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a good idea to ignore here? It seems like we may need to rethrow an unchecked exception....

@trumpetinc
Copy link
Copy Markdown

I've read over the core changes, and I had one smallish input on this change:

"Removed feign.RequestTemplate#charset instance variable — charset should be read from Content-Type headers."

I think that this should be carefully considered from a backwards compatibility viewpoint. Some REST servers do not properly set the charset header - in those cases, it is probably important that the Feign user be able to set an override using the @headers annotation.

I have not reviewed the code in sufficient detail to know whether this type of override is still possible, but that should probably be checked.

We should also be aware that existing Feign users that rely on the old default UTF-8 content type behavior (for example, when interacting with servers that send the wrong content type header in the response), could have problems. I don't really see a good way around this.

Personally, I think that this is a breaking change that we should be ok with - the library should have always used the headers for content type determination, and it is better to fix this now, even if it introduced breaking changes for some misconfigured servers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants