Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
* `DefaultEncoder` now supports streaming request bodies for `File`, `Path`, `InputStream`, and `Request.Body` types,
avoiding in-memory buffering. New `Request.PathBody` and `Request.InputStreamBody` implementations are provided for
these cases. (https://github.com/OpenFeign/feign/pull/3396)
* `Encoder.encode()` now returns `boolean` — return `true` when encoding succeeds, `false` if
the encoder does not handle the type. The `Encoder.of()` factory method composes multiple encoders
into a `DelegatingEncoder` that tries each delegate's `encode()` in order. Built-in JSON encoders (Jackson, Gson,
Moshi, Fastjson2, Jackson-Jr) and XML encoders (JAXB, SOAP) now check the request's
`Content-Type` header via `Util.isJsonContentType()` / `Util.isXmlContentType()` and return
`false` when it does not match. Existing custom `Encoder` implementations
must update the `encode()` return type from `void` to `boolean`.
(https://github.com/OpenFeign/feign/pull/3476)

### Version 13.12

Expand Down
166 changes: 166 additions & 0 deletions MIGRATION-v14.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,172 @@ VertxFeign.builder()

---

### 14. `Encoder.encode()` now returns `boolean` (https://github.com/OpenFeign/feign/pull/3476)

`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:**

```java
public class MyEncoder implements Encoder {
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
template.body(Request.Body.of(serialize(object)));
}
}
```

**After:**

```java
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 `DelegatingEncoder` (see section 19) will try the next encoder. If
no encoder returns `true`, an `EncodeException` is thrown.

---

### 15. JSON and XML encoders require `Content-Type` header (https://github.com/OpenFeign/feign/pull/3476)

Built-in JSON and XML encoders now gate on the `Content-Type` header and return
`false` from `encode()` when it does not match:

- JSON encoders (Jackson, Gson, Moshi, Fastjson2, Jackson-Jr, Jackson-Jaxb) check
`Util.isJsonContentType(template)` and return `false` when it does not match
(e.g., `application/json`, `application/ld+json`).
- XML encoders (JAXB, SOAP) check `Util.isXmlContentType(template)` and return
`false` when it does not match (e.g., `text/xml`, `application/xml`).

Comment thread
yvasyliev marked this conversation as resolved.
When using `Encoder.of()` (see section 18), a JSON or XML encoder will only claim
the request if a matching `Content-Type` header is present.

**You must ensure a `Content-Type` header is set** before the encoder runs. If
your interface does not set the `Content-Type` header via annotations (e.g.,
`@Headers("Content-Type: application/json")`), add it in one of the following ways:

```java
// 1. Add the header via @Headers on the interface or method:
@Headers("Content-Type: application/json")
public interface MyApi {
@RequestLine("POST /data")
void post(Data data);
}

// 2. Add the header globally via a RequestInterceptor:
Feign.builder()
.requestInterceptor(template ->
template.header("Content-Type", "application/json"))
.target(MyApi.class, "https://api.example.com");
```

> [!NOTE]
> Spring Cloud OpenFeign automatically sets `Content-Type: application/json` for `@RequestBody`-annotated parameters, so
> Spring Feign users are not affected by this change.

---

### 16. `Encoder` moved from `core` to `api` module

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

---

### 17. `Encoder.Default` removed

The deprecated inner class `Encoder.Default` (which extended `DefaultEncoder`) has been removed.

**Before:**

```java
new Encoder.Default()
```

**After:**

```java
new feign.core.codec.DefaultEncoder()
```

---

### 18. Composing multiple encoders with `Encoder.of()` (https://github.com/OpenFeign/feign/pull/3476)

Use `Encoder.of(...)` to compose multiple encoders into a single `DelegatingEncoder`, which
delegates to the first encoder whose `encode()` returns `true`.

**Before:**

```java
Feign.builder()
.encoder(new JacksonEncoder())
.target(MyApi.class, "https://api.example.com");
```

**After (multiple encoders):**

```java
Feign.builder()
.encoder(Encoder.of(
new FormEncoder(),
new JacksonEncoder(),
new JAXBEncoder(factory)
))
.target(MyApi.class, "https://api.example.com");
```

**After (single encoder is unchanged):**

```java
Feign.builder()
.encoder(new JacksonEncoder())
.target(MyApi.class, "https://api.example.com");
```

The `Encoder.of()` factory wraps the supplied encoders in a `DelegatingEncoder`, which tries
each encoder's `encode()` and uses the first one that returns `true`.

---

### 19. Multi-encoder support — `DelegatingEncoder` (https://github.com/OpenFeign/feign/pull/3476)

You can now compose multiple encoders via `Encoder.of()`, and Feign will pick the right one at
request time based on the return value of `encode()`. This is especially useful for APIs that mix
JSON, XML, and other content types:

**Before:**

