From 080f356a06d63f98a709da057dddd6ea91a7691d Mon Sep 17 00:00:00 2001 From: uuuyuqi Date: Tue, 28 Apr 2026 10:36:41 +0800 Subject: [PATCH 1/2] feat: add Apache HttpClient 5.x adapter support Add sentinel-apache-httpclient5-adapter module to support flow control for Apache HttpClient 5.x outgoing HTTP requests. The adapter implements ExecChainHandler to intercept requests in the HttpClient exec chain. Key features: - SentinelApacheHttpClient5Handler: ExecChainHandler implementation - Default resource name format: METHOD:url (query/fragment stripped) - Pluggable resource extractor and fallback interfaces - Comprehensive unit and integration tests Change-Id: I6be84c73078102a92c5701cce9f66e1c543f32f7 Co-developed-by: Cursor --- sentinel-adapter/pom.xml | 1 + .../README.md | 77 ++++++++ .../pom.xml | 69 +++++++ .../SentinelApacheHttpClient5Handler.java | 92 ++++++++++ .../SentinelApacheHttpClientConfig.java | 59 ++++++ .../ApacheHttpClientResourceExtractor.java | 34 ++++ ...aultApacheHttpClientResourceExtractor.java | 55 ++++++ .../fallback/ApacheHttpClientFallback.java | 37 ++++ .../DefaultApacheHttpClientFallback.java | 32 ++++ .../SentinelApacheHttpClientTest.java | 168 ++++++++++++++++++ .../httpclient5/app/TestApplication.java | 30 ++++ .../app/controller/TestController.java | 52 ++++++ .../SentinelApacheHttpClientConfigTest.java | 70 ++++++++ ...ApacheHttpClientResourceExtractorTest.java | 86 +++++++++ .../ApacheHttpClientFallbackTest.java | 34 ++++ 15 files changed, 896 insertions(+) create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/README.md create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/pom.xml create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/SentinelApacheHttpClient5Handler.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/config/SentinelApacheHttpClientConfig.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/ApacheHttpClientResourceExtractor.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/DefaultApacheHttpClientResourceExtractor.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/ApacheHttpClientFallback.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/DefaultApacheHttpClientFallback.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/SentinelApacheHttpClientTest.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/TestApplication.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/controller/TestController.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/config/SentinelApacheHttpClientConfigTest.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/DefaultApacheHttpClientResourceExtractorTest.java create mode 100644 sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/ApacheHttpClientFallbackTest.java diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index e29d926506..fc7acbdf1a 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -20,6 +20,7 @@ sentinel-apache-dubbo-adapter sentinel-apache-dubbo3-adapter sentinel-apache-httpclient-adapter + sentinel-apache-httpclient5-adapter sentinel-sofa-rpc-adapter sentinel-grpc-adapter sentinel-zuul-adapter diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/README.md b/sentinel-adapter/sentinel-apache-httpclient5-adapter/README.md new file mode 100644 index 0000000000..336a7c8268 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/README.md @@ -0,0 +1,77 @@ +# Sentinel Apache HttpClient 5.x Adapter + +## Introduction + +Sentinel provides integration for Apache HttpClient 5.x to enable flow control for outgoing HTTP requests. + +## Usage + +### Add dependency + +```xml + + com.alibaba.csp + sentinel-apache-httpclient5-adapter + x.y.z + +``` + +### Build the HttpClient + +```java +CloseableHttpClient httpclient = HttpClients.custom() + .addExecInterceptorBefore(ChainElement.MAIN_TRANSPORT.name(), "sentinel", + new SentinelApacheHttpClient5Handler()) + .build(); +``` + +Or with custom configuration: + +```java +SentinelApacheHttpClientConfig config = new SentinelApacheHttpClientConfig(); +config.setPrefix("httpclient:"); +config.setExtractor(myExtractor); +config.setFallback(myFallback); + +CloseableHttpClient httpclient = HttpClients.custom() + .addExecInterceptorBefore(ChainElement.MAIN_TRANSPORT.name(), "sentinel", + new SentinelApacheHttpClient5Handler(config)) + .build(); +``` + +### Configuration + +| Name | Description | Type | Default Value | +|------|------------|------|---------------| +| prefix | Customize resource prefix | `String` | `httpclient:` | +| extractor | Customize resource extractor | `ApacheHttpClientResourceExtractor` | `DefaultApacheHttpClientResourceExtractor` | +| fallback | Handle request when it is blocked | `ApacheHttpClientFallback` | `DefaultApacheHttpClientFallback` | + +### Resource Extractor + +The default extractor generates resource names in the format `METHOD:url` (e.g. `GET:/api/users`), +with query parameters and fragments stripped. You can customize this by implementing `ApacheHttpClientResourceExtractor`: + +```java +public class MyResourceExtractor implements ApacheHttpClientResourceExtractor { + @Override + public String extractor(ClassicHttpRequest request) { + // custom resource name extraction logic + return request.getMethod() + ":" + request.getRequestUri(); + } +} +``` + +### Fallback + +The default fallback throws `SentinelRpcException`. You can customize the behavior: + +```java +public class MyFallback implements ApacheHttpClientFallback { + @Override + public ClassicHttpResponse handle(ClassicHttpRequest request, BlockException e) { + // return a custom response or throw exception + throw new SentinelRpcException(e); + } +} +``` diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/pom.xml b/sentinel-adapter/sentinel-apache-httpclient5-adapter/pom.xml new file mode 100644 index 0000000000..e9c97ef1a7 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/pom.xml @@ -0,0 +1,69 @@ + + + + sentinel-adapter + com.alibaba.csp + 2.0.0-alpha2-SNAPSHOT + + 4.0.0 + + sentinel-apache-httpclient5-adapter + jar + + + 5.1 + 2.1.3.RELEASE + 5.1.5.RELEASE + + + + + com.alibaba.csp + sentinel-core + + + org.apache.httpcomponents.client5 + httpclient5 + ${apache.httpclient5.version} + provided + + + + junit + junit + test + + + + org.mockito + mockito-core + test + + + com.alibaba + fastjson + test + + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + test + + + org.springframework.boot + spring-boot-test + ${spring.boot.version} + test + + + org.springframework + spring-test + ${spring-test.version} + test + + + diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/SentinelApacheHttpClient5Handler.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/SentinelApacheHttpClient5Handler.java new file mode 100644 index 0000000000..2de5aa2f77 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/SentinelApacheHttpClient5Handler.java @@ -0,0 +1,92 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.config.SentinelApacheHttpClientConfig; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.apache.hc.client5.http.classic.ExecChain; +import org.apache.hc.client5.http.classic.ExecChainHandler; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpException; + +import java.io.IOException; + +/** + * Apache HttpClient 5.x adapter for Sentinel. + * + *

This handler implements {@link ExecChainHandler} to intercept outgoing HTTP requests + * and protect them with Sentinel flow control.

+ * + *

Usage example:

+ *
{@code
+ * CloseableHttpClient httpclient = HttpClients.custom()
+ *     .addExecInterceptorBefore(ChainElement.MAIN_TRANSPORT.name(), "sentinel",
+ *         new SentinelApacheHttpClient5Handler())
+ *     .build();
+ * }
+ * + * @author qihuai.wyq + */ +public class SentinelApacheHttpClient5Handler implements ExecChainHandler { + + private final SentinelApacheHttpClientConfig config; + + public SentinelApacheHttpClient5Handler() { + this.config = new SentinelApacheHttpClientConfig(); + } + + public SentinelApacheHttpClient5Handler(SentinelApacheHttpClientConfig config) { + this.config = config; + } + + @Override + public ClassicHttpResponse execute(ClassicHttpRequest classicHttpRequest, ExecChain.Scope scope, + ExecChain execChain) throws IOException, HttpException { + String name = config.getExtractor().extractor(classicHttpRequest); + if (StringUtil.isEmpty(name)) { + return execChain.proceed(classicHttpRequest, scope); + } + + if (StringUtil.isNotEmpty(config.getPrefix())) { + name = config.getPrefix() + name; + } + + Entry entry = null; + try { + entry = SphU.entry(name, ResourceTypeConstants.COMMON_WEB, EntryType.OUT); + return execChain.proceed(classicHttpRequest, scope); + } catch (BlockException e) { + return config.getFallback().handle(classicHttpRequest, e); + } catch (IOException | HttpException | RuntimeException e) { + Tracer.traceEntry(e, entry); + throw e; + } catch (Throwable t) { + Tracer.traceEntry(t, entry); + throw new RuntimeException(t); + } finally { + if (entry != null) { + entry.exit(); + } + } + } +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/config/SentinelApacheHttpClientConfig.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/config/SentinelApacheHttpClientConfig.java new file mode 100644 index 0000000000..b52f49a056 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/config/SentinelApacheHttpClientConfig.java @@ -0,0 +1,59 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.config; + +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.extractor.ApacheHttpClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.extractor.DefaultApacheHttpClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.fallback.ApacheHttpClientFallback; +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.fallback.DefaultApacheHttpClientFallback; +import com.alibaba.csp.sentinel.util.AssertUtil; + +/** + * @author qihuai.wyq + */ +public class SentinelApacheHttpClientConfig { + + private String prefix = "httpclient:"; + private ApacheHttpClientResourceExtractor extractor = new DefaultApacheHttpClientResourceExtractor(); + private ApacheHttpClientFallback fallback = new DefaultApacheHttpClientFallback(); + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + AssertUtil.notNull(prefix, "prefix cannot be null"); + this.prefix = prefix; + } + + public ApacheHttpClientResourceExtractor getExtractor() { + return extractor; + } + + public void setExtractor(ApacheHttpClientResourceExtractor extractor) { + AssertUtil.notNull(extractor, "extractor cannot be null"); + this.extractor = extractor; + } + + public ApacheHttpClientFallback getFallback() { + return fallback; + } + + public void setFallback(ApacheHttpClientFallback fallback) { + AssertUtil.notNull(fallback, "fallback cannot be null"); + this.fallback = fallback; + } +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/ApacheHttpClientResourceExtractor.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/ApacheHttpClientResourceExtractor.java new file mode 100644 index 0000000000..94ab406022 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/ApacheHttpClientResourceExtractor.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.extractor; + +import org.apache.hc.core5.http.ClassicHttpRequest; + +/** + * Extracts Sentinel resource name from an Apache HttpClient 5.x request. + * + * @author qihuai.wyq + */ +public interface ApacheHttpClientResourceExtractor { + + /** + * Extract resource name from the given request. + * + * @param request the HTTP request + * @return the resource name, or {@code null}/{@code ""} to skip Sentinel protection + */ + String extractor(ClassicHttpRequest request); +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/DefaultApacheHttpClientResourceExtractor.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/DefaultApacheHttpClientResourceExtractor.java new file mode 100644 index 0000000000..7f9fb3adb4 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/DefaultApacheHttpClientResourceExtractor.java @@ -0,0 +1,55 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.extractor; + +import com.alibaba.csp.sentinel.log.RecordLog; +import org.apache.hc.core5.http.ClassicHttpRequest; + +/** + * Default implementation of {@link ApacheHttpClientResourceExtractor}. + * + *

Generates resource name in the format {@code METHOD:url}, with query string + * and fragment stripped. This is consistent with the OkHttp adapter's resource naming + * convention.

+ * + * @author qihuai.wyq + */ +public class DefaultApacheHttpClientResourceExtractor implements ApacheHttpClientResourceExtractor { + + @Override + public String extractor(ClassicHttpRequest request) { + try { + String httpMethod = request.getMethod(); + String originalUrl = request.getUri().toString(); + int firstIndexOfQuery = originalUrl.indexOf('?'); + int firstIndexOfFragment = originalUrl.indexOf('#'); + if (firstIndexOfFragment < 0 && firstIndexOfQuery < 0) { + return httpMethod + ":" + originalUrl; + } + if (firstIndexOfFragment > 0 && firstIndexOfQuery > 0) { + int pos = Math.min(firstIndexOfQuery, firstIndexOfFragment); + return httpMethod + ":" + originalUrl.substring(0, pos); + } else if (firstIndexOfQuery > 0) { + return httpMethod + ":" + originalUrl.substring(0, firstIndexOfQuery); + } else { + return httpMethod + ":" + originalUrl.substring(0, firstIndexOfFragment); + } + } catch (Exception ex) { + RecordLog.warn("Failed to extract resource name of HttpClient 5 request, request={}", request, ex); + return null; + } + } +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/ApacheHttpClientFallback.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/ApacheHttpClientFallback.java new file mode 100644 index 0000000000..f3e890ed04 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/ApacheHttpClientFallback.java @@ -0,0 +1,37 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.fallback; + +import com.alibaba.csp.sentinel.slots.block.BlockException; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; + +/** + * Fallback handler for Apache HttpClient 5.x when the request is blocked by Sentinel. + * + * @author qihuai.wyq + */ +public interface ApacheHttpClientFallback { + + /** + * Handle the blocked request. + * + * @param request the original HTTP request + * @param e the block exception + * @return the fallback response + */ + ClassicHttpResponse handle(ClassicHttpRequest request, BlockException e); +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/DefaultApacheHttpClientFallback.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/DefaultApacheHttpClientFallback.java new file mode 100644 index 0000000000..8136f3922c --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/DefaultApacheHttpClientFallback.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.fallback; + +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; + +/** + * @author qihuai.wyq + */ +public class DefaultApacheHttpClientFallback implements ApacheHttpClientFallback { + + @Override + public ClassicHttpResponse handle(ClassicHttpRequest request, BlockException e) { + throw new SentinelRpcException(e); + } +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/SentinelApacheHttpClientTest.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/SentinelApacheHttpClientTest.java new file mode 100644 index 0000000000..72234c6921 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/SentinelApacheHttpClientTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.app.TestApplication; +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.config.SentinelApacheHttpClientConfig; +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.extractor.ApacheHttpClientResourceExtractor; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.ChainElement; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.protocol.BasicHttpContext; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.net.URISyntaxException; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * @author qihuai.wyq + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, + properties = { + "server.port=8185" + }) +public class SentinelApacheHttpClientTest { + + @Value("${server.port}") + private Integer port; + + @Test + public void testDefaultInterceptor() throws Exception { + CloseableHttpClient httpclient = HttpClients.custom() + .addExecInterceptorBefore(ChainElement.MAIN_TRANSPORT.name(), "sentinel", + new SentinelApacheHttpClient5Handler()) + .build(); + + String url = "http://localhost:" + port + "/httpclient/back"; + HttpGet httpGet = new HttpGet(url); + getRemoteString(httpclient, httpGet); + ClusterNode cn = ClusterBuilderSlot.getClusterNode("httpclient:GET:" + url); + assertNotNull(cn); + } + + @Test + public void testWithUrlQuery() throws Exception { + CloseableHttpClient httpclient = HttpClients.custom() + .addExecInterceptorBefore(ChainElement.MAIN_TRANSPORT.name(), "sentinel", + new SentinelApacheHttpClient5Handler()) + .build(); + + String urlWithQuery = "http://localhost:" + port + "/httpclient/query?foo=baz&baz=foo"; + String urlWithoutQuery = "http://localhost:" + port + "/httpclient/query"; + HttpGet httpGet = new HttpGet(urlWithQuery); + getRemoteString(httpclient, httpGet); + assertNotNull(ClusterBuilderSlot.getClusterNode("httpclient:GET:" + urlWithoutQuery)); + assertNull(ClusterBuilderSlot.getClusterNode("httpclient:GET:" + urlWithQuery)); + } + + @Test + public void testWithUrlQueryAndFragment() throws Exception { + CloseableHttpClient httpclient = HttpClients.custom() + .addExecInterceptorBefore(ChainElement.MAIN_TRANSPORT.name(), "sentinel", + new SentinelApacheHttpClient5Handler()) + .build(); + + String urlPrefix = "http://localhost:" + port + "/httpclient/fragment"; + String suffix = "#foo?baz=xxx"; + HttpGet httpGet = new HttpGet(urlPrefix + suffix); + getRemoteString(httpclient, httpGet); + assertNotNull(ClusterBuilderSlot.getClusterNode("httpclient:GET:" + urlPrefix)); + assertNull(ClusterBuilderSlot.getClusterNode("httpclient:GET:" + urlPrefix + suffix)); + } + + @Test + public void testWithCustomizedResourceExtractor() throws Exception { + SentinelApacheHttpClientConfig config = new SentinelApacheHttpClientConfig(); + config.setExtractor(new ApacheHttpClientResourceExtractor() { + @Override + public String extractor(ClassicHttpRequest request) { + String contains = "/httpclient/back/"; + String uri; + try { + uri = request.getUri().toString(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + if (uri.contains(contains)) { + uri = uri.substring(0, uri.indexOf(contains) + contains.length()) + "{id}"; + } + return request.getMethod() + ":" + uri; + } + }); + CloseableHttpClient httpclient = HttpClients.custom() + .addExecInterceptorBefore(ChainElement.MAIN_TRANSPORT.name(), "sentinel", + new SentinelApacheHttpClient5Handler(config)) + .build(); + + HttpGet httpGet = new HttpGet("http://localhost:" + port + "/httpclient/back/1"); + getRemoteString(httpclient, httpGet); + ClusterNode cn = ClusterBuilderSlot.getClusterNode( + "httpclient:GET:http://localhost:" + port + "/httpclient/back/{id}"); + assertNotNull(cn); + } + + @Test + public void testWithEmptyPrefix() throws Exception { + SentinelApacheHttpClientConfig config = new SentinelApacheHttpClientConfig(); + config.setPrefix(""); + + CloseableHttpClient httpclient = HttpClients.custom() + .addExecInterceptorBefore(ChainElement.MAIN_TRANSPORT.name(), "sentinel", + new SentinelApacheHttpClient5Handler(config)) + .build(); + + String url = "http://localhost:" + port + "/httpclient/noprefix"; + HttpGet httpGet = new HttpGet(url); + getRemoteString(httpclient, httpGet); + ClusterNode cn = ClusterBuilderSlot.getClusterNode("GET:" + url); + assertNotNull(cn); + } + + private String getRemoteString(CloseableHttpClient httpclient, HttpGet httpGet) + throws IOException, ParseException { + String result; + HttpContext context = new BasicHttpContext(); + CloseableHttpResponse response = httpclient.execute(httpGet, context); + try { + HttpEntity entity = response.getEntity(); + result = EntityUtils.toString(entity, "utf-8"); + EntityUtils.consume(entity); + } finally { + response.close(); + } + httpclient.close(); + return result; + } +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/TestApplication.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/TestApplication.java new file mode 100644 index 0000000000..a6d382ffaa --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/TestApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author qihuai.wyq + */ +@SpringBootApplication +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/controller/TestController.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/controller/TestController.java new file mode 100644 index 0000000000..a1a4432451 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/controller/TestController.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.app.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author qihuai.wyq + */ +@RestController +public class TestController { + + @GetMapping("/httpclient/back") + public String back() { + return "Welcome Back!"; + } + + @GetMapping("/httpclient/back/{id}") + public String back(@PathVariable String id) { + return "Welcome Back! " + id; + } + + @GetMapping("/httpclient/query") + public String query() { + return "Query!"; + } + + @GetMapping("/httpclient/fragment") + public String fragment() { + return "Fragment!"; + } + + @GetMapping("/httpclient/noprefix") + public String noPrefix() { + return "No Prefix!"; + } +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/config/SentinelApacheHttpClientConfigTest.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/config/SentinelApacheHttpClientConfigTest.java new file mode 100644 index 0000000000..dbde163943 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/config/SentinelApacheHttpClientConfigTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.config; + +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.extractor.DefaultApacheHttpClientResourceExtractor; +import com.alibaba.csp.sentinel.adapter.apache.httpclient5.fallback.DefaultApacheHttpClientFallback; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author qihuai.wyq + */ +public class SentinelApacheHttpClientConfigTest { + + @Test + public void testDefaultConfig() { + SentinelApacheHttpClientConfig config = new SentinelApacheHttpClientConfig(); + assertEquals("httpclient:", config.getPrefix()); + assertNotNull(config.getExtractor()); + assertNotNull(config.getFallback()); + assertTrue(config.getExtractor() instanceof DefaultApacheHttpClientResourceExtractor); + assertTrue(config.getFallback() instanceof DefaultApacheHttpClientFallback); + } + + @Test + public void testSetPrefix() { + SentinelApacheHttpClientConfig config = new SentinelApacheHttpClientConfig(); + config.setPrefix("custom:"); + assertEquals("custom:", config.getPrefix()); + } + + @Test + public void testSetEmptyPrefix() { + SentinelApacheHttpClientConfig config = new SentinelApacheHttpClientConfig(); + config.setPrefix(""); + assertEquals("", config.getPrefix()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetNullPrefix() { + SentinelApacheHttpClientConfig config = new SentinelApacheHttpClientConfig(); + config.setPrefix(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetNullExtractor() { + SentinelApacheHttpClientConfig config = new SentinelApacheHttpClientConfig(); + config.setExtractor(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetNullFallback() { + SentinelApacheHttpClientConfig config = new SentinelApacheHttpClientConfig(); + config.setFallback(null); + } +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/DefaultApacheHttpClientResourceExtractorTest.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/DefaultApacheHttpClientResourceExtractorTest.java new file mode 100644 index 0000000000..eed117e710 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/extractor/DefaultApacheHttpClientResourceExtractorTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.extractor; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author qihuai.wyq + */ +public class DefaultApacheHttpClientResourceExtractorTest { + + private final DefaultApacheHttpClientResourceExtractor extractor = + new DefaultApacheHttpClientResourceExtractor(); + + @Test + public void testSimpleUrl() { + HttpGet request = new HttpGet("http://localhost:8080/api/users"); + assertEquals("GET:http://localhost:8080/api/users", extractor.extractor(request)); + } + + @Test + public void testUrlWithQueryString() { + HttpGet request = new HttpGet("http://localhost:8080/api/users?page=1&size=10"); + assertEquals("GET:http://localhost:8080/api/users", extractor.extractor(request)); + } + + @Test + public void testUrlWithFragment() { + HttpGet request = new HttpGet("http://localhost:8080/api/users#section"); + assertEquals("GET:http://localhost:8080/api/users", extractor.extractor(request)); + } + + @Test + public void testUrlWithQueryAndFragment() { + HttpGet request = new HttpGet("http://localhost:8080/api/users?page=1#section"); + assertEquals("GET:http://localhost:8080/api/users", extractor.extractor(request)); + } + + @Test + public void testUrlWithFragmentBeforeQuery() { + HttpGet request = new HttpGet("http://localhost:8080/api/users#section?page=1"); + assertEquals("GET:http://localhost:8080/api/users", extractor.extractor(request)); + } + + @Test + public void testPostMethod() { + HttpPost request = new HttpPost("http://localhost:8080/api/users"); + assertEquals("POST:http://localhost:8080/api/users", extractor.extractor(request)); + } + + @Test + public void testRootPath() { + HttpGet request = new HttpGet("http://localhost:8080/"); + assertEquals("GET:http://localhost:8080/", extractor.extractor(request)); + } + + @Test + public void testRelativePath() { + HttpGet request = new HttpGet("/api/users"); + assertEquals("GET:/api/users", extractor.extractor(request)); + } + + @Test + public void testRelativePathWithQuery() { + HttpGet request = new HttpGet("/api/users?page=1"); + assertEquals("GET:/api/users", extractor.extractor(request)); + } +} diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/ApacheHttpClientFallbackTest.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/ApacheHttpClientFallbackTest.java new file mode 100644 index 0000000000..86d9be8862 --- /dev/null +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/fallback/ApacheHttpClientFallbackTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2020 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.apache.httpclient5.fallback; + +import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; +import com.alibaba.csp.sentinel.slots.block.flow.FlowException; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.junit.Test; + +/** + * @author qihuai.wyq + */ +public class ApacheHttpClientFallbackTest { + + @Test(expected = SentinelRpcException.class) + public void testDefaultFallbackThrowsException() { + DefaultApacheHttpClientFallback fallback = new DefaultApacheHttpClientFallback(); + HttpGet request = new HttpGet("http://localhost:8080/test"); + fallback.handle(request, new FlowException("test")); + } +} From d891c489455c00634e6052adc3f9b425f5eda400 Mon Sep 17 00:00:00 2001 From: uuuyuqi Date: Tue, 28 Apr 2026 11:06:43 +0800 Subject: [PATCH 2/2] fix: remove user input reflection in test controller to resolve CodeQL XSS alert Change-Id: Idadec9e77dbebc0d64dee5bd873b4c47b42f4048 Co-developed-by: Cursor --- .../apache/httpclient5/app/controller/TestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/controller/TestController.java b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/controller/TestController.java index a1a4432451..f1e1eada37 100644 --- a/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/controller/TestController.java +++ b/sentinel-adapter/sentinel-apache-httpclient5-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/apache/httpclient5/app/controller/TestController.java @@ -32,7 +32,7 @@ public String back() { @GetMapping("/httpclient/back/{id}") public String back(@PathVariable String id) { - return "Welcome Back! " + id; + return "Welcome Back!"; } @GetMapping("/httpclient/query")