Skip to content

Commit 1770f0b

Browse files
committed
Merge remote-tracking branch 'origin/main'
# Conflicts: # spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.java
2 parents 560239a + 4575b41 commit 1770f0b

File tree

8 files changed

+138
-13
lines changed

8 files changed

+138
-13
lines changed

docs/modules/ROOT/pages/spring-cloud-openfeign.adoc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,12 @@ public class CustomConfiguration {
288288
}
289289
----
290290

291-
TIP: By default, Feign clients do not encode slash `/` characters. You can change this behaviour, by setting the value of `spring.cloud.openfeign.client.decodeSlash` to `false`.
291+
TIP: By default, Feign clients do not encode slash `/` characters. You can change this behaviour, by setting the value of `spring.cloud.openfeign.client.decode-slash` to `false`.
292+
293+
294+
TIP: By default, Feign clients do not remove trailing slash `/` characters from the request path.
295+
You can change this behaviour, by setting the value of `spring.cloud.openfeign.client.remove-trailing-slash` to `true`.
296+
Trailing slash removal from the request path is going to be made the default behaviour in the next major release.
292297

293298
[[springencoder-configuration]]
294299
==== `SpringEncoder` configuration

docs/modules/ROOT/partials/_configprops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
|spring.cloud.openfeign.client.default-config | `+++default+++` |
7474
|spring.cloud.openfeign.client.default-to-properties | `+++true+++` |
7575
|spring.cloud.openfeign.client.refresh-enabled | `+++false+++` | Enables options value refresh capability for Feign.
76+
|spring.cloud.openfeign.client.remove-trailing-slash | `+++false+++` | If {@code true}, trailing slashes at the end of request urls will be removed.
7677
|spring.cloud.openfeign.compression.request.content-encoding-types | | The list of content encodings (applicable encodings depend on the used client).
7778
|spring.cloud.openfeign.compression.request.enabled | `+++false+++` | Enables the request sent by Feign to be compressed.
7879
|spring.cloud.openfeign.compression.request.mime-types | `+++[text/xml, application/xml, application/json]+++` | The list of supported mime types.

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2023 the original author or authors.
2+
* Copyright 2013-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -61,6 +61,11 @@ public class FeignClientProperties {
6161
*/
6262
private boolean decodeSlash = true;
6363

64+
/**
65+
* If {@code true}, trailing slashes at the end of request urls will be removed.
66+
*/
67+
private boolean removeTrailingSlash;
68+
6469
public boolean isDefaultToProperties() {
6570
return defaultToProperties;
6671
}
@@ -93,6 +98,14 @@ public void setDecodeSlash(boolean decodeSlash) {
9398
this.decodeSlash = decodeSlash;
9499
}
95100

101+
public boolean isRemoveTrailingSlash() {
102+
return removeTrailingSlash;
103+
}
104+
105+
public void setRemoveTrailingSlash(boolean removeTrailingSlash) {
106+
this.removeTrailingSlash = removeTrailingSlash;
107+
}
108+
96109
@Override
97110
public boolean equals(Object o) {
98111
if (this == o) {
@@ -103,12 +116,13 @@ public boolean equals(Object o) {
103116
}
104117
FeignClientProperties that = (FeignClientProperties) o;
105118
return defaultToProperties == that.defaultToProperties && Objects.equals(defaultConfig, that.defaultConfig)
106-
&& Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash);
119+
&& Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash)
120+
&& Objects.equals(removeTrailingSlash, that.removeTrailingSlash);
107121
}
108122

109123
@Override
110124
public int hashCode() {
111-
return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash);
125+
return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash, removeTrailingSlash);
112126
}
113127

