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/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); } 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 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; 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-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); } 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 bfd93ca037f..ceb97b91be2 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 @@ -208,8 +208,10 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { ClientHttpResponse httpResponse = new BuiltClientHttpResponse( this.statusCode, this.headers, this.cookies, this.body, this.originalResponse); - 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 70cebb42f4f..04c2849cccd 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 @@ -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; @@ -65,27 +69,35 @@ public class DefaultClientResponseBuilderTests { @Test public void mutate() { + Flux otherBody = Flux.just("foo", "bar") + .map(s -> s.getBytes(StandardCharsets.UTF_8)) + .map(dataBufferFactory::wrap); - ClientResponse originalResponse = ClientResponse - .create(HttpStatus.BAD_REQUEST, ExchangeStrategies.withDefaults()) - .header("foo", "bar") - .header("bar", "baz") - .cookie("baz", "qux") - .body(Flux.just("foobar".getBytes(StandardCharsets.UTF_8)).map(dataBufferFactory::wrap)) - .build(); + HttpRequest mockClientHttpRequest = new MockClientHttpRequest(HttpMethod.GET, "/path"); - ClientResponse result = originalResponse.mutate() - .statusCode(HttpStatus.OK) + MockClientHttpResponse httpResponse = new MockClientHttpResponse(HttpStatus.OK); + httpResponse.getHeaders().add("foo", "bar"); + httpResponse.getHeaders().add("bar", "baz"); + httpResponse.getCookies().add("baz", ResponseCookie.from("baz", "qux").build()); + httpResponse.setBody(otherBody); + + DefaultClientResponse otherResponse = new DefaultClientResponse( + httpResponse, ExchangeStrategies.withDefaults(), "my-prefix", "", () -> mockClientHttpRequest); + + ClientResponse result = otherResponse.mutate() + .statusCode(HttpStatus.BAD_REQUEST) .headers(headers -> headers.set("foo", "baar")) .cookies(cookies -> cookies.set("baz", ResponseCookie.from("baz", "quux").build())) .build(); - assertThat(result.statusCode()).isEqualTo(HttpStatus.OK); - assertThat(result.headers().asHttpHeaders().size()).isEqualTo(2); + + assertThat(result.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(result.headers().asHttpHeaders().size()).isEqualTo(3); assertThat(result.headers().asHttpHeaders().getFirst("foo")).isEqualTo("baar"); assertThat(result.headers().asHttpHeaders().getFirst("bar")).isEqualTo("baz"); 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("foobar") @@ -100,5 +112,4 @@ public class DefaultClientResponseBuilderTests { assertThat(result.rawStatusCode()).isEqualTo(499); assertThatIllegalArgumentException().isThrownBy(result::statusCode); } - }