From 8204055924fbcd6cbb6a5be55cebe150f830aa93 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 22 May 2020 06:45:38 +0100 Subject: [PATCH 1/5] MockServerHttpRequest accepts custom HTTP method Closes gh-25109 --- .../reactive/MockServerHttpRequest.java | 100 ++++++++++++------ .../reactive/MockServerHttpRequest.java | 100 ++++++++++++------ 2 files changed, 140 insertions(+), 60 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java index 66c741eb6b9..356367c55d6 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -40,6 +40,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.AbstractServerHttpRequest; import org.springframework.http.server.reactive.SslInfo; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MimeType; import org.springframework.util.MultiValueMap; @@ -54,8 +55,12 @@ import org.springframework.web.util.UriComponentsBuilder; */ public final class MockServerHttpRequest extends AbstractServerHttpRequest { + @Nullable private final HttpMethod httpMethod; + @Nullable + private final String customHttpMethod; + private final MultiValueMap cookies; @Nullable @@ -70,13 +75,15 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { private final Flux body; - private MockServerHttpRequest(HttpMethod httpMethod, URI uri, @Nullable String contextPath, - HttpHeaders headers, MultiValueMap cookies, + private MockServerHttpRequest(@Nullable HttpMethod httpMethod, @Nullable String customHttpMethod, + URI uri, @Nullable String contextPath, HttpHeaders headers, MultiValueMap cookies, @Nullable InetSocketAddress remoteAddress, @Nullable InetSocketAddress localAddress, @Nullable SslInfo sslInfo, Publisher body) { super(uri, contextPath, headers); + Assert.isTrue(httpMethod != null || customHttpMethod != null, "HTTP method must not be null"); this.httpMethod = httpMethod; + this.customHttpMethod = customHttpMethod; this.cookies = cookies; this.remoteAddress = remoteAddress; this.localAddress = localAddress; @@ -91,8 +98,9 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { } @Override + @SuppressWarnings("ConstantConditions") public String getMethodValue() { - return this.httpMethod.name(); + return (this.httpMethod != null ? this.httpMethod.name() : this.customHttpMethod); } @Override @@ -131,30 +139,6 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { // Static builder methods - /** - * Create a builder with the given HTTP method and a {@link URI}. - * @param method the HTTP method (GET, POST, etc) - * @param url the URL - * @return the created builder - */ - public static BodyBuilder method(HttpMethod method, URI url) { - return new DefaultBodyBuilder(method, url); - } - - /** - * Alternative to {@link #method(HttpMethod, URI)} that accepts a URI template. - * The given URI may contain query parameters, or those may be added later via - * {@link BaseBuilder#queryParam queryParam} builder methods. - * @param method the HTTP method (GET, POST, etc) - * @param urlTemplate the URL template - * @param vars variables to expand into the template - * @return the created builder - */ - public static BodyBuilder method(HttpMethod method, String urlTemplate, Object... vars) { - URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri(); - return new DefaultBodyBuilder(method, url); - } - /** * Create an HTTP GET builder with the given URI template. The given URI may * contain query parameters, or those may be added later via @@ -228,6 +212,44 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { return method(HttpMethod.OPTIONS, urlTemplate, uriVars); } + /** + * Create a builder with the given HTTP method and a {@link URI}. + * @param method the HTTP method (GET, POST, etc) + * @param url the URL + * @return the created builder + */ + public static BodyBuilder method(HttpMethod method, URI url) { + return new DefaultBodyBuilder(method, url); + } + + /** + * Alternative to {@link #method(HttpMethod, URI)} that accepts a URI template. + * The given URI may contain query parameters, or those may be added later via + * {@link BaseBuilder#queryParam queryParam} builder methods. + * @param method the HTTP method (GET, POST, etc) + * @param urlTemplate the URL template + * @param vars variables to expand into the template + * @return the created builder + */ + public static BodyBuilder method(HttpMethod method, String urlTemplate, Object... vars) { + URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri(); + return new DefaultBodyBuilder(method, url); + } + + /** + * Create a builder with a raw HTTP method value that is outside the range + * of {@link HttpMethod} enum values. + * @param method the HTTP method value + * @param urlTemplate the URL template + * @param vars variables to expand into the template + * @return the created builder + * @since 5.2.7 + */ + public static BodyBuilder method(String method, String urlTemplate, Object... vars) { + URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri(); + return new DefaultBodyBuilder(method, url); + } + /** * Request builder exposing properties not related to the body. @@ -408,8 +430,12 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { private static final DataBufferFactory BUFFER_FACTORY = new DefaultDataBufferFactory(); + @Nullable private final HttpMethod method; + @Nullable + private final String customMethod; + private final URI url; @Nullable @@ -431,8 +457,22 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { private SslInfo sslInfo; - public DefaultBodyBuilder(HttpMethod method, URI url) { + DefaultBodyBuilder(HttpMethod method, URI url) { this.method = method; + this.customMethod = null; + this.url = url; + } + + DefaultBodyBuilder(String method, URI url) { + HttpMethod resolved = HttpMethod.resolve(method); + if (resolved != null) { + this.method = resolved; + this.customMethod = null; + } + else { + this.method = null; + this.customMethod = method; + } this.url = url; } @@ -569,7 +609,7 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { @Override public MockServerHttpRequest body(Publisher body) { applyCookiesIfNecessary(); - return new MockServerHttpRequest(this.method, getUrlToUse(), this.contextPath, + return new MockServerHttpRequest(this.method, this.customMethod, getUrlToUse(), this.contextPath, this.headers, this.cookies, this.remoteAddress, this.localAddress, this.sslInfo, body); } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java index 0f41a41d21d..96708ef2fb5 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -40,6 +40,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.AbstractServerHttpRequest; import org.springframework.http.server.reactive.SslInfo; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MimeType; import org.springframework.util.MultiValueMap; @@ -54,8 +55,12 @@ import org.springframework.web.util.UriComponentsBuilder; */ public final class MockServerHttpRequest extends AbstractServerHttpRequest { + @Nullable private final HttpMethod httpMethod; + @Nullable + private final String customHttpMethod; + private final MultiValueMap cookies; @Nullable @@ -70,13 +75,15 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { private final Flux body; - private MockServerHttpRequest(HttpMethod httpMethod, URI uri, @Nullable String contextPath, - HttpHeaders headers, MultiValueMap cookies, + private MockServerHttpRequest(@Nullable HttpMethod httpMethod, @Nullable String customHttpMethod, + URI uri, @Nullable String contextPath, HttpHeaders headers, MultiValueMap cookies, @Nullable InetSocketAddress remoteAddress, @Nullable InetSocketAddress localAddress, @Nullable SslInfo sslInfo, Publisher body) { super(uri, contextPath, headers); + Assert.isTrue(httpMethod != null || customHttpMethod != null, "HTTP method must not be null"); this.httpMethod = httpMethod; + this.customHttpMethod = customHttpMethod; this.cookies = cookies; this.remoteAddress = remoteAddress; this.localAddress = localAddress; @@ -91,8 +98,9 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { } @Override + @SuppressWarnings("ConstantConditions") public String getMethodValue() { - return this.httpMethod.name(); + return (this.httpMethod != null ? this.httpMethod.name() : this.customHttpMethod); } @Override @@ -131,30 +139,6 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { // Static builder methods - /** - * Create a builder with the given HTTP method and a {@link URI}. - * @param method the HTTP method (GET, POST, etc) - * @param url the URL - * @return the created builder - */ - public static BodyBuilder method(HttpMethod method, URI url) { - return new DefaultBodyBuilder(method, url); - } - - /** - * Alternative to {@link #method(HttpMethod, URI)} that accepts a URI template. - * The given URI may contain query parameters, or those may be added later via - * {@link BaseBuilder#queryParam queryParam} builder methods. - * @param method the HTTP method (GET, POST, etc) - * @param urlTemplate the URL template - * @param vars variables to expand into the template - * @return the created builder - */ - public static BodyBuilder method(HttpMethod method, String urlTemplate, Object... vars) { - URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri(); - return new DefaultBodyBuilder(method, url); - } - /** * Create an HTTP GET builder with the given URI template. The given URI may * contain query parameters, or those may be added later via @@ -228,6 +212,44 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { return method(HttpMethod.OPTIONS, urlTemplate, uriVars); } + /** + * Create a builder with the given HTTP method and a {@link URI}. + * @param method the HTTP method (GET, POST, etc) + * @param url the URL + * @return the created builder + */ + public static BodyBuilder method(HttpMethod method, URI url) { + return new DefaultBodyBuilder(method, url); + } + + /** + * Alternative to {@link #method(HttpMethod, URI)} that accepts a URI template. + * The given URI may contain query parameters, or those may be added later via + * {@link BaseBuilder#queryParam queryParam} builder methods. + * @param method the HTTP method (GET, POST, etc) + * @param urlTemplate the URL template + * @param vars variables to expand into the template + * @return the created builder + */ + public static BodyBuilder method(HttpMethod method, String urlTemplate, Object... vars) { + URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri(); + return new DefaultBodyBuilder(method, url); + } + + /** + * Create a builder with a raw HTTP method value that is outside the range + * of {@link HttpMethod} enum values. + * @param method the HTTP method value + * @param urlTemplate the URL template + * @param vars variables to expand into the template + * @return the created builder + * @since 5.2.7 + */ + public static BodyBuilder method(String method, String urlTemplate, Object... vars) { + URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri(); + return new DefaultBodyBuilder(method, url); + } + /** * Request builder exposing properties not related to the body. @@ -408,8 +430,12 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { private static final DataBufferFactory BUFFER_FACTORY = new DefaultDataBufferFactory(); + @Nullable private final HttpMethod method; + @Nullable + private final String customMethod; + private final URI url; @Nullable @@ -431,8 +457,22 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { private SslInfo sslInfo; - public DefaultBodyBuilder(HttpMethod method, URI url) { + DefaultBodyBuilder(HttpMethod method, URI url) { this.method = method; + this.customMethod = null; + this.url = url; + } + + DefaultBodyBuilder(String method, URI url) { + HttpMethod resolved = HttpMethod.resolve(method); + if (resolved != null) { + this.method = resolved; + this.customMethod = null; + } + else { + this.method = null; + this.customMethod = method; + } this.url = url; } @@ -569,7 +609,7 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { @Override public MockServerHttpRequest body(Publisher body) { applyCookiesIfNecessary(); - return new MockServerHttpRequest(this.method, getUrlToUse(), this.contextPath, + return new MockServerHttpRequest(this.method, this.customMethod, getUrlToUse(), this.contextPath, this.headers, this.cookies, this.remoteAddress, this.localAddress, this.sslInfo, body); } From d7a29bbcef7d278a3ca9fd66ceb70c60fb283cc0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 22 May 2020 07:32:16 +0100 Subject: [PATCH 2/5] DefaultClientResponseBuilder copies logPrefix Closes gh-25069 --- .../reactive/MockClientHttpRequest.java | 8 +++++- .../reactive/MockClientHttpResponse.java | 2 +- .../client/DefaultClientResponseBuilder.java | 18 +++++++------ .../DefaultClientResponseBuilderTests.java | 25 +++++++++++++------ 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java index 1fdd0edcd3b..f21c01c195d 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java @@ -33,6 +33,7 @@ import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; import org.springframework.http.client.reactive.AbstractClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.util.Assert; @@ -46,7 +47,7 @@ import org.springframework.web.util.UriComponentsBuilder; * @author Rossen Stoyanchev * @since 5.0 */ -public class MockClientHttpRequest extends AbstractClientHttpRequest { +public class MockClientHttpRequest extends AbstractClientHttpRequest implements HttpRequest { private final HttpMethod httpMethod; @@ -96,6 +97,11 @@ public class MockClientHttpRequest extends AbstractClientHttpRequest { return this.httpMethod; } + @Override + public String getMethodValue() { + return this.httpMethod.name(); + } + @Override public URI getURI() { return this.url; diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpResponse.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpResponse.java index 7ee7e6afb95..e5f014e8712 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpResponse.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpResponse.java @@ -83,7 +83,7 @@ public class MockClientHttpResponse implements ClientHttpResponse { public HttpHeaders getHeaders() { if (!getCookies().isEmpty() && this.headers.get(HttpHeaders.SET_COOKIE) == null) { getCookies().values().stream().flatMap(Collection::stream) - .forEach(cookie -> getHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString())); + .forEach(cookie -> this.headers.add(HttpHeaders.SET_COOKIE, cookie.toString())); } return this.headers; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java index 78751e00fce..b3520aae138 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -31,6 +31,7 @@ import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.http.client.reactive.ClientHttpResponse; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; @@ -75,6 +76,9 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { private Flux body = Flux.empty(); + @Nullable + private ClientResponse originalResponse; + private HttpRequest request; @@ -90,12 +94,9 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { this.statusCode = other.rawStatusCode(); this.headers.addAll(other.headers().asHttpHeaders()); this.cookies.addAll(other.cookies()); - if (other instanceof DefaultClientResponse) { - this.request = ((DefaultClientResponse) other).request(); - } - else { - this.request = EMPTY_REQUEST; - } + this.originalResponse = other; + this.request = (other instanceof DefaultClientResponse ? + ((DefaultClientResponse) other).request() : EMPTY_REQUEST); } @@ -178,7 +179,10 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { // When building ClientResponse manually, the ClientRequest.logPrefix() has to be passed, // e.g. via ClientResponse.Builder, but this (builder) is not used currently. - return new DefaultClientResponse(httpResponse, this.strategies, "", "", () -> this.request); + return new DefaultClientResponse(httpResponse, this.strategies, + this.originalResponse != null ? this.originalResponse.logPrefix() : "", + this.request.getMethodValue() + " " + this.request.getURI(), + () -> this.request); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java index 8e6c01ef63b..ab83781e392 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -26,8 +26,12 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; +import org.springframework.web.testfixture.http.client.reactive.MockClientHttpRequest; +import org.springframework.web.testfixture.http.client.reactive.MockClientHttpResponse; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -69,11 +73,16 @@ public class DefaultClientResponseBuilderTests { .map(s -> s.getBytes(StandardCharsets.UTF_8)) .map(dataBufferFactory::wrap); - ClientResponse other = ClientResponse.create(HttpStatus.BAD_REQUEST, ExchangeStrategies.withDefaults()) - .header("foo", "bar") - .cookie("baz", "qux") - .body(otherBody) - .build(); + HttpRequest mockClientHttpRequest = new MockClientHttpRequest(HttpMethod.GET, "/path"); + + MockClientHttpResponse httpResponse = new MockClientHttpResponse(HttpStatus.BAD_REQUEST); + httpResponse.getHeaders().add("foo", "bar"); + httpResponse.getCookies().add("baz", ResponseCookie.from("baz", "qux").build()); + httpResponse.setBody(otherBody); + + + DefaultClientResponse other = new DefaultClientResponse( + httpResponse, ExchangeStrategies.withDefaults(), "my-prefix", "", () -> mockClientHttpRequest); Flux body = Flux.just("baz") .map(s -> s.getBytes(StandardCharsets.UTF_8)) @@ -86,10 +95,11 @@ public class DefaultClientResponseBuilderTests { .build(); assertThat(result.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(result.headers().asHttpHeaders().size()).isEqualTo(1); + assertThat(result.headers().asHttpHeaders().size()).isEqualTo(2); assertThat(result.headers().asHttpHeaders().getFirst("foo")).isEqualTo("baar"); assertThat(result.cookies().size()).isEqualTo(1); assertThat(result.cookies().getFirst("baz").getValue()).isEqualTo("quux"); + assertThat(result.logPrefix()).isEqualTo("my-prefix"); StepVerifier.create(result.bodyToFlux(String.class)) .expectNext("baz") @@ -104,5 +114,4 @@ public class DefaultClientResponseBuilderTests { assertThat(result.rawStatusCode()).isEqualTo(499); assertThatIllegalArgumentException().isThrownBy(result::statusCode); } - } From c3e55d537e45e1e18b19557e918655aa04c59491 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 22 May 2020 07:49:01 +0100 Subject: [PATCH 3/5] DefaultResponseErrorHandler javadoc update Closes gh-25067 --- .../client/DefaultResponseErrorHandler.java | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java index 90c1b7a04a7..ddd3ab2ae5c 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java @@ -35,11 +35,14 @@ import org.springframework.util.ObjectUtils; /** * Spring's default implementation of the {@link ResponseErrorHandler} interface. * - *

This error handler checks for the status code on the {@link ClientHttpResponse}: - * Any code with series {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR} - * or {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR} is considered to be - * an error; this behavior can be changed by overriding the {@link #hasError(HttpStatus)} - * method. Unknown status codes will be ignored by {@link #hasError(ClientHttpResponse)}. + *

This error handler checks for the status code on the + * {@link ClientHttpResponse}. Any code in the 4xx or 5xx series is considered + * to be an error. This behavior can be changed by overriding + * {@link #hasError(HttpStatus)}. Unknown status codes will be ignored by + * {@link #hasError(ClientHttpResponse)}. + * + *

See {@link #handleError(ClientHttpResponse)} for more details on specific + * exception types. * * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -93,8 +96,18 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler { } /** - * Delegates to {@link #handleError(ClientHttpResponse, HttpStatus)} with the - * response status code. + * Handle the error in the given response with the given resolved status code. + *

The default implementation throws: + *

    + *
  • {@link HttpClientErrorException} if the status code is in the 4xx + * series, or one of its sub-classes such as + * {@link HttpClientErrorException.BadRequest} and others. + *
  • {@link HttpServerErrorException} if the status code is in the 5xx + * series, or one of its sub-classes such as + * {@link HttpServerErrorException.InternalServerError} and others. + *
  • {@link UnknownHttpStatusCodeException} for error status codes not in the + * {@link HttpStatus} enum range. + *
* @throws UnknownHttpStatusCodeException in case of an unresolvable status code * @see #handleError(ClientHttpResponse, HttpStatus) */ @@ -148,12 +161,13 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler { } /** - * Handle the error in the given response with the given resolved status code. - *

The default implementation throws an {@link HttpClientErrorException} - * if the status code is {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR - * CLIENT_ERROR}, an {@link HttpServerErrorException} if it is - * {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR SERVER_ERROR}, - * or an {@link UnknownHttpStatusCodeException} in other cases. + * Handle the error based on the resolved status code. + * + *

The default implementation delegates to + * {@link HttpClientErrorException#create} for errors in the 4xx range, to + * {@link HttpServerErrorException#create} for errors in the 5xx range, + * or otherwise raises {@link UnknownHttpStatusCodeException}. + * * @since 5.0 * @see HttpClientErrorException#create * @see HttpServerErrorException#create From 23a66e016eac178e0b922ebbe0fba50165f37ab3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 24 May 2020 20:40:43 +0100 Subject: [PATCH 4/5] UriBuilder javadoc update Closes gh-25055 --- .../web/util/DefaultUriBuilderFactory.java | 24 +++++--- .../springframework/web/util/UriBuilder.java | 61 ++++++++++++++++--- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java index d7654582436..1df9e750991 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java +++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -41,7 +41,14 @@ import org.springframework.util.StringUtils; public class DefaultUriBuilderFactory implements UriBuilderFactory { /** - * Enum to represent multiple URI encoding strategies. + * Enum to represent multiple URI encoding strategies. The following are + * available: + *

    + *
  • {@link #TEMPLATE_AND_VALUES} + *
  • {@link #VALUES_ONLY} + *
  • {@link #URI_COMPONENT} + *
  • {@link #NONE} + *
* @see #setEncodingMode */ public enum EncodingMode { @@ -130,16 +137,13 @@ public class DefaultUriBuilderFactory implements UriBuilderFactory { /** - * Set the encoding mode to use. + * Set the {@link EncodingMode encoding mode} to use. *

By default this is set to {@link EncodingMode#TEMPLATE_AND_VALUES * EncodingMode.TEMPLATE_AND_VALUES}. - *

Note: In 5.1 the default was changed from - * {@link EncodingMode#URI_COMPONENT EncodingMode.URI_COMPONENT}. - * Consequently the {@code WebClient}, which relies on the built-in default - * has also been switched to the new default. The {@code RestTemplate} - * however sets this explicitly to {@link EncodingMode#URI_COMPONENT - * EncodingMode.URI_COMPONENT} explicitly for historic and backwards - * compatibility reasons. + *

Note: Prior to 5.1 the default was + * {@link EncodingMode#URI_COMPONENT EncodingMode.URI_COMPONENT} + * therefore the {@code WebClient} {@code RestTemplate} have switched their + * default behavior. * @param encodingMode the encoding mode to use */ public void setEncodingMode(EncodingMode encodingMode) { diff --git a/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java index 14618e75208..fbd04de36a1 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -77,22 +77,69 @@ public interface UriBuilder { UriBuilder port(@Nullable String port); /** - * Append the given path to the existing path of this builder. - * The given path may contain URI template variables. + * Append to the path of this builder. + *

The given value is appended as-is without any checks for slashes other + * than to clean up duplicates. For example: + *

+	 *
+	 * builder.path("/first-").path("value/").path("/{id}").build("123")
+	 *
+	 * // Results is "/first-value/123"
+	 * 
+ *

By contrast {@link #pathSegment(String...)} builds the path from + * individual path segments and in that case slashes are inserted transparently. + * In some cases you may use a combination of both {@code pathSegment} and + * {@code path}. For example: + *

+	 *
+	 * builder.pathSegment("first-value", "second-value").path("/")
+	 *
+	 * // Results is "/first-value/second-value/"
+	 *
+	 * 
+ *

If a URI variable value contains slashes, whether those are encoded or + * not depends on the configured encoding mode. See + * {@link UriComponentsBuilder#encode()}, or if using + * {@code UriComponentsBuilder} via {@link DefaultUriBuilderFactory} + * (e.g. {@code WebClient} or {@code RestTemplate}) see its + * {@link DefaultUriBuilderFactory#setEncodingMode encodingMode} property. + * Also see the + * URI Encoding section of the reference docs. * @param path the URI path */ UriBuilder path(String path); /** - * Set the path of this builder overriding the existing path values. + * Override the existing path. * @param path the URI path, or {@code null} for an empty path */ UriBuilder replacePath(@Nullable String path); /** - * Append path segments to the existing path. Each path segment may contain - * URI template variables and should not contain any slashes. - * Use {@code path("/")} subsequently to ensure a trailing slash. + * Append to the path using path segments. For example: + *

+	 *
+	 * builder.pathSegment("first-value", "second-value", "{id}").build("123")
+	 *
+	 * // Results is "/first-value/second-value/123"
+	 *
+	 * 
+ *

If slashes are present in a path segment, they are encoded: + *

+	 *
+	 * builder.pathSegment("ba/z", "{id}").build("a/b")
+	 *
+	 * // Results is "/ba%2Fz/a%2Fb"
+	 *
+	 * 
+ * To insert a trailing slash, use the {@link #path} builder method: + *
+	 *
+	 * builder.pathSegment("first-value", "second-value").path("/")
+	 *
+	 * // Results is "/first-value/second-value/"
+	 *
+	 * 
* @param pathSegments the URI path segments */ UriBuilder pathSegment(String... pathSegments) throws IllegalArgumentException; From 4f65ba4e74096e2391a576d865aaa1ec7c2a70ff Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 24 May 2020 20:58:28 +0100 Subject: [PATCH 5/5] Simplify creation of HttpContext for Apache HttpClient Closes gh-25066 --- ...ttpComponentsClientHttpRequestFactory.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java index 5d6cbd9fb75..db8cb8cefce 100644 --- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -19,6 +19,7 @@ package org.springframework.http.client; import java.io.Closeable; import java.io.IOException; import java.net.URI; +import java.util.function.BiFunction; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; @@ -66,6 +67,9 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest private boolean bufferRequestBody = true; + @Nullable + private BiFunction httpContextFactory; + /** * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory} @@ -157,6 +161,19 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest this.bufferRequestBody = bufferRequestBody; } + /** + * Configure a factory to pre-create the {@link HttpContext} for each request. + *

This may be useful for example in mutual TLS authentication where a + * different {@code RestTemplate} for each client certificate such that + * all calls made through a given {@code RestTemplate} instance as associated + * for the same client identity. {@link HttpClientContext#setUserToken(Object)} + * can be used to specify a fixed user token for all requests. + * @param httpContextFactory the context factory to use + * @since 5.2.7 + */ + public void setHttpContextFactory(BiFunction httpContextFactory) { + this.httpContextFactory = httpContextFactory; + } @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { @@ -296,7 +313,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest */ @Nullable protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) { - return null; + return (this.httpContextFactory != null ? this.httpContextFactory.apply(httpMethod, uri) : null); }