Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4065b5a
Add streaming decoders
trumpetinc Sep 1, 2026
0f3c444
code formatting
trumpetinc Sep 1, 2026
4e479c7
Fix formatting
trumpetinc Sep 2, 2026
d98a3cd
Make InputStreamAndReaderDecoder a PredicatedDecoder
trumpetinc Sep 2, 2026
7321620
Fix formatting
trumpetinc Sep 2, 2026
f6750bc
Make sure mock web server gets closed
trumpetinc Sep 8, 2026
93d299f
Parse charset using existing code
trumpetinc Sep 8, 2026
7dbed76
Kill accidental diff
trumpetinc Sep 8, 2026
1a8fa04
Revert change - it should come in from a merge from master into 14.x,
trumpetinc Sep 8, 2026
05bf465
Fix formatting
trumpetinc Sep 8, 2026
d9be32f
Apache download no longer hosting mvnd 1.0.2 - moving to 1.0.6
trumpetinc Sep 2, 2026
7c406d2
Revert change - it should come in from a merge from master into 14.x,
trumpetinc Sep 8, 2026
41b4581
Merge branch 'streaming_decoder_support2' of
trumpetinc Sep 22, 2026
bbf0cc0
Handle streaming TypedResponses
trumpetinc Sep 22, 2026
d32f339
Fix formatting
trumpetinc Sep 22, 2026
91a1b61
Fix scenario where decoding throws exception leaves the body unclosed.
trumpetinc Sep 23, 2026
f6f53b5
Document breaking changes in migration-v14 documentation
trumpetinc Sep 23, 2026
b3dd854
Add javadoc
trumpetinc Sep 23, 2026
c970cca
Add javadoc about Decoders for Closeable return types
trumpetinc Sep 23, 2026
0e25883
formatting
trumpetinc Sep 23, 2026
f2047e4
formatting
trumpetinc Sep 23, 2026
0c7d030
Add tests for void returns and ensure closeable return types don't close
trumpetinc Sep 23, 2026
cdbd2e0
Move InputStreamAndReaderDecoder functionality into DefaultDecoder
trumpetinc Sep 23, 2026
b93fa2f
Add tests checking that a) thrown exceptions result in body being closed
trumpetinc Oct 6, 2026
6317776
decoder can't return null if we want to fully test logic
trumpetinc Oct 6, 2026
d597b4c
Reimplementing the autoclose logic
trumpetinc Oct 6, 2026
2455154
Adjustments from 10/2/2026 code review feedback
trumpetinc Oct 6, 2026
0aafdac
Add tests for Dismiss404 and streaming responses
trumpetinc Oct 6, 2026
3e8fc29
Merge branch '14.x' into streaming_decoder_support2
trumpetinc Oct 6, 2026
0b890bc
Remove dup methods added by git merge
trumpetinc Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 118 additions & 25 deletions MIGRATION-v14.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Migration Guide — Feign v14 (Request Body Streaming)
# Migration Guide — Feign v14 (Request and Response Body Streaming)

This guide covers the breaking changes introduced in https://github.com/OpenFeign/feign/pull/3360 and explains how to update your code.
This guide covers the breaking changes introduced in https://github.com/OpenFeign/feign/pull/3360 (streaming requests) and https://github.com/OpenFeign/feign/pull/3551 (streaming responses) and explains how to update your code.

> **Target release:** `v14`

---

## Overview
## Request Streaming

### Overview

`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.
Expand All @@ -19,17 +21,17 @@ The **breaking changes** primarily affect code that interacts directly with requ
- Any code that directly reads `Request.body()`, `Request.length()`, `Request.charset()`, or `RequestTemplate.body()`/
`RequestTemplate.requestBody()`

### `DefaultEncoder` streaming support (non-breaking) (https://github.com/OpenFeign/feign/pull/3396)
#### `DefaultEncoder` streaming support (non-breaking) (https://github.com/OpenFeign/feign/pull/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.

---

## Breaking Changes
### Breaking Changes

### 1. `Request.Body` is now an interface
#### 1. `Request.Body` is now an interface

**Before:**

Expand Down Expand Up @@ -74,7 +76,7 @@ boolean repeatable = body.isRepeatable();

---

### 2. `Request.body()` now returns `Optional<Request.Body>`
#### 2. `Request.body()` now returns `Optional<Request.Body>`

**Before:**

Expand All @@ -98,7 +100,7 @@ if (body.isPresent()) {

---

### 3. `Request.length()` removed
#### 3. `Request.length()` removed

**Before:**

Expand All @@ -117,7 +119,7 @@ long length = request.body()

---

### 4. `Request.charset()` removed
#### 4. `Request.charset()` removed

**Before:**

Expand All @@ -130,7 +132,7 @@ itself.

---

### 5. `Request.isBinary()` removed
#### 5. `Request.isBinary()` removed

**Before:**

Expand All @@ -150,7 +152,7 @@ boolean repeatable = request.body()

---

### 6. `Request.create(...)` overloads removed
#### 6. `Request.create(...)` overloads removed

The `byte[]` + `Charset`-based `Request.create(...)` overloads have been removed.

Expand All @@ -175,9 +177,9 @@ Request.create(HttpMethod.GET, url, headers, null, null);

---

### 7. `RequestTemplate` API changes
#### 7. `RequestTemplate` API changes

#### `RequestTemplate.body(String)` removed
##### `RequestTemplate.body(String)` removed

**Before:**

Expand All @@ -191,7 +193,7 @@ template.body("hello world");
template.body(Request.Body.of("hello world"));
```

#### `RequestTemplate.body(byte[], Charset)` deprecated
##### `RequestTemplate.body(byte[], Charset)` deprecated

**Before:**

Expand All @@ -207,7 +209,7 @@ template.body(Request.Body.of(bytes, StandardCharsets.UTF_8));
template.body(Request.Body.of(bytes));
```

#### `RequestTemplate.body()` (returns `byte[]`) removed
##### `RequestTemplate.body()` (returns `byte[]`) removed

**Before:**

Expand All @@ -227,11 +229,11 @@ if (requestBody.isPresent()) {
}
```

#### `RequestTemplate.requestBody()` is no longer `@Deprecated`
##### `RequestTemplate.requestBody()` is no longer `@Deprecated`

The method now returns `Optional<Request.Body>` and is the primary accessor.

#### `RequestTemplate.requestCharset()` removed
##### `RequestTemplate.requestCharset()` removed

**Before:**

Expand All @@ -243,7 +245,7 @@ Charset charset = template.requestCharset();

---

### 8. Custom `Encoder` implementations
#### 8. Custom `Encoder` implementations

If you implement a custom `Encoder`, update calls to `template.body(...)`:

Expand All @@ -265,7 +267,7 @@ template.body(Request.Body.of(serialized));

---

### 9. Custom `Client` implementations
#### 9. Custom `Client` implementations

If you implement a custom `Client`, update how you write the request body:

Expand Down Expand Up @@ -294,7 +296,7 @@ cannot be re-sent.

---

### 10. `FeignException.errorReading(...)` — request body no longer captured
#### 10. `FeignException.errorReading(...)` — request body no longer captured

`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:
Expand All @@ -313,7 +315,7 @@ assertThat(exception.contentUTF8()).isEmpty();

---

### 11. `mock` module — `RequestKey` no longer stores `Charset`
#### 11. `mock` module — `RequestKey` no longer stores `Charset`

**Before:**

Expand All @@ -326,15 +328,15 @@ code.

---

### 12. `Request.Body` no longer implements `Serializable`
#### 12. `Request.Body` no longer implements `Serializable`

`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.

---

### 13. Vert.x integration — `VertxFeign.Builder` now requires `.vertx(Vertx)`
#### 13. Vert.x integration — `VertxFeign.Builder` now requires `.vertx(Vertx)`

**Before:**

Expand All @@ -355,7 +357,7 @@ VertxFeign.builder()

---

## Implementing a Custom Streaming Body
### Implementing a Custom Streaming Body

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
Expand Down Expand Up @@ -420,8 +422,99 @@ public class FileBody implements Request.Body {

---

## Spring Cloud OpenFeign Compatibility
### Spring Cloud OpenFeign Compatibility

`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.

---

## Response Streaming

### Overview

Feign now supports response streaming via `DefaultDecoder` (which now supports InputStream and Reader return types) or via custom `Decoder` implementations. Previously, Feign read the entire content of the response into memory prior to processing - this made it difficult to use Feign when downloading large response payloads.

Example:

```java
interface ExampleInterface {

@RequestLine("GET /")
InputStream getLargeStream();

}

ExampleInterface api = Feign.builder()
.target(ExampleInterface.class, "http://localhost");

try(InputStream is = api.getLargeStream()){ // Note that caller is responsible for closing the returned InputStream
// work with the stream...
}
```

To support this change, a breaking change was made in how Feign automatically closes the underlying response body during response processing.

### Breaking Changes

If a Feign interface template method returns a `Closeable` object type, Feign will no longer automatically close the input stream from the server. It is now the caller's responsibility to call `close()`.

The caller is now responsible for calling close:

```java
try (InputStream is = api.getLargeStream()) {
// process stream
}

```

or

```java
InputStream is = api.getLargeStream();
try{
// process stream
} finally {
is.close();
}

```

### Implementing a Custom Streaming Response Decoder

A custom streaming response decoder handles a type that implements `Closeable`.

For example:

```java

public class ExampleStream implements Closeable{
// ...
}

public class ExampleStreamingDecoder implements PredicatedDecoder {

@Override
public boolean canDecode(Response response, Type type) {
if (ExampleStream.class.equals(type)) return true;

return false;
}

@Override
public Object decode(Response response, Type type)
throws IOException, DecodeException, FeignException {

if (ExampleStream.class.equals(type)) return new ExampleStream(response.body().asInputStream());

throw new DecodeException(
response.status(),
format("%s is not a type supported by this decoder.", type),
response.request());
}

}
```

---
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ public class MyApp {
}
```

### Streaming Responses

To handle large response bodies (instead of reading them into memory), Feign supports InputStream and Reader as return types for interface methods.

**Important:** The caller is responsible for closing the returned InputStream and Reader instance.

Other custom `Decoder` implementations may also support streaming. As a general rule any Closeable instance returned from a Feign target must be closed by the caller.

#### Impact of Logger Settings on Streaming Responses

If Logger level is set to `Logger.Level.FULL`, the entire response is buffered, effectively disabling response streaming.

### Interface Annotations

Feign annotations define the `Contract` between the interface and how the underlying client
Expand Down
43 changes: 34 additions & 9 deletions api/src/main/java/feign/InvocationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import feign.codec.PredicatedDecoder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
Expand Down Expand Up @@ -101,6 +102,8 @@ public Object proceed() throws Exception {
}

Response response = this.response;
boolean shouldClose = closeAfterDecode;

try {
final boolean shouldDecodeResponseBody =
(response.status() >= 200 && response.status() < 300)
Expand All @@ -117,31 +120,53 @@ public Object proceed() throws Exception {
}
Exception error = errorDecoder.decode(configKey, response);
if (error instanceof RetryableException) {
ensureClosed(response.body());
throw error;
}
return decodeErrorResponseBody(response, error);
}

if (isVoidType(returnType) && !decodeVoid) {
ensureClosed(response.body());
return kotlinUnitInstance(returnType);
// By default, Closeable return types will leave the body stream open, but there may be
// downstream logic that reverses this decision
if (isReturnTypeCloseable()) {
shouldClose = false;
}

Class<?> rawType = Types.getRawType(returnType);
if (TypedResponse.class.isAssignableFrom(rawType)) {
if (isVoidType(returnType)) {
shouldClose = true; // override closeAfterDecode if void return type
if (!decodeVoid) return kotlinUnitInstance(returnType);
}

if (TypedResponse.class.isAssignableFrom(Types.getRawType(returnType))) {
Type bodyType = Types.resolveLastTypeParameter(returnType, TypedResponse.class);
return TypedResponse.builder(response).body(decode(response, bodyType)).build();
Object decodeResult = decode(response, bodyType);
if (decodeResult == null) shouldClose = true;
return TypedResponse.builder(response).body(decodeResult).build();
} else {
Object result = decode(response, returnType);
if (result == null) shouldClose = true;
return result;
}

return decode(response, returnType);
} finally {
if (closeAfterDecode) {
if (shouldClose) {
ensureClosed(response.body());
}
}
}

private boolean isReturnTypeCloseable() {

if (isVoidType(returnType)) return false;

Type rawType = Types.getRawType(returnType);

if (TypedResponse.class.isAssignableFrom(Types.getRawType(rawType))) {
rawType = Types.resolveLastTypeParameter(returnType, TypedResponse.class);
}

return Closeable.class.isAssignableFrom(Types.getRawType(rawType));
}

private boolean shouldDecodeErrorResponseBody(Response response) {
if (!decodeErrorResponses
|| response.status() < 400
Expand Down
Loading