114128
/**

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2022 the original author or authors.
2+
* Copyright 2013-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -145,8 +145,7 @@ public QueryMapEncoder feignQueryMapEncoderPageable() {
145145
@Bean
146146
@ConditionalOnMissingBean
147147
public Contract feignContract(ConversionService feignConversionService) {
148-
boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
149-
return new SpringMvcContract(parameterProcessors, feignConversionService, decodeSlash);
148+
return new SpringMvcContract(parameterProcessors, feignConversionService, feignClientProperties);
150149
}
151150

152151
@Bean

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
4343
import org.springframework.cloud.openfeign.CollectionFormat;
44+
import org.springframework.cloud.openfeign.FeignClientProperties;
4445
import org.springframework.cloud.openfeign.SpringQueryMap;
4546
import org.springframework.cloud.openfeign.annotation.CookieValueParameterProcessor;
4647
import org.springframework.cloud.openfeign.annotation.MatrixVariableParameterProcessor;
@@ -115,6 +116,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
115116

116117
private final boolean decodeSlash;
117118

119+
private final boolean removeTrailingSlash;
120+
118121
public SpringMvcContract() {
119122
this(Collections.emptyList());
120123
}
@@ -128,8 +131,36 @@ public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterPro
128131
this(annotatedParameterProcessors, conversionService, true);
129132
}
130133

134+
/**
135+
* Creates a {@link SpringMvcContract} based on annotatedParameterProcessors,
136+
* conversionService and decodeSlash value.
137+
* @param annotatedParameterProcessors list of {@link AnnotatedParameterProcessor}
138+
* objects used to resolve parameters
139+
* @param conversionService {@link ConversionService} used for type conversion
140+
* @param decodeSlash indicates whether slashes should be decoded
141+
* @deprecated in favour of
142+
* {@link SpringMvcContract#SpringMvcContract(List, ConversionService, FeignClientProperties)}
143+
*/
144+
@Deprecated
131145
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
132146
ConversionService conversionService, boolean decodeSlash) {
147+
this(annotatedParameterProcessors, conversionService, decodeSlash, false);
148+
}
149+
150+
/**
151+
* Creates a {@link SpringMvcContract} based on annotatedParameterProcessors,
152+
* conversionService and decodeSlash value.
153+
* @param annotatedParameterProcessors list of {@link AnnotatedParameterProcessor}
154+
* objects used to resolve parameters
155+
* @param conversionService {@link ConversionService} used for type conversion
156+
* @param decodeSlash indicates whether slashes should be decoded
157+
* @param removeTrailingSlash indicates whether trailing slashes should be removed
158+
* @deprecated in favour of
159+
* {@link SpringMvcContract#SpringMvcContract(List, ConversionService, FeignClientProperties)}
160+
*/
161+
@Deprecated
162+
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
163+
ConversionService conversionService, boolean decodeSlash, boolean removeTrailingSlash) {
133164
Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null.");
134165
Assert.notNull(conversionService, "ConversionService can not be null.");
135166

@@ -140,6 +171,14 @@ public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterPro
140171
this.conversionService = conversionService;
141172
convertingExpanderFactory = new ConvertingExpanderFactory(conversionService);
142173
this.decodeSlash = decodeSlash;
174+
this.removeTrailingSlash = removeTrailingSlash;
175+
}
176+
177+
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
178+
ConversionService conversionService, FeignClientProperties feignClientProperties) {
179+
this(annotatedParameterProcessors, conversionService,
180+
feignClientProperties == null || feignClientProperties.isDecodeSlash(),
181+
feignClientProperties != null && feignClientProperties.isRemoveTrailingSlash());
143182
}
144183

145184
private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) {
@@ -229,6 +268,9 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA
229268
if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
230269
pathValue = "/" + pathValue;
231270
}
271+
if (removeTrailingSlash && pathValue.endsWith("/")) {
272+
pathValue = pathValue.substring(0, pathValue.length() - 1);
273+
}
232274
data.template().uri(pathValue, true);
233275
if (data.template().decodeSlash() != decodeSlash) {
234276
data.template().decodeSlash(decodeSlash);

spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2022 the original author or authors.
2+
* Copyright 2013-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -90,7 +90,7 @@ private String testGetName(String name) {
9090
}
9191

9292
@Test
93-
void removeLastSlashOfUrl() {
93+
void testRemoveTrailingSlashFromUrl() {
9494
String url = FeignClientsRegistrar.getUrl("http://localhost/");
9595
assertThat(url).isEqualTo("http://localhost");
9696
}

spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.junit.jupiter.api.Test;
3737

3838
import org.springframework.cloud.openfeign.CollectionFormat;
39+
import org.springframework.cloud.openfeign.FeignClientProperties;
3940
import org.springframework.cloud.openfeign.SpringQueryMap;
4041
import org.springframework.core.convert.ConversionService;
4142
import org.springframework.data.domain.Page;
@@ -189,8 +190,23 @@ void testProcessAnnotations_SimpleNoPath() throws Exception {
189190
}
190191

191192
@Test
192-
void testProcessAnnotations_SimplePathIsOnlyASlash() throws Exception {
193-
Method method = TestTemplate_Simple.class.getDeclaredMethod("getSlashPath", String.class);
193+
void testProcessAnnotations_SimplePathIsOnlyASlashWithParam() throws Exception {
194+
Method method = TestTemplate_Simple.class.getDeclaredMethod("getSlashPathWithParam", String.class);
195+
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
196+
197+
assertThat(data.template().url()).isEqualTo("/?id=" + "{id}");
198+
assertThat(data.template().method()).isEqualTo("GET");
199+
assertThat(data.template().headers().get("Accept").iterator().next())
200+
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
201+
}
202+
203+
@Test
204+
void testProcessAnnotations_SimplePathIsOnlyASlashWithParamWithTrailingSlashRemoval() throws Exception {
205+
FeignClientProperties properties = new FeignClientProperties();
206+
properties.setRemoveTrailingSlash(true);
207+
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), properties);
208+
Method method = TestTemplate_Simple.class.getDeclaredMethod("getSlashPathWithParam", String.class);
209+
194210
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
195211

196212
assertThat(data.template().url()).isEqualTo("/?id=" + "{id}");
@@ -284,6 +300,48 @@ void testProcessAnnotations_SimplePostMapping() throws Exception {
284300

285301
}
286302

303+
@Test
304+
void testProcessAnnotations_SimplePathIsOnlyASlashWithTrailingSlashRemoval() throws Exception {
305+
FeignClientProperties properties = new FeignClientProperties();
306+
properties.setRemoveTrailingSlash(true);
307+
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), properties);
308+
Method method = TestTemplate_Simple.class.getDeclaredMethod("getSlashPath");
309+
310+
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
311+
312+
assertThat(data.template().url()).isEqualTo("/");
313+
assertThat(data.template().method()).isEqualTo("GET");
314+
assertThat(data.template().headers().get("Accept").iterator().next())
315+
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
316+
}
317+
318+
@Test
319+
void testProcessAnnotations_SimplePathHasTrailingSlash() throws Exception {
320+
Method method = TestTemplate_Simple.class.getDeclaredMethod("getTrailingSlash");
321+
322+
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
323+
324+
assertThat(data.template().url()).isEqualTo("/test1/test2/");
325+
assertThat(data.template().method()).isEqualTo("GET");
326+
assertThat(data.template().headers().get("Accept").iterator().next())
327+
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
328+
}
329+
330+
@Test
331+
void testProcessAnnotations_SimplePathHasTrailingSlashWithTrailingSlashRemoval() throws Exception {
332+
FeignClientProperties properties = new FeignClientProperties();
333+
properties.setRemoveTrailingSlash(true);
334+
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), properties);
335+
Method method = TestTemplate_Simple.class.getDeclaredMethod("getTrailingSlash");
336+
337+
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
338+
339+
assertThat(data.template().url()).isEqualTo("/test1/test2");
340+
assertThat(data.template().method()).isEqualTo("GET");
341+
assertThat(data.template().headers().get("Accept").iterator().next())
342+
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
343+
}
344+
287345
@Test
288346
void testProcessAnnotationsOnMethod_Advanced() throws Exception {
289347
Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest", String.class, String.class,
@@ -738,7 +796,13 @@ public interface TestTemplate_Simple {
738796
TestObject postMappingTest(@RequestBody TestObject object);
739797

740798
@GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
741-
ResponseEntity<TestObject> getSlashPath(@RequestParam("id") String id);
799+
ResponseEntity<TestObject> getSlashPathWithParam(@RequestParam("id") String id);
800+
801+
@GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
802+
ResponseEntity<TestObject> getSlashPath();
803+
804+
@GetMapping(value = "test1/test2/", produces = MediaType.APPLICATION_JSON_VALUE)
805+
ResponseEntity<TestObject> getTrailingSlash();
742806

743807
@GetMapping(path = "test", produces = MediaType.APPLICATION_JSON_VALUE)
744808
ResponseEntity<TestObject> getTestNoLeadingSlash(@RequestParam("name") String name);

spring-cloud-openfeign-dependencies/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<name>spring-cloud-openfeign-dependencies</name>
1616
<description>Spring Cloud OpenFeign Dependencies</description>
1717
<properties>
18-
<feign.version>13.4</feign.version>
18+
<feign.version>13.5</feign.version>
1919
<feign-form.version>3.8.0</feign-form.version>
2020
</properties>
2121
<dependencyManagement>

0 commit comments

Comments
 (0)