Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -61,6 +61,12 @@ public class FeignClientProperties {
*/
private boolean decodeSlash = true;

/**
* If {@code true}, trailing slashes at the end
* of request urls will be removed.
*/
private boolean removeTrailingSlash;

public boolean isDefaultToProperties() {
return defaultToProperties;
}
Expand Down Expand Up @@ -93,6 +99,14 @@ public void setDecodeSlash(boolean decodeSlash) {
this.decodeSlash = decodeSlash;
}

public boolean isRemoveTrailingSlash() {
return removeTrailingSlash;
}

public void setRemoveTrailingSlash(boolean removeTrailingSlash) {
this.removeTrailingSlash = removeTrailingSlash;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -103,12 +117,13 @@ public boolean equals(Object o) {
}
FeignClientProperties that = (FeignClientProperties) o;
return defaultToProperties == that.defaultToProperties && Objects.equals(defaultConfig, that.defaultConfig)
&& Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash);
&& Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash)
&& Objects.equals(removeTrailingSlash, that.removeTrailingSlash);
}

@Override
public int hashCode() {
return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash);
return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash, removeTrailingSlash);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 the original author or authors.
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -145,8 +145,7 @@ public QueryMapEncoder feignQueryMapEncoderPageable() {
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
return new SpringMvcContract(parameterProcessors, feignConversionService, decodeSlash);
return new SpringMvcContract(parameterProcessors, feignConversionService, feignClientProperties);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ static String getUrl(String url) {
if (!url.contains("://")) {
url = "http://" + url;
}
if (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
try {
new URL(url);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
import org.springframework.cloud.openfeign.CollectionFormat;
import org.springframework.cloud.openfeign.FeignClientProperties;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.cloud.openfeign.annotation.CookieValueParameterProcessor;
import org.springframework.cloud.openfeign.annotation.MatrixVariableParameterProcessor;
Expand Down Expand Up @@ -115,6 +116,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource

private final boolean decodeSlash;

private final boolean removeTrailingSlash;

public SpringMvcContract() {
this(Collections.emptyList());
}
Expand All @@ -128,8 +131,32 @@ public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterPro
this(annotatedParameterProcessors, conversionService, true);
}

/**
* Creates a {@link SpringMvcContract} based on annotatedParameterProcessors,
* conversionService and decodeSlash value.
* @param annotatedParameterProcessors list of {@link AnnotatedParameterProcessor} objects used to resolve parameters
* @param conversionService {@link ConversionService} used for type conversion
* @param decodeSlash indicates whether slashes should be decoded
* @deprecated in favour of {@link SpringMvcContract#SpringMvcContract(List, ConversionService, FeignClientProperties)}
*/
@Deprecated
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
ConversionService conversionService, boolean decodeSlash) {
this(annotatedParameterProcessors, conversionService, decodeSlash, false);
}

/**
* Creates a {@link SpringMvcContract} based on annotatedParameterProcessors,
* conversionService and decodeSlash value.
* @param annotatedParameterProcessors list of {@link AnnotatedParameterProcessor} objects used to resolve parameters
* @param conversionService {@link ConversionService} used for type conversion
* @param decodeSlash indicates whether slashes should be decoded
* @param removeTrailingSlash indicates whether trailing slashes should be removed
* @deprecated in favour of {@link SpringMvcContract#SpringMvcContract(List, ConversionService, FeignClientProperties)}
*/
@Deprecated
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
ConversionService conversionService, boolean decodeSlash, boolean removeTrailingSlash) {
Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null.");
Assert.notNull(conversionService, "ConversionService can not be null.");

Expand All @@ -140,6 +167,14 @@ public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterPro
this.conversionService = conversionService;
convertingExpanderFactory = new ConvertingExpanderFactory(conversionService);
this.decodeSlash = decodeSlash;
this.removeTrailingSlash = removeTrailingSlash;
}

public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
ConversionService conversionService, FeignClientProperties feignClientProperties) {
this(annotatedParameterProcessors, conversionService,
feignClientProperties == null || feignClientProperties.isDecodeSlash(),
feignClientProperties != null && feignClientProperties.isRemoveTrailingSlash());
}

private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) {
Expand Down Expand Up @@ -229,6 +264,9 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA
if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
pathValue = "/" + pathValue;
}
if (removeTrailingSlash && pathValue.endsWith("/")) {
pathValue = pathValue.substring(0, pathValue.length() - 1);
}
data.template().uri(pathValue, true);
if (data.template().decodeSlash() != decodeSlash) {
data.template().decodeSlash(decodeSlash);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ void forType_allFieldsSetOnBuilder() {
assertFactoryBeanField(builder, "contextId", "TestContext");

// and:
assertFactoryBeanField(builder, "url", "http://Url/");
assertFactoryBeanField(builder, "url", "http://Url");
assertFactoryBeanField(builder, "path", "/Path");
assertFactoryBeanField(builder, "dismiss404", true);

Expand All @@ -155,7 +155,7 @@ void forType_clientFactoryBeanProvided() {
assertFactoryBeanField(builder, "contextId", "TestContext");

// and:
assertFactoryBeanField(builder, "url", "http://Url/");
assertFactoryBeanField(builder, "url", "http://Url");
assertFactoryBeanField(builder, "path", "/Path");
assertFactoryBeanField(builder, "dismiss404", true);
List<FeignBuilderCustomizer> additionalCustomizers = getFactoryBeanField(builder, "additionalCustomizers");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 the original author or authors.
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -89,6 +89,12 @@ private String testGetName(String name) {
return registrar.getName(Collections.singletonMap("name", name));
}

@Test
void testRemoveTrailingSlashFromUrl() {
String url = FeignClientsRegistrar.getUrl("http://localhost/");
assertThat(url).isEqualTo("http://localhost");
}

@Test
void testFallback() {
assertThatExceptionOfType(IllegalArgumentException.class)
Expand Down