Skip to content

Commit a0542f0

Browse files
committed
Merge branch '6.2.x'
2 parents 2f262af + 6e2fbfe commit a0542f0

File tree

6 files changed

+132
-17
lines changed

6 files changed

+132
-17
lines changed

framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,13 @@ Kotlin::
256256
======
257257
--
258258

259-
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
260-
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
261-
other property sources. You can use this, for example, to parameterize a base URL based on
262-
some external configuration.
259+
URI path patterns can also have:
260+
261+
- Embedded `${...}` placeholders that are resolved on startup via
262+
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
263+
other property sources. This is useful, for example, to parameterize a base URL based on
264+
external configuration.
265+
- SpEL expressions `#{...}`.
263266

264267
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
265268
Both classes are located in `spring-web` and are expressly designed for use with HTTP URL

framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,13 @@ Kotlin::
251251
----
252252
======
253253

254-
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
255-
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
256-
other property sources. You can use this, for example, to parameterize a base URL based on
257-
some external configuration.
254+
URI path patterns can also have:
255+
256+
- Embedded `${...}` placeholders that are resolved on startup via
257+
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
258+
other property sources. This is useful, for example, to parameterize a base URL based on
259+
external configuration.
260+
- SpEL expression `#{...}`.
258261

259262

260263
[[mvc-ann-requestmapping-pattern-comparison]]

spring-web/src/main/java/org/springframework/http/ProblemDetail.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ protected ProblemDetail() {
108108
* @param type the problem type
109109
*/
110110
public void setType(URI type) {
111-
Assert.notNull(type, "'type' is required");
112111
this.type = type;
113112
}
114113

@@ -245,7 +244,7 @@ public void setProperties(@Nullable Map<String, Object> properties) {
245244
@Override
246245
public boolean equals(@Nullable Object other) {
247246
return (this == other || (other instanceof ProblemDetail that &&
248-
getType().equals(that.getType()) &&
247+
ObjectUtils.nullSafeEquals(getType(), that.getType()) &&
249248
ObjectUtils.nullSafeEquals(getTitle(), that.getTitle()) &&
250249
this.status == that.status &&
251250
ObjectUtils.nullSafeEquals(this.detail, that.detail) &&

spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.concurrent.Executor;
3838
import java.util.concurrent.Flow;
3939
import java.util.concurrent.TimeUnit;
40+
import java.util.concurrent.atomic.AtomicBoolean;
4041

4142
import org.jspecify.annotations.Nullable;
4243

@@ -96,12 +97,13 @@ public URI getURI() {
9697
@Override
9798
protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException {
9899
CompletableFuture<HttpResponse<InputStream>> responseFuture = null;
100+
TimeoutHandler timeoutHandler = null;
99101
try {
100102
HttpRequest request = buildRequest(headers, body);
101103
responseFuture = this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream());
102104

103105
if (this.timeout != null) {
104-
TimeoutHandler timeoutHandler = new TimeoutHandler(responseFuture, this.timeout);
106+
timeoutHandler = new TimeoutHandler(responseFuture, this.timeout);
105107
HttpResponse<InputStream> response = responseFuture.get();
106108
InputStream inputStream = timeoutHandler.wrapInputStream(response);
107109
return new JdkClientHttpResponse(response, inputStream);
@@ -119,8 +121,11 @@ protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body
119121
catch (ExecutionException ex) {
120122
Throwable cause = ex.getCause();
121123

122-
if (cause instanceof CancellationException) {
123-
throw new HttpTimeoutException("Request timed out");
124+
if (cause instanceof CancellationException ce) {
125+
if (timeoutHandler != null) {
126+
timeoutHandler.handleCancellationException(ce);
127+
}
128+
throw new IOException("Request cancelled", cause);
124129
}
125130
if (cause instanceof UncheckedIOException uioEx) {
126131
throw uioEx.getCause();
@@ -136,6 +141,12 @@ else if (cause instanceof IOException ioEx) {
136141
throw (message == null ? new IOException(cause) : new IOException(message, cause));
137142
}
138143
}
144+
catch (CancellationException ex) {
145+
if (timeoutHandler != null) {
146+
timeoutHandler.handleCancellationException(ex);
147+
}
148+
throw new IOException("Request cancelled", ex);
149+
}
139150
}
140151

141152
private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) {
@@ -234,12 +245,15 @@ private static final class TimeoutHandler {
234245

235246
private final CompletableFuture<Void> timeoutFuture;
236247

248+
private final AtomicBoolean timeout = new AtomicBoolean(false);
249+
237250
private TimeoutHandler(CompletableFuture<HttpResponse<InputStream>> future, Duration timeout) {
238251

239252
this.timeoutFuture = new CompletableFuture<Void>()
240253
.completeOnTimeout(null, timeout.toMillis(), TimeUnit.MILLISECONDS);
241254

242255
this.timeoutFuture.thenRun(() -> {
256+
this.timeout.set(true);
243257
if (future.cancel(true) || future.isCompletedExceptionally() || !future.isDone()) {
244258
return;
245259
}
@@ -250,7 +264,6 @@ private TimeoutHandler(CompletableFuture<HttpResponse<InputStream>> future, Dura
250264
// ignore
251265
}
252266
});
253-
254267
}
255268

256269
public @Nullable InputStream wrapInputStream(HttpResponse<InputStream> response) {
@@ -267,6 +280,12 @@ public void close() throws IOException {
267280
}
268281
};
269282
}
283+
284+
public void handleCancellationException(CancellationException ex) throws HttpTimeoutException {
285+
if (this.timeout.get()) {
286+
throw new HttpTimeoutException(ex.getMessage());
287+
}
288+
}
270289
}
271290

272291
}

spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,20 @@
105105

106106
/**
107107
* The path mapping URIs &mdash; for example, {@code "/profile"}.
108-
* <p>Ant-style path patterns are also supported (for example, {@code "/profile/**"}).
109-
* At the method level, relative paths (for example, {@code "edit"}) are supported
108+
* <p>Ant-style path patterns are also supported, e.g. {@code "/profile/**"}.
109+
* At the method level, relative paths, e.g., {@code "edit"} are supported
110110
* within the primary mapping expressed at the type level.
111-
* Path mapping URIs may contain placeholders (for example, <code>"/${profile_path}"</code>).
111+
* Path mapping URIs may contain property placeholders, e.g. <code>"/${profile_path}"</code>,
112+
* and SpEL expressions, e.g. {@code "/profile/#{@bean.property}"}.
112113
* <p><b>Supported at the type level as well as at the method level!</b>
113114
* When used at the type level, all method-level mappings inherit
114115
* this primary mapping, narrowing it for a specific handler method.
115116
* <p><strong>NOTE</strong>: A handler method that is not mapped to any path
116117
* explicitly is effectively mapped to an empty path.
117118
* @since 4.2
119+
* @see org.springframework.beans.factory.config.EmbeddedValueResolver
120+
* @see org.springframework.context.expression.StandardBeanExpressionResolver
121+
* @see org.springframework.context.support.AbstractApplicationContext
118122
*/
119123
@AliasFor("value")
120124
String[] path() default {};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.client;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.net.URI;
22+
import java.net.http.HttpClient;
23+
import java.net.http.HttpRequest;
24+
import java.net.http.HttpResponse;
25+
import java.net.http.HttpTimeoutException;
26+
import java.time.Duration;
27+
import java.util.concurrent.CompletableFuture;
28+
import java.util.concurrent.ExecutorService;
29+
import java.util.concurrent.Executors;
30+
31+
import org.junit.jupiter.api.AfterEach;
32+
import org.junit.jupiter.api.BeforeEach;
33+
import org.junit.jupiter.api.Test;
34+
35+
import org.springframework.http.HttpHeaders;
36+
import org.springframework.http.HttpMethod;
37+
38+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
39+
import static org.mockito.Mockito.any;
40+
import static org.mockito.Mockito.mock;
41+
import static org.mockito.Mockito.when;
42+
43+
/**
44+
* Unit tests for {@link JdkClientHttpRequest}.
45+
*/
46+
class JdkClientHttpRequestTests {
47+
48+
private final HttpClient client = mock(HttpClient.class);
49+
50+
private ExecutorService executor;
51+
52+
53+
@BeforeEach
54+
void setup() {
55+
executor = Executors.newSingleThreadExecutor();
56+
}
57+
58+
@AfterEach
59+
void tearDown() {
60+
executor.shutdownNow();
61+
}
62+
63+
64+
@Test
65+
void futureCancelledAfterTimeout() {
66+
CompletableFuture<HttpResponse<InputStream>> future = new CompletableFuture<>();
67+
when(client.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future);
68+
69+
assertThatThrownBy(() -> createRequest(Duration.ofMillis(10)).executeInternal(new HttpHeaders(), null))
70+
.isExactlyInstanceOf(HttpTimeoutException.class);
71+
}
72+
73+
@Test
74+
void futureCancelled() {
75+
CompletableFuture<HttpResponse<InputStream>> future = new CompletableFuture<>();
76+
future.cancel(true);
77+
when(client.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future);
78+
79+
assertThatThrownBy(() -> createRequest(null).executeInternal(new HttpHeaders(), null))
80+
.isExactlyInstanceOf(IOException.class);
81+
}
82+
83+
private JdkClientHttpRequest createRequest(Duration timeout) {
84+
return new JdkClientHttpRequest(client, URI.create("http://abc.com"), HttpMethod.GET, executor, timeout);
85+
}
86+
87+
}

0 commit comments

Comments
 (0)