```java
// Only one encoder — had to manually choose or wrap
Feign.builder()
.encoder(new FormEncoder(new JacksonEncoder()))
.target(MyApi.class, "https://api.example.com");
```

**After:**

```java
Feign.builder()
.encoder(Encoder.of(
new FormEncoder(), // handles multipart/form-urlencoded by Content-Type
new JacksonEncoder(), // implements JsonEncoder marker interface
new JAXBEncoder(factory) // handles XML
))
.target(MyApi.class, "https://api.example.com");
```

---

## Implementing a Custom Streaming Body

If you want to stream a body (e.g., from a file or `InputStream`), implement `Request.Body` directly. Because
Expand Down
2 changes: 1 addition & 1 deletion api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
Expand Down
22 changes: 19 additions & 3 deletions api/src/main/java/feign/RequestTemplateFactoryResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.template.UriUtils;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
Expand Down Expand Up @@ -232,7 +233,9 @@ protected RequestTemplate resolve(
}
}
try {
encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable);
if (!encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable)) {
throw new EncodeException("This encoder does not support form encoding: " + encoder);
Comment thread
yvasyliev marked this conversation as resolved.
}
} catch (EncodeException e) {
throw e;
} catch (RuntimeException e) {
Expand Down Expand Up @@ -269,9 +272,9 @@ protected RequestTemplate resolve(
try {
if (alwaysEncodeBody) {
body = argv == null ? new Object[0] : argv;
encoder.encode(body, Object[].class, mutable);
encode(body, Object[].class, mutable);
} else {
encoder.encode(body, metadata.bodyType(), mutable);
encode(body, metadata.bodyType(), mutable);
}
} catch (EncodeException e) {
throw e;
Expand All @@ -280,5 +283,18 @@ protected RequestTemplate resolve(
}
return super.resolve(argv, mutable, variables);
}

private void encode(Object object, Type bodyType, RequestTemplate mutable)
Comment thread
yvasyliev marked this conversation as resolved.
throws EncodeException {
if (!encoder.encode(object, bodyType, mutable)) {
throw new EncodeException(
"This encoder does not support encoding of type: "
+ bodyType
+ " with object: "
+ object
+ ", encoder: "
+ encoder);
}
}
}
}
30 changes: 30 additions & 0 deletions api/src/main/java/feign/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -371,4 +371,34 @@ public static String getThreadIdentifier() {
+ "_"
+ currentThread.getId();
}

/**
* Checks if the request template has a content type header that is JSON.
*
* @param template the request template to check
* @return {@code true} if the content type is JSON, {@code false} otherwise
* @since 14
*/
public static boolean isJsonContentType(RequestTemplate template) {
return template.headers().getOrDefault("Content-Type", List.of()).stream()
.anyMatch(
contentType ->
contentType != null
&& contentType.trim().matches("(?i)\\w+/(?:[\\w._-]+\\+)?json.*"));
}

/**
* Checks if the request template has a content type header that is XML.
*
* @param template the request template to check
* @return {@code true} if the content type is XML, {@code false} otherwise
* @since 14
*/
public static boolean isXmlContentType(RequestTemplate template) {
return template.headers().getOrDefault("Content-Type", List.of()).stream()
.anyMatch(
contentType ->
contentType != null
&& contentType.trim().matches("(?i)\\w+/(?:[\\w._-]+\\+)?xml.*"));
}
}
76 changes: 76 additions & 0 deletions api/src/main/java/feign/codec/DelegatingEncoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.codec;

import feign.RequestTemplate;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Objects;

/**
* An encoder that delegates to a list of encoders, using the first one that can encode the given
* object.
*
* @since 14
*/
class DelegatingEncoder implements Encoder {
private final List<Encoder> delegates;

/**
* Creates a new {@link DelegatingEncoder} with the given list of delegates.
*
* @param delegates the list of delegates to use for encoding. Both list and its elements must not
* be {@code null}.
*/
DelegatingEncoder(List<Encoder> delegates) {
this.delegates = Objects.requireNonNull(delegates, "delegates cannot be null");
}

/**
* Encodes the given object using the first delegate that can encode it. If no delegate can encode
* the object, an {@link EncodeException} is thrown.
*
* @param object {@inheritDoc}
* @param bodyType {@inheritDoc}
* @param template {@inheritDoc}
* @throws EncodeException {@inheritDoc}
*/
@Override
public boolean encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
return delegates.stream()
.map(encoder -> encoder.encode(object, bodyType, template))
.filter(Boolean::booleanValue)
.findFirst()
.orElseThrow(
() ->
new EncodeException(
"No suitable encoder found for object encoding: "
+ object
+ ", encoders: "
+ delegates));
}

/**
* {@inheritDoc}
*
* @return {@inheritDoc}
*/
@Override
public String toString() {
return "DelegatingEncoder{" + "delegates=" + delegates + '}';
}
}
Loading