Skip to content

Commit 5be2434

Browse files
authored
Merge branch 'master' into remove-content-type-header-with-empty-post-body
2 parents 49d5f4d + 3e6d4b2 commit 5be2434

File tree

63 files changed

+3917
-31
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+3917
-31
lines changed

.mvn/extensions.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
<extension>
2020
<groupId>com.gradle</groupId>
2121
<artifactId>develocity-maven-extension</artifactId>
22-
<version>1.22</version>
22+
<version>1.22.1</version>
2323
</extension>
2424
<extension>
2525
<groupId>com.gradle</groupId>
2626
<artifactId>common-custom-user-data-maven-extension</artifactId>
27-
<version>2.0</version>
27+
<version>2.0.1</version>
2828
</extension>
2929
</extensions>

README.md

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,3 +1275,261 @@ The Bill Of Material is a special POM file that groups dependency versions that
12751275
</dependencyManagement>
12761276
</project>
12771277
```
1278+
# Form Encoder
1279+
1280+
[![build_status](https://travis-ci.org/OpenFeign/feign-form.svg?branch=master)](https://travis-ci.org/OpenFeign/feign-form)
1281+
[![maven_central](https://maven-badges.herokuapp.com/maven-central/io.github.openfeign.form/feign-form/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.openfeign.form/feign-form)
1282+
[![License](http://img.shields.io/:license-apache-brightgreen.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
1283+
1284+
This module adds support for encoding **application/x-www-form-urlencoded** and **multipart/form-data** forms.
1285+
1286+
## Add dependency
1287+
1288+
Include the dependency to your app:
1289+
1290+
**Maven**:
1291+
1292+
```xml
1293+
<dependencies>
1294+
...
1295+
<dependency>
1296+
<groupId>io.github.openfeign.form</groupId>
1297+
<artifactId>feign-form</artifactId>
1298+
<version>4.0.0</version>
1299+
</dependency>
1300+
...
1301+
</dependencies>
1302+
```
1303+
1304+
**Gradle**:
1305+
1306+
```groovy
1307+
compile 'io.github.openfeign.form:feign-form:4.0.0'
1308+
```
1309+
1310+
## Requirements
1311+
1312+
The `feign-form` extension depend on `OpenFeign` and its *concrete* versions:
1313+
1314+
- all `feign-form` releases before **3.5.0** works with `OpenFeign` **9.\*** versions;
1315+
- starting from `feign-form`'s version **3.5.0**, the module works with `OpenFeign` **10.1.0** versions and greater.
1316+
1317+
> **IMPORTANT:** there is no backward compatibility and no any gurantee that the `feign-form`'s versions after **3.5.0** work with `OpenFeign` before **10.\***. `OpenFeign` was refactored in 10th release, so the best approach - use the freshest `OpenFeign` and `feign-form` versions.
1318+
1319+
Notes:
1320+
1321+
- [spring-cloud-openfeign](https://github.com/spring-cloud/spring-cloud-openfeign) uses `OpenFeign` **9.\*** till **v2.0.3.RELEASE** and uses **10.\*** after. Anyway, the dependency already has suitable `feign-form` version, see [dependency pom](https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/spring-cloud-openfeign-dependencies/pom.xml#L19), so you don't need to specify it separately;
1322+
1323+
- `spring-cloud-starter-feign` is a **deprecated** dependency and it always uses the `OpenFeign`'s **9.\*** versions.
1324+
1325+
## Usage
1326+
1327+
Add `FormEncoder` to your `Feign.Builder` like so:
1328+
1329+
```java
1330+
SomeApi github = Feign.builder()
1331+
.encoder(new FormEncoder())
1332+
.target(SomeApi.class, "http://api.some.org");
1333+
```
1334+
1335+
Moreover, you can decorate the existing encoder, for example JsonEncoder like this:
1336+
1337+
```java
1338+
SomeApi github = Feign.builder()
1339+
.encoder(new FormEncoder(new JacksonEncoder()))
1340+
.target(SomeApi.class, "http://api.some.org");
1341+
```
1342+
1343+
And use them together:
1344+
1345+
```java
1346+
interface SomeApi {
1347+
1348+
@RequestLine("POST /json")
1349+
@Headers("Content-Type: application/json")
1350+
void json (Dto dto);
1351+
1352+
@RequestLine("POST /form")
1353+
@Headers("Content-Type: application/x-www-form-urlencoded")
1354+
void from (@Param("field1") String field1, @Param("field2") String[] values);
1355+
}
1356+
```
1357+
1358+
You can specify two types of encoding forms by `Content-Type` header.
1359+
1360+
### application/x-www-form-urlencoded
1361+
1362+
```java
1363+
interface SomeApi {
1364+
1365+
@RequestLine("POST /authorization")
1366+
@Headers("Content-Type: application/x-www-form-urlencoded")
1367+
void authorization (@Param("email") String email, @Param("password") String password);
1368+
1369+
// Group all parameters within a POJO
1370+
@RequestLine("POST /user")
1371+
@Headers("Content-Type: application/x-www-form-urlencoded")
1372+
void addUser (User user);
1373+
1374+
class User {
1375+
1376+
Integer id;
1377+
1378+
String name;
1379+
}
1380+
}
1381+
```
1382+
1383+
### multipart/form-data
1384+
1385+
```java
1386+
interface SomeApi {
1387+
1388+
// File parameter
1389+
@RequestLine("POST /send_photo")
1390+
@Headers("Content-Type: multipart/form-data")
1391+
void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") File photo);
1392+
1393+
// byte[] parameter
1394+
@RequestLine("POST /send_photo")
1395+
@Headers("Content-Type: multipart/form-data")
1396+
void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") byte[] photo);
1397+
1398+
// FormData parameter
1399+
@RequestLine("POST /send_photo")
1400+
@Headers("Content-Type: multipart/form-data")
1401+
void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") FormData photo);
1402+
1403+
// Group all parameters within a POJO
1404+
@RequestLine("POST /send_photo")
1405+
@Headers("Content-Type: multipart/form-data")
1406+
void sendPhoto (MyPojo pojo);
1407+
1408+
class MyPojo {
1409+
1410+
@FormProperty("is_public")
1411+
Boolean isPublic;
1412+
1413+
File photo;
1414+
}
1415+
}
1416+
```
1417+
1418+
In the example above, the `sendPhoto` method uses the `photo` parameter using three different supported types.
1419+
1420+
* `File` will use the File's extension to detect the `Content-Type`;
1421+
* `byte[]` will use `application/octet-stream` as `Content-Type`;
1422+
* `FormData` will use the `FormData`'s `Content-Type` and `fileName`;
1423+
* Client's custom POJO for grouping parameters (including types above).
1424+
1425+
`FormData` is custom object that wraps a `byte[]` and defines a `Content-Type` and `fileName` like this:
1426+
1427+
```java
1428+
FormData formData = new FormData("image/png", "filename.png", myDataAsByteArray);
1429+
someApi.sendPhoto(true, formData);
1430+
```
1431+
1432+
### Spring MultipartFile and Spring Cloud Netflix @FeignClient support
1433+
1434+
You can also use Form Encoder with Spring `MultipartFile` and `@FeignClient`.
1435+
1436+
Include the dependencies to your project's pom.xml file:
1437+
1438+
```xml
1439+
<dependencies>
1440+
<dependency>
1441+
<groupId>io.github.openfeign.form</groupId>
1442+
<artifactId>feign-form</artifactId>
1443+
<version>4.0.0</version>
1444+
</dependency>
1445+
<dependency>
1446+
<groupId>io.github.openfeign.form</groupId>
1447+
<artifactId>feign-form-spring</artifactId>
1448+
<version>4.0.0</version>
1449+
</dependency>
1450+
</dependencies>
1451+
```
1452+
1453+
```java
1454+
@FeignClient(
1455+
name = "file-upload-service",
1456+
configuration = FileUploadServiceClient.MultipartSupportConfig.class
1457+
)
1458+
public interface FileUploadServiceClient extends IFileUploadServiceClient {
1459+
1460+
public class MultipartSupportConfig {
1461+
1462+
@Autowired
1463+
private ObjectFactory<HttpMessageConverters> messageConverters;
1464+
1465+
@Bean
1466+
public Encoder feignFormEncoder () {
1467+
return new SpringFormEncoder(new SpringEncoder(messageConverters));
1468+
}
1469+
}
1470+
}
1471+
```
1472+
1473+
Or, if you don't need Spring's standard encoder:
1474+
1475+
```java
1476+
@FeignClient(
1477+
name = "file-upload-service",
1478+
configuration = FileUploadServiceClient.MultipartSupportConfig.class
1479+
)
1480+
public interface FileUploadServiceClient extends IFileUploadServiceClient {
1481+
1482+
public class MultipartSupportConfig {
1483+
1484+
@Bean
1485+
public Encoder feignFormEncoder () {
1486+
return new SpringFormEncoder();
1487+
}
1488+
}
1489+
}
1490+
```
1491+
1492+
Thanks to [tf-haotri-pham](https://github.com/tf-haotri-pham) for his feature, which makes use of Apache commons-fileupload library, which handles the parsing of the multipart response. The body data parts are held as byte arrays in memory.
1493+
1494+
To use this feature, include SpringManyMultipartFilesReader in the list of message converters for the Decoder and have the Feign client return an array of MultipartFile:
1495+
1496+
```java
1497+
@FeignClient(
1498+
name = "${feign.name}",
1499+
url = "${feign.url}"
1500+
configuration = DownloadClient.ClientConfiguration.class
1501+
)
1502+
public interface DownloadClient {
1503+
1504+
@RequestMapping("/multipart/download/{fileId}")
1505+
MultipartFile[] download(@PathVariable("fileId") String fileId);
1506+
1507+
class ClientConfiguration {
1508+
1509+
@Autowired
1510+
private ObjectFactory<HttpMessageConverters> messageConverters;
1511+
1512+
@Bean
1513+
public Decoder feignDecoder () {
1514+
List<HttpMessageConverter<?>> springConverters =
1515+
messageConverters.getObject().getConverters();
1516+
1517+
List<HttpMessageConverter<?>> decoderConverters =
1518+
new ArrayList<HttpMessageConverter<?>>(springConverters.size() + 1);
1519+
1520+
decoderConverters.addAll(springConverters);
1521+
decoderConverters.add(new SpringManyMultipartFilesReader(4096));
1522+
1523+
HttpMessageConverters httpMessageConverters = new HttpMessageConverters(decoderConverters);
1524+
1525+
return new SpringDecoder(new ObjectFactory<HttpMessageConverters>() {
1526+
1527+
@Override
1528+
public HttpMessageConverters getObject() {
1529+
return httpMessageConverters;
1530+
}
1531+
});
1532+
}
1533+
}
1534+
}
1535+
```

core/src/test/java/feign/template/UriUtilsTest.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 The Feign Authors
2+
* Copyright 2012-2024 The Feign Authors
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
55
* in compliance with the License. You may obtain a copy of the License at
@@ -14,9 +14,15 @@
1414
package feign.template;
1515

1616

17+
import feign.Param;
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.Arguments;
21+
import org.junit.jupiter.params.provider.MethodSource;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.stream.Stream;
1724
import static java.nio.charset.StandardCharsets.UTF_8;
1825
import static org.assertj.core.api.Assertions.assertThat;
19-
import org.junit.jupiter.api.Test;
2026

2127
class UriUtilsTest {
2228

@@ -41,4 +47,26 @@ void pctEncodeWithReservedCharacters() {
4147
String encoded = UriUtils.encode(withReserved, UTF_8, true);
4248
assertThat(encoded).isEqualTo("/api/user@host:port#section[a-z]/data");
4349
}
50+
51+
@ParameterizedTest
52+
@MethodSource("provideValuesToEncode")
53+
void testVariousEncodingScenarios(String input, String expected) {
54+
assertThat(UriUtils.encode(input, UTF_8)).isEqualTo(expected);
55+
}
56+
57+
private static Stream<Arguments> provideValuesToEncode() {
58+
return Stream.of(
59+
Arguments.of("foo", "foo"),
60+
Arguments.of("foo bar", "foo%20bar"),
61+
Arguments.of("foo%20bar", "foo%20bar"),
62+
Arguments.of("foo%2520bar", "foo%2520bar"),
63+
Arguments.of("foo&bar", "foo%26bar"),
64+
Arguments.of("foo& bar", "foo%26%20bar"),
65+
Arguments.of("foo = bar", "foo%20%3D%20bar"),
66+
Arguments.of("foo ", "foo%20%20%20"),
67+
Arguments.of("foo/bar", "foo%2Fbar"),
68+
Arguments.of("foo!\"/$%?& *( ) _%20^¨ >`:É. ',. é ;`^ ¸< nasty stuff here!",
69+
"foo%21%22%2F%24%25%3F%26%20%20%20%2A%28%20%29%20%20_%2520%5E%C2%A8%20%20%3E%60%3A%C3%89.%20%20%20%27%2C.%20%20%C3%A9%20%3B%60%5E%20%C2%B8%3C%20nasty%20stuff%20here%21"));
70+
71+
}
4472
}

0 commit comments

Comments
 (0)