├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ ├── codes │ └── rafael │ │ └── interceptablehttpclient │ │ ├── InterceptableHttpClient.java │ │ ├── InterceptableHttpClientBuilder.java │ │ └── Interceptor.java │ └── module-info.java └── test └── java └── codes └── rafael └── interceptablehttpclient └── InterceptableHttpClientTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # Intellij 7 | .idea/ 8 | *.iml 9 | *.iws 10 | 11 | # Mac 12 | .DS_Store 13 | 14 | # Maven 15 | log/ 16 | target/ 17 | site/ 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Interception extension for Java's HttpClient 2 | -------------------------------------------- 3 | 4 | This module adds a common feature of HTTP client implementations to Java 11's `java.net.http.HttpClient`: 5 | 6 | - Adding request decorators, to for example add or modify request headers. 7 | - Adding request interception, to for example collect request metrics. 8 | 9 | The extension is straight-forward to use by simply replacing `HttpClient.builder()` by `InterceptableHttpClient.builder()`. The extended builder API is then capable of registering decorators and interceptors by the two additional build steps `addDecorator` and `addInterceptor`. 10 | 11 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | codes.rafael.interceptablehttpclient 6 | interceptable-http-client 7 | 1.1-SNAPSHOT 8 | 9 | 10 | 11 | junit 12 | junit 13 | 4.13 14 | test 15 | 16 | 17 | 18 | com.github.tomakehurst 19 | wiremock 20 | 2.26.0 21 | test 22 | 23 | 24 | 25 | 2020 26 | 27 | An interception and decoration wrapper for Java's HttpClient 28 | Allows for the registration of request decorators and interceptors 29 | https://github.com/raphw/interceptable-http-client 30 | 31 | 32 | 33 | The Apache Software License, Version 2.0 34 | http://www.apache.org/licenses/LICENSE-2.0.txt 35 | repo 36 | A business-friendly OSS license 37 | 38 | 39 | 40 | 41 | 42 | raphw 43 | Rafael Winterhalter 44 | rafael.wth@gmail.com 45 | http://rafael.codes 46 | 47 | developer 48 | 49 | +1 50 | 51 | 52 | 53 | 54 | 55 | org.sonatype.oss 56 | oss-parent 57 | 7 58 | 59 | 60 | 61 | github.com 62 | https://github.com/raphw/interceptable-http-client/issues 63 | 64 | 65 | 66 | scm:git:git@github.com:raphw/interceptable-http-client.git 67 | scm:git:git@github.com:raphw/interceptable-http-client.git 68 | git@github.com:raphw/interceptable-http-client.git 69 | HEAD 70 | 71 | 72 | 73 | 74 | bintray 75 | https://api.bintray.com/maven/raphw/maven/interceptable-http-client 76 | 77 | 78 | 79 | 80 | 81 | 82 | org.apache.maven.plugins 83 | maven-compiler-plugin 84 | 3.8.0 85 | 86 | 11 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-release-plugin 92 | 2.5.3 93 | 94 | false 95 | extras,gpg 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | extras 104 | 105 | false 106 | 107 | 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-source-plugin 112 | 3.2.1 113 | 114 | 115 | 116 | jar 117 | 118 | 119 | 120 | 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-javadoc-plugin 125 | 3.1.1 126 | 127 | 128 | attach-javadoc 129 | 130 | jar 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | gpg 141 | 142 | false 143 | 144 | 145 | 146 | 147 | org.apache.maven.plugins 148 | maven-gpg-plugin 149 | 1.6 150 | 151 | 152 | verify 153 | 154 | sign 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /src/main/java/codes/rafael/interceptablehttpclient/InterceptableHttpClient.java: -------------------------------------------------------------------------------- 1 | package codes.rafael.interceptablehttpclient; 2 | 3 | import javax.net.ssl.SSLContext; 4 | import javax.net.ssl.SSLParameters; 5 | import java.io.IOException; 6 | import java.net.Authenticator; 7 | import java.net.CookieHandler; 8 | import java.net.ProxySelector; 9 | import java.net.http.HttpClient; 10 | import java.net.http.HttpRequest; 11 | import java.net.http.HttpResponse; 12 | import java.time.Duration; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Optional; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.Executor; 18 | import java.util.function.BiConsumer; 19 | import java.util.function.Consumer; 20 | import java.util.function.Function; 21 | 22 | public class InterceptableHttpClient extends HttpClient { 23 | 24 | private final HttpClient client; 25 | 26 | private final List> decorators; 27 | 28 | private final List> interceptors; 29 | 30 | public static Builder builder() { 31 | return new InterceptableHttpClientBuilder(); 32 | } 33 | 34 | InterceptableHttpClient( 35 | HttpClient client, 36 | List> decorators, 37 | List> interceptors) { 38 | this.client = client; 39 | this.decorators = decorators; 40 | this.interceptors = interceptors; 41 | } 42 | 43 | @Override 44 | public Optional cookieHandler() { 45 | return client.cookieHandler(); 46 | } 47 | 48 | @Override 49 | public Optional connectTimeout() { 50 | return client.connectTimeout(); 51 | } 52 | 53 | @Override 54 | public Redirect followRedirects() { 55 | return client.followRedirects(); 56 | } 57 | 58 | @Override 59 | public Optional proxy() { 60 | return client.proxy(); 61 | } 62 | 63 | @Override 64 | public SSLContext sslContext() { 65 | return client.sslContext(); 66 | } 67 | 68 | @Override 69 | public SSLParameters sslParameters() { 70 | return client.sslParameters(); 71 | } 72 | 73 | @Override 74 | public Optional authenticator() { 75 | return client.authenticator(); 76 | } 77 | 78 | @Override 79 | public Version version() { 80 | return client.version(); 81 | } 82 | 83 | @Override 84 | public Optional executor() { 85 | return client.executor(); 86 | } 87 | 88 | @Override 89 | @SuppressWarnings("unchecked") 90 | public HttpResponse send(HttpRequest httpRequest, HttpResponse.BodyHandler bodyHandler) throws IOException, InterruptedException { 91 | httpRequest = decorate(httpRequest); 92 | if (interceptors.isEmpty()) { 93 | return client.send(httpRequest, bodyHandler); 94 | } else { 95 | List values = new ArrayList<>(interceptors.size()); 96 | for (Interceptor interceptor : interceptors) { 97 | try { 98 | values.add(interceptor.getOnRequest().apply(httpRequest)); 99 | } catch (Throwable ignored) { } 100 | } 101 | try { 102 | HttpResponse response = client.send(httpRequest, bodyHandler); 103 | for (int index = 0; index < interceptors.size(); index++) { 104 | BiConsumer onResponse = interceptors.get(index).getOnResponse(); 105 | try { 106 | onResponse.accept(response, values.get(index)); 107 | } catch (Throwable ignored) { } 108 | } 109 | return response; 110 | } catch (IOException exception) { 111 | for (int index = 0; index < interceptors.size(); index++) { 112 | BiConsumer onResponse = interceptors.get(index).getOnError(); 113 | try { 114 | onResponse.accept(exception, values.get(index)); 115 | } catch (Throwable ignored) { } 116 | } 117 | throw exception; 118 | } 119 | } 120 | } 121 | 122 | @Override 123 | public CompletableFuture> sendAsync(HttpRequest httpRequest, HttpResponse.BodyHandler bodyHandler) { 124 | return sendAsync(httpRequest, bodyHandler, null); 125 | } 126 | 127 | @Override 128 | @SuppressWarnings("unchecked") 129 | public CompletableFuture> sendAsync(HttpRequest httpRequest, HttpResponse.BodyHandler bodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) { 130 | httpRequest = decorate(httpRequest); 131 | if (interceptors.isEmpty()) { 132 | return client.sendAsync(httpRequest, bodyHandler); 133 | } else { 134 | List values = new ArrayList<>(interceptors.size()); 135 | for (Interceptor interceptor : interceptors) { 136 | values.add(interceptor.getOnRequest().apply(httpRequest)); 137 | } 138 | return client.sendAsync(httpRequest, bodyHandler, pushPromiseHandler).handle((response, throwable) -> { 139 | for (int index = 0; index < interceptors.size(); index++) { 140 | if (throwable == null) { 141 | BiConsumer onResponse = interceptors.get(index).getOnResponse(); 142 | onResponse.accept(response, values.get(index)); 143 | } else { 144 | BiConsumer onResponse = interceptors.get(index).getOnError(); 145 | onResponse.accept(throwable, values.get(index)); 146 | } 147 | } 148 | return response; 149 | }); 150 | } 151 | } 152 | 153 | private HttpRequest decorate(HttpRequest httpRequest) { 154 | if (decorators.isEmpty()) { 155 | return httpRequest; 156 | } 157 | HttpRequest.Builder builder = HttpRequest.newBuilder(httpRequest.uri()); 158 | builder.expectContinue(httpRequest.expectContinue()); 159 | httpRequest.headers().map().forEach((key, values) -> values.forEach(value -> builder.header(key, value))); 160 | httpRequest.bodyPublisher().ifPresentOrElse( 161 | publisher -> builder.method(httpRequest.method(), publisher), 162 | () -> { 163 | switch (httpRequest.method()) { 164 | case "GET": 165 | builder.GET(); 166 | break; 167 | case "DELETE": 168 | builder.DELETE(); 169 | break; 170 | default: 171 | throw new IllegalStateException(httpRequest.method()); 172 | } 173 | } 174 | ); 175 | httpRequest.timeout().ifPresent(builder::timeout); 176 | httpRequest.version().ifPresent(builder::version); 177 | for (Consumer decorator : decorators) { 178 | decorator.accept(builder); 179 | } 180 | return builder.build(); 181 | } 182 | 183 | public interface Builder extends HttpClient.Builder { 184 | 185 | @Override 186 | Builder cookieHandler(CookieHandler cookieHandler); 187 | 188 | @Override 189 | Builder connectTimeout(Duration duration); 190 | 191 | @Override 192 | Builder sslContext(SSLContext sslContext); 193 | 194 | @Override 195 | Builder sslParameters(SSLParameters sslParameters); 196 | 197 | @Override 198 | Builder executor(Executor executor); 199 | 200 | @Override 201 | Builder followRedirects(Redirect redirect); 202 | 203 | @Override 204 | Builder version(Version version); 205 | 206 | @Override 207 | Builder priority(int i); 208 | 209 | @Override 210 | Builder proxy(ProxySelector proxySelector); 211 | 212 | @Override 213 | Builder authenticator(Authenticator authenticator); 214 | 215 | Builder decorator(Consumer decorator); 216 | 217 | Builder interceptor(Function onRequest, BiConsumer, T> onResponse, BiConsumer onError); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/main/java/codes/rafael/interceptablehttpclient/InterceptableHttpClientBuilder.java: -------------------------------------------------------------------------------- 1 | package codes.rafael.interceptablehttpclient; 2 | 3 | import javax.net.ssl.SSLContext; 4 | import javax.net.ssl.SSLParameters; 5 | import java.net.Authenticator; 6 | import java.net.CookieHandler; 7 | import java.net.ProxySelector; 8 | import java.net.http.HttpClient; 9 | import java.net.http.HttpRequest; 10 | import java.net.http.HttpResponse; 11 | import java.time.Duration; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | import java.util.concurrent.Executor; 17 | import java.util.function.BiConsumer; 18 | import java.util.function.Consumer; 19 | import java.util.function.Function; 20 | 21 | class InterceptableHttpClientBuilder implements InterceptableHttpClient.Builder { 22 | 23 | private final HttpClient.Builder builder = HttpClient.newBuilder(); 24 | 25 | private final List> decorators = new ArrayList<>(); 26 | 27 | private final List> interceptors = new ArrayList<>(); 28 | 29 | @Override 30 | public InterceptableHttpClient.Builder cookieHandler(CookieHandler cookieHandler) { 31 | builder.cookieHandler(cookieHandler); 32 | return this; 33 | } 34 | 35 | @Override 36 | public InterceptableHttpClient.Builder connectTimeout(Duration duration) { 37 | builder.connectTimeout(duration); 38 | return this; 39 | } 40 | 41 | @Override 42 | public InterceptableHttpClient.Builder sslContext(SSLContext sslContext) { 43 | builder.sslContext(sslContext); 44 | return this; 45 | } 46 | 47 | @Override 48 | public InterceptableHttpClient.Builder sslParameters(SSLParameters sslParameters) { 49 | builder.sslParameters(sslParameters); 50 | return this; 51 | } 52 | 53 | @Override 54 | public InterceptableHttpClient.Builder executor(Executor executor) { 55 | builder.executor(executor); 56 | return this; 57 | } 58 | 59 | @Override 60 | public InterceptableHttpClient.Builder followRedirects(HttpClient.Redirect redirect) { 61 | builder.followRedirects(redirect); 62 | return this; 63 | } 64 | 65 | @Override 66 | public InterceptableHttpClient.Builder version(HttpClient.Version version) { 67 | builder.version(version); 68 | return this; 69 | } 70 | 71 | @Override 72 | public InterceptableHttpClient.Builder priority(int priority) { 73 | builder.priority(priority); 74 | return this; 75 | } 76 | 77 | @Override 78 | public InterceptableHttpClient.Builder proxy(ProxySelector proxySelector) { 79 | builder.proxy(proxySelector); 80 | return this; 81 | } 82 | 83 | @Override 84 | public InterceptableHttpClient.Builder authenticator(Authenticator authenticator) { 85 | builder.authenticator(authenticator); 86 | return this; 87 | } 88 | 89 | @Override 90 | public InterceptableHttpClient.Builder decorator(Consumer decorator) { 91 | Objects.requireNonNull(decorator, "decorator"); 92 | decorators.add(decorator); 93 | return this; 94 | } 95 | 96 | @Override 97 | public InterceptableHttpClient.Builder interceptor( 98 | Function onRequest, 99 | BiConsumer, T> onResponse, 100 | BiConsumer onError) { 101 | Objects.requireNonNull(onRequest, "onRequest"); 102 | Objects.requireNonNull(onResponse, "onResponse"); 103 | Objects.requireNonNull(onError, "onError"); 104 | interceptors.add(new Interceptor<>(onRequest, onResponse, onError)); 105 | return this; 106 | } 107 | 108 | @Override 109 | public HttpClient build() { 110 | return new InterceptableHttpClient(builder.build(), new ArrayList<>(decorators), new ArrayList<>(interceptors)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/codes/rafael/interceptablehttpclient/Interceptor.java: -------------------------------------------------------------------------------- 1 | package codes.rafael.interceptablehttpclient; 2 | 3 | import java.net.http.HttpRequest; 4 | import java.net.http.HttpResponse; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.Function; 7 | 8 | class Interceptor { 9 | 10 | private final Function onRequest; 11 | 12 | private final BiConsumer, T> onResponse; 13 | 14 | private final BiConsumer onError; 15 | 16 | Interceptor(Function onRequest, BiConsumer, T> onResponse, BiConsumer onError) { 17 | this.onRequest = onRequest; 18 | this.onResponse = onResponse; 19 | this.onError = onError; 20 | } 21 | 22 | Function getOnRequest() { 23 | return onRequest; 24 | } 25 | 26 | BiConsumer, T> getOnResponse() { 27 | return onResponse; 28 | } 29 | 30 | BiConsumer getOnError() { 31 | return onError; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module codes.rafael.interceptablehttpclient { 2 | requires java.net.http; 3 | exports codes.rafael.interceptablehttpclient; 4 | } -------------------------------------------------------------------------------- /src/test/java/codes/rafael/interceptablehttpclient/InterceptableHttpClientTest.java: -------------------------------------------------------------------------------- 1 | package codes.rafael.interceptablehttpclient; 2 | 3 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | 7 | import java.io.IOException; 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.net.http.HttpClient; 11 | import java.net.http.HttpRequest; 12 | import java.net.http.HttpResponse; 13 | import java.util.concurrent.ExecutionException; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.TimeoutException; 16 | import java.util.concurrent.atomic.AtomicReference; 17 | 18 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 19 | import static junit.framework.TestCase.assertEquals; 20 | import static junit.framework.TestCase.fail; 21 | 22 | public class InterceptableHttpClientTest { 23 | 24 | @Rule 25 | public WireMockRule wireMock = new WireMockRule(); 26 | 27 | @Test 28 | public void can_decorate_request() throws URISyntaxException, IOException, InterruptedException { 29 | stubFor(get(urlEqualTo("/")) 30 | .withHeader("foo", equalTo("bar")) 31 | .willReturn(aResponse().withStatus(200).withBody("qux"))); 32 | 33 | HttpClient client = InterceptableHttpClient.builder().decorator(builder -> builder.header("foo", "bar")).build(); 34 | 35 | HttpResponse response = client.send(HttpRequest.newBuilder(new URI("http://localhost:" + wireMock.port())) 36 | .GET() 37 | .build(), HttpResponse.BodyHandlers.ofString()); 38 | 39 | assertEquals("qux", response.body()); 40 | } 41 | 42 | @Test 43 | public void can_decorate_async_request() throws URISyntaxException, InterruptedException, ExecutionException, TimeoutException { 44 | stubFor(get(urlEqualTo("/")) 45 | .withHeader("foo", equalTo("bar")) 46 | .willReturn(aResponse().withStatus(200).withBody("qux"))); 47 | 48 | HttpClient client = InterceptableHttpClient.builder().decorator(builder -> builder.header("foo", "bar")).build(); 49 | 50 | HttpResponse response = client.sendAsync(HttpRequest.newBuilder(new URI("http://localhost:" + wireMock.port())) 51 | .GET() 52 | .build(), HttpResponse.BodyHandlers.ofString()).get(1, TimeUnit.SECONDS); 53 | 54 | assertEquals("qux", response.body()); 55 | } 56 | 57 | @Test 58 | public void can_intercept_request() throws URISyntaxException, IOException, InterruptedException { 59 | stubFor(get(urlEqualTo("/")).willReturn(aResponse().withStatus(200).withBody("qux"))); 60 | 61 | AtomicReference value = new AtomicReference<>(); 62 | HttpClient client = InterceptableHttpClient.builder().interceptor( 63 | builder -> "baz", (response, payload) -> value.set(payload), (throwable, payload) -> fail() 64 | ).build(); 65 | 66 | HttpResponse response = client.send(HttpRequest.newBuilder(new URI("http://localhost:" + wireMock.port())) 67 | .GET() 68 | .build(), HttpResponse.BodyHandlers.ofString()); 69 | 70 | assertEquals("qux", response.body()); 71 | 72 | assertEquals("baz", value.get()); 73 | } 74 | 75 | @Test 76 | public void can_intercept_async_request() throws URISyntaxException, InterruptedException, ExecutionException, TimeoutException { 77 | stubFor(get(urlEqualTo("/")).willReturn(aResponse().withStatus(200).withBody("qux"))); 78 | 79 | AtomicReference value = new AtomicReference<>(); 80 | HttpClient client = InterceptableHttpClient.builder().interceptor( 81 | builder -> "baz", (response, payload) -> value.set(payload), (throwable, payload) -> fail() 82 | ).build(); 83 | 84 | HttpResponse response = client.sendAsync(HttpRequest.newBuilder(new URI("http://localhost:" + wireMock.port())) 85 | .GET() 86 | .build(), HttpResponse.BodyHandlers.ofString()).get(1, TimeUnit.SECONDS); 87 | 88 | assertEquals("qux", response.body()); 89 | 90 | assertEquals("baz", value.get()); 91 | } 92 | } --------------------------------------------------------------------------------