Skip to content

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

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

feat!: make feign.Request.Body streaming-ready#3360
yvasyliev wants to merge 7 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.rc.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.

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.

Copy link
Copy Markdown
Contributor Author

@yvasyliev yvasyliev May 19, 2026

Choose a reason for hiding this comment

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

While I like the elegance of that suggestion, I find three reasons to keep it as is:

  1. In vast majority of cases, XML/JSON encoders operate with small chunk of data fitting in-memory.
  2. It appears that some clients, servers and the Logger might benefit from Content-Length header. And to calculate the length, the data should be eagerly encoded into tangible bytes one way or another.
  3. If there's retry feature enabled, jacksonJaxbJsonProvider.writeTo will be executed two or more times which is not efficient.

Additionally, I like your idea from the other thread to add support for raw OutputStream and Writer bodies (not implemented in this PR). If there's a really big piece of XML or JSON document, users could leverage raw body supplier to stream the large payload:

interface Client {
    @RequestLine("POST /api/v1/send")
    @Header("Content-Type: application/json; charset=UTF-8")
    void send(ThrowingConsumer<OutputStream, IOException> body);
}

client.send(outputStream -> jacksonJaxbJsonProvider.writeTo(..., outputStream));

Alternatively, users can provide custom Encoder implementation with lazy encoding configured.

I'd be happy to hear maintainers' opinion too, and I'm open to any idea.

TL;DR I prefer to keep existing encoders as is and let users enable streaming on demand if needed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I agree with you about most json encoders working on relatively small chunks. And I agree that there is really no need to make changes to the existing encoders.

You make a very good point about logging - Feign users rely on body logging, and having the body content in memory makes logging a lot simpler. I actually explored logging with streams in my initial prototypes for this project using buffered streams and mark/reset, but it was overly complicated and did not add sufficient value. Your design is much better.

Note that there is a "streaming" encoder for Feign that looks like it is designed to handle very large json streams, and they might benefit from this (I haven't dug into that encoder in depth), but for the general case, I am in agreement that we should leave the existing encoders alone until there is a compelling reason to change them.

Comment thread java11/src/main/java/feign/http2client/Http2Client.java Outdated
@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.

yvasyliev and others added 3 commits May 19, 2026 06:25
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>
@yvasyliev
Copy link
Copy Markdown
Contributor Author

yvasyliev commented May 19, 2026

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.

I believe this is out of #2734 scope. From my experience, there's only a handful of cases where the client might not want to trust the server's Content-Type response header. For those scenarios, I would consider adding a charset instance variable to Decoder implementations so that the users can hardcode the desired charset via Feign builder. But again, I would do this as part of a separate initiative

If we're talking about the scenario where the server doesn't return charset or Content-Type values, the fallback remains UTF-8

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