You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by gr...@apache.org on 2022/08/11 21:26:02 UTC

[nifi] branch main updated: NIFI-10244 Added nifi-web-client-api and implementation

This is an automated email from the ASF dual-hosted git repository.

greyp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 864036674e NIFI-10244 Added nifi-web-client-api and implementation
864036674e is described below

commit 864036674ec101c81701bbd8fbb7f96ad25dd3be
Author: exceptionfactory <ex...@apache.org>
AuthorDate: Tue Jul 19 23:15:57 2022 -0500

    NIFI-10244 Added nifi-web-client-api and implementation
    
    - Added nifi-web-client implementation based on OkHttp
    - Added WebClientServiceProvider Controller Service interface and implementation
    - Corrected comments and added unmodifiableMap wrapper
    - Added getHeaderNames() and corrected ProxyContext comments
    
    This closes #6268
    Signed-off-by: Paul Grey <gr...@apache.org>
---
 nifi-assembly/pom.xml                              |   6 +
 nifi-commons/nifi-web-client-api/pom.xml           |  25 ++
 .../nifi/web/client/api/HttpEntityHeaders.java     |  49 +++
 .../nifi/web/client/api/HttpRequestBodySpec.java   |  34 ++
 .../web/client/api/HttpRequestHeadersSpec.java     |  38 ++
 .../nifi/web/client/api/HttpRequestMethod.java     |  29 ++
 .../nifi/web/client/api/HttpRequestUriSpec.java    |  32 ++
 .../nifi/web/client/api/HttpResponseEntity.java    |  46 +++
 .../nifi/web/client/api/HttpResponseStatus.java    |  58 +++
 .../apache/nifi/web/client/api/HttpUriBuilder.java |  80 ++++
 .../web/client/api/StandardHttpRequestMethod.java  |  37 ++
 .../nifi/web/client/api/WebClientService.java      |  65 +++
 .../web/client/api/WebClientServiceException.java  |  45 +++
 nifi-commons/nifi-web-client/pom.xml               |  42 ++
 .../nifi/web/client/BasicProxyAuthenticator.java   |  43 ++
 .../nifi/web/client/InputStreamRequestBody.java    |  56 +++
 .../nifi/web/client/StandardHttpEntityHeaders.java |  55 +++
 .../web/client/StandardHttpResponseEntity.java     |  64 +++
 .../nifi/web/client/StandardHttpUriBuilder.java    |  80 ++++
 .../nifi/web/client/StandardWebClientService.java  | 303 ++++++++++++++
 .../apache/nifi/web/client/proxy/ProxyContext.java |  46 +++
 .../nifi/web/client/redirect/RedirectHandling.java |  28 ++
 .../web/client/ssl/SSLSocketFactoryProvider.java   |  32 ++
 .../ssl/StandardSSLSocketFactoryProvider.java      |  70 ++++
 .../org/apache/nifi/web/client/ssl/TlsContext.java |  47 +++
 .../web/client/StandardHttpUriBuilderTest.java     | 134 +++++++
 .../web/client/StandardWebClientServiceTest.java   | 434 +++++++++++++++++++++
 nifi-commons/pom.xml                               |   2 +
 .../nifi-standard-services-api-nar/pom.xml         |   6 +
 .../nifi-web-client-provider-api/pom.xml           |  35 ++
 .../provider/api/WebClientServiceProvider.java     |  40 ++
 .../nifi-web-client-provider-service-nar/pom.xml   |  38 ++
 .../nifi-web-client-provider-service/pom.xml       |  72 ++++
 .../provider/service/KeyManagerProvider.java       |  35 ++
 .../service/StandardKeyManagerProvider.java        | 109 ++++++
 .../service/StandardWebClientServiceProvider.java  | 206 ++++++++++
 .../org.apache.nifi.controller.ControllerService   |  15 +
 .../service/StandardKeyManagerProviderTest.java    |  76 ++++
 .../StandardWebClientServiceProviderTest.java      | 245 ++++++++++++
 .../nifi-web-client-provider-bundle/pom.xml        |  30 ++
 nifi-nar-bundles/nifi-standard-services/pom.xml    |   1 +
 41 files changed, 2888 insertions(+)

diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml
index caf87d0a18..c50e2ca7fe 100644
--- a/nifi-assembly/pom.xml
+++ b/nifi-assembly/pom.xml
@@ -844,6 +844,12 @@ language governing permissions and limitations under the License. -->
             <version>1.18.0-SNAPSHOT</version>
             <type>nar</type>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-client-provider-service-nar</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <type>nar</type>
+        </dependency>
         <!-- dependencies for jaxb/activation/annotation for running NiFi on Java 11 -->
         <!-- TODO: remove these once minimum Java version is 11 -->
         <dependency>
diff --git a/nifi-commons/nifi-web-client-api/pom.xml b/nifi-commons/nifi-web-client-api/pom.xml
new file mode 100644
index 0000000000..b1d67b506c
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/pom.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-commons</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-web-client-api</artifactId>
+    <description>Abstracts standard HTTP client operations without depending on a specific HTTP library</description>
+</project>
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java
new file mode 100644
index 0000000000..a402de153e
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * HTTP Entity Headers supporting retrieval of single or multiple header values
+ */
+public interface HttpEntityHeaders {
+    /**
+     * Get First Header using specified Header Name
+     *
+     * @param headerName Header Name to be retrieved
+     * @return First Header Value or empty when not found
+     */
+    Optional<String> getFirstHeader(String headerName);
+
+    /**
+     * Get Header Values using specified Header Name
+     *
+     * @param headerName Header Name to be retrieved
+     * @return List of Header Values or empty when not found
+     */
+    List<String> getHeader(String headerName);
+
+    /**
+     * Get Header Names
+     *
+     * @return Collection of Header Names or empty when not found
+     */
+    Collection<String> getHeaderNames();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java
new file mode 100644
index 0000000000..9ea80bab4d
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+import java.io.InputStream;
+import java.util.OptionalLong;
+
+/**
+ * HTTP Request Body Specification builder
+ */
+public interface HttpRequestBodySpec extends HttpRequestHeadersSpec {
+    /**
+     * Set Request Body as stream
+     *
+     * @param inputStream Request Body stream is required
+     * @param contentLength Content Length or empty when not known
+     * @return HTTP Request Headers Specification builder
+     */
+    HttpRequestHeadersSpec body(InputStream inputStream, OptionalLong contentLength);
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java
new file mode 100644
index 0000000000..8ed965cff5
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+/**
+ * HTTP Request Headers Specification builder
+ */
+public interface HttpRequestHeadersSpec {
+    /**
+     * Add HTTP Request Header using specified name and value
+     *
+     * @param headerName HTTP Header Name
+     * @param headerValue HTTP Header Value
+     * @return HTTP Request Body Specification builder
+     */
+    HttpRequestBodySpec header(String headerName, String headerValue);
+
+    /**
+     * Execute HTTP Request and retrieve HTTP Response
+     *
+     * @return HTTP Response Entity
+     */
+    HttpResponseEntity retrieve();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java
new file mode 100644
index 0000000000..80f94864d7
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+/**
+ * HTTP Request Method
+ */
+public interface HttpRequestMethod {
+    /**
+     * Get HTTP Method
+     *
+     * @return HTTP Method
+     */
+    String getMethod();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java
new file mode 100644
index 0000000000..cad4cd74d3
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+import java.net.URI;
+
+/**
+ * HTTP Request URI Specification builder
+ */
+public interface HttpRequestUriSpec {
+    /**
+     * Create HTTP Request Body builder using specified Request URI
+     *
+     * @param uri Request URI
+     * @return HTTP Request Body Specification builder
+     */
+    HttpRequestBodySpec uri(URI uri);
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java
new file mode 100644
index 0000000000..f0386aece1
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+import java.io.Closeable;
+import java.io.InputStream;
+
+/**
+ * HTTP Response Entity extends Closeable to handle closing Response Body
+ */
+public interface HttpResponseEntity extends Closeable {
+    /**
+     * Get HTTP Response Status Code
+     *
+     * @return HTTP Response Status Code
+     */
+    int statusCode();
+
+    /**
+     * Get HTTP Response Headers
+     *
+     * @return HTTP Response Headers
+     */
+    HttpEntityHeaders headers();
+
+    /**
+     * Get HTTP Response Body stream
+     *
+     * @return HTTP Response Body stream can be empty
+     */
+    InputStream body();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java
new file mode 100644
index 0000000000..915cf3cb63
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+/**
+ * Enumeration of Standard HTTP Response Status Codes
+ */
+public enum HttpResponseStatus {
+    OK(200),
+
+    CREATED(201),
+
+    ACCEPTED(202),
+
+    NO_CONTENT(204),
+
+    MOVED_PERMANENTLY(301),
+
+    BAD_REQUEST(400),
+
+    UNAUTHORIZED(401),
+
+    FORBIDDEN(403),
+
+    NOT_FOUND(404),
+
+    METHOD_NOT_ALLOWED(405),
+
+    PROXY_AUTHENTICATION_REQUIRED(407),
+
+    INTERNAL_SERVER_ERROR(500),
+
+    SERVICE_UNAVAILABLE(503);
+
+    private final int code;
+
+    HttpResponseStatus(final int code) {
+        this.code = code;
+    }
+
+    public int getCode() {
+        return code;
+    }
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java
new file mode 100644
index 0000000000..95db442ec9
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+import java.net.URI;
+
+/**
+ * HTTP URI Builder supports construction of a URI using component elements
+ */
+public interface HttpUriBuilder {
+    /**
+     * Build URI based on current component elements
+     *
+     * @return URI
+     */
+    URI build();
+
+    /**
+     * Set URI scheme as http or https
+     *
+     * @param scheme URI scheme
+     * @return Builder
+     */
+    HttpUriBuilder scheme(String scheme);
+
+    /**
+     * Set URI host address
+     *
+     * @param host Host address
+     * @return Builder
+     */
+    HttpUriBuilder host(String host);
+
+    /**
+     * Set URI port number
+     *
+     * @param port Port number
+     * @return Builder
+     */
+    HttpUriBuilder port(int port);
+
+    /**
+     * Set path with segments encoded according to URL standard requirements
+     *
+     * @param encodedPath URL-encoded path
+     * @return Builder
+     */
+    HttpUriBuilder encodedPath(String encodedPath);
+
+    /**
+     * Add path segment appending to current path
+     *
+     * @param pathSegment Path segment
+     * @return Builder
+     */
+    HttpUriBuilder addPathSegment(String pathSegment);
+
+    /**
+     * Add query parameter using specified name and value
+     *
+     * @param name Query parameter name
+     * @param value Query parameter value can be null
+     * @return Builder
+     */
+    HttpUriBuilder addQueryParameter(String name, String value);
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java
new file mode 100644
index 0000000000..feb24af2b6
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+/**
+ * Enumeration of standard HTTP Request Methods
+ */
+public enum StandardHttpRequestMethod implements HttpRequestMethod {
+    DELETE,
+
+    GET,
+
+    PATCH,
+
+    POST,
+
+    PUT;
+
+    @Override
+    public String getMethod() {
+        return name();
+    }
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java
new file mode 100644
index 0000000000..bc33a72da4
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+/**
+ * Service abstraction for HTTP client operations
+ */
+public interface WebClientService {
+    /**
+     * Create HTTP Request builder starting with specified HTTP Request Method
+     *
+     * @param requestMethod HTTP Request Method
+     * @return HTTP Request URI Specification builder
+     */
+    HttpRequestUriSpec method(HttpRequestMethod requestMethod);
+
+    /**
+     * Create HTTP Request builder starting with HTTP DELETE
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    HttpRequestUriSpec delete();
+
+    /**
+     * Create HTTP Request builder starting with HTTP GET
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    HttpRequestUriSpec get();
+
+    /**
+     * Create HTTP Request builder starting with HTTP PATCH
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    HttpRequestUriSpec patch();
+
+    /**
+     * Create HTTP Request builder starting with HTTP POST
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    HttpRequestUriSpec post();
+
+    /**
+     * Create HTTP Request builder starting with HTTP PUT
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    HttpRequestUriSpec put();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java
new file mode 100644
index 0000000000..95f7966cb4
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.api;
+
+import java.net.URI;
+
+/**
+ * Web Client Service Exception provides a generalized wrapper for HTTP communication failures
+ */
+public class WebClientServiceException extends RuntimeException {
+    /**
+     * Web Service Client Exception with standard HTTP request properties
+     *
+     * @param message Failure message
+     * @param cause Failure cause
+     * @param uri HTTP Request URI
+     * @param httpRequestMethod HTTP Request Method
+     */
+    public WebClientServiceException(
+            final String message,
+            final Throwable cause,
+            final URI uri,
+            final HttpRequestMethod httpRequestMethod
+    ) {
+        super(getMessage(message, uri, httpRequestMethod), cause);
+    }
+
+    private static String getMessage(final String message, final URI uri, final HttpRequestMethod httpRequestMethod) {
+        return String.format("%s HTTP Method [%s] URI [%s]", message, httpRequestMethod, uri);
+    }
+}
diff --git a/nifi-commons/nifi-web-client/pom.xml b/nifi-commons/nifi-web-client/pom.xml
new file mode 100644
index 0000000000..ac8563aed6
--- /dev/null
+++ b/nifi-commons/nifi-web-client/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-commons</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-web-client</artifactId>
+    <description>Standard implementation of nifi-web-client-api using OkHttp</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-client-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>mockwebserver</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java
new file mode 100644
index 0000000000..2377812cbb
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client;
+
+import okhttp3.Authenticator;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.Route;
+
+/**
+ * OkHttp Authenticator supporting Proxy Authentication using HTTP Basic credentials
+ */
+class BasicProxyAuthenticator implements Authenticator {
+    private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
+
+    private final String credentials;
+
+    BasicProxyAuthenticator(final String credentials) {
+        this.credentials = credentials;
+    }
+
+    @Override
+    public Request authenticate(final Route route, final Response response) {
+        return response.request()
+                .newBuilder()
+                .header(PROXY_AUTHORIZATION_HEADER, credentials)
+                .build();
+    }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java
new file mode 100644
index 0000000000..68ab5183da
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client;
+
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Source;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * OkHttp Request Body implementation based on an InputStream
+ */
+class InputStreamRequestBody extends RequestBody {
+    private final InputStream inputStream;
+
+    private final long contentLength;
+
+    InputStreamRequestBody(final InputStream inputStream, final long contentLength) {
+        this.inputStream = inputStream;
+        this.contentLength = contentLength;
+    }
+
+    @Override
+    public long contentLength() {
+        return contentLength;
+    }
+
+    @Override
+    public MediaType contentType() {
+        return null;
+    }
+
+    @Override
+    public void writeTo(final BufferedSink bufferedSink) throws IOException {
+        final Source source = Okio.source(inputStream);
+        bufferedSink.writeAll(source);
+    }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java
new file mode 100644
index 0000000000..26f84e0319
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client;
+
+import org.apache.nifi.web.client.api.HttpEntityHeaders;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Standard implementation of HTTP Entity Headers for Standard Web Client Service
+ */
+class StandardHttpEntityHeaders implements HttpEntityHeaders {
+    private final Map<String, List<String>> headers;
+
+    StandardHttpEntityHeaders(final Map<String, List<String>> headers) {
+        this.headers = Collections.unmodifiableMap(headers);
+    }
+
+    @Override
+    public Optional<String> getFirstHeader(final String headerName) {
+        final List<String> values = getHeader(headerName);
+        return values.stream().findFirst();
+    }
+
+    @Override
+    public List<String> getHeader(final String headerName) {
+        Objects.requireNonNull(headerName, "Header Name required");
+        final List<String> values = headers.get(headerName);
+        return values == null ? Collections.emptyList() : Collections.unmodifiableList(values);
+    }
+
+    @Override
+    public Collection<String> getHeaderNames() {
+        return Collections.unmodifiableSet(headers.keySet());
+    }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java
new file mode 100644
index 0000000000..c1e8bd1825
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client;
+
+import org.apache.nifi.web.client.api.HttpEntityHeaders;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Standard implementation of HTTP Response Entity for Standard Web Client Service
+ */
+class StandardHttpResponseEntity implements HttpResponseEntity {
+    private final int statusCode;
+
+    private final HttpEntityHeaders headers;
+
+    private final InputStream body;
+
+    StandardHttpResponseEntity(
+            final int statusCode,
+            final HttpEntityHeaders headers,
+            final InputStream body
+    ) {
+        this.statusCode = statusCode;
+        this.headers = headers;
+        this.body = body;
+    }
+
+    @Override
+    public int statusCode() {
+        return statusCode;
+    }
+
+    @Override
+    public HttpEntityHeaders headers() {
+        return headers;
+    }
+
+    @Override
+    public InputStream body() {
+        return body;
+    }
+
+    @Override
+    public void close() throws IOException {
+        body.close();
+    }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java
new file mode 100644
index 0000000000..33ecfa3f46
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client;
+
+import okhttp3.HttpUrl;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * Standard HTTP URI Builder based on OkHttp HttpUrl
+ */
+public class StandardHttpUriBuilder implements HttpUriBuilder {
+    private final HttpUrl.Builder builder;
+
+    public StandardHttpUriBuilder() {
+        this.builder = new HttpUrl.Builder();
+    }
+
+    @Override
+    public URI build() {
+        final HttpUrl httpUrl = builder.build();
+        return httpUrl.uri();
+    }
+
+    @Override
+    public HttpUriBuilder scheme(final String scheme) {
+        Objects.requireNonNull(scheme, "Scheme required");
+        builder.scheme(scheme);
+        return this;
+    }
+
+    @Override
+    public HttpUriBuilder host(final String host) {
+        Objects.requireNonNull(host, "Host required");
+        builder.host(host);
+        return this;
+    }
+
+    @Override
+    public HttpUriBuilder port(int port) {
+        builder.port(port);
+        return this;
+    }
+
+    @Override
+    public HttpUriBuilder encodedPath(final String encodedPath) {
+        builder.encodedPath(encodedPath);
+        return this;
+    }
+
+    @Override
+    public HttpUriBuilder addPathSegment(final String pathSegment) {
+        Objects.requireNonNull(pathSegment, "Path segment required");
+        builder.addPathSegment(pathSegment);
+        return this;
+    }
+
+    @Override
+    public HttpUriBuilder addQueryParameter(final String name, final String value) {
+        Objects.requireNonNull(name, "Parameter name required");
+        builder.addQueryParameter(name, value);
+        return this;
+    }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java
new file mode 100644
index 0000000000..c0da84130b
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client;
+
+import okhttp3.Call;
+import okhttp3.Credentials;
+import okhttp3.Headers;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import org.apache.nifi.web.client.api.HttpEntityHeaders;
+import org.apache.nifi.web.client.api.HttpRequestBodySpec;
+import org.apache.nifi.web.client.api.HttpRequestHeadersSpec;
+import org.apache.nifi.web.client.api.HttpRequestMethod;
+import org.apache.nifi.web.client.api.HttpRequestUriSpec;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.StandardHttpRequestMethod;
+import org.apache.nifi.web.client.api.WebClientService;
+import org.apache.nifi.web.client.api.WebClientServiceException;
+import org.apache.nifi.web.client.proxy.ProxyContext;
+import org.apache.nifi.web.client.redirect.RedirectHandling;
+import org.apache.nifi.web.client.ssl.SSLSocketFactoryProvider;
+import org.apache.nifi.web.client.ssl.StandardSSLSocketFactoryProvider;
+import org.apache.nifi.web.client.ssl.TlsContext;
+
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Proxy;
+import java.net.URI;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalLong;
+
+/**
+ * Standard implementation of Web Client Service using OkHttp
+ */
+public class StandardWebClientService implements WebClientService {
+    private static final byte[] EMPTY_BYTES = new byte[0];
+
+    private static final SSLSocketFactoryProvider sslSocketFactoryProvider = new StandardSSLSocketFactoryProvider();
+
+    private OkHttpClient okHttpClient;
+
+    /**
+     * Standard Web Client Service constructor creates OkHttpClient using default settings
+     */
+    public StandardWebClientService() {
+        okHttpClient = new OkHttpClient.Builder().build();
+    }
+
+    /**
+     * Set timeout for initial socket connection
+     *
+     * @param connectTimeout Connect Timeout
+     */
+    public void setConnectTimeout(final Duration connectTimeout) {
+        Objects.requireNonNull(connectTimeout, "Connect Timeout required");
+        okHttpClient = okHttpClient.newBuilder().connectTimeout(connectTimeout).build();
+    }
+
+    /**
+     * Set timeout for reading responses from socket connection
+     *
+     * @param readTimeout Read Timeout
+     */
+    public void setReadTimeout(final Duration readTimeout) {
+        Objects.requireNonNull(readTimeout, "Read Timeout required");
+        okHttpClient = okHttpClient.newBuilder().readTimeout(readTimeout).build();
+    }
+
+    /**
+     * Set timeout for writing requests to socket connection
+     *
+     * @param writeTimeout Write Timeout
+     */
+    public void setWriteTimeout(final Duration writeTimeout) {
+        Objects.requireNonNull(writeTimeout, "Write Timeout required");
+        okHttpClient = okHttpClient.newBuilder().writeTimeout(writeTimeout).build();
+    }
+
+    /**
+     * Set Proxy Context configuration for socket communication
+     *
+     * @param proxyContext Proxy Context configuration
+     */
+    public void setProxyContext(final ProxyContext proxyContext) {
+        Objects.requireNonNull(proxyContext, "Proxy Context required");
+        final Proxy proxy = Objects.requireNonNull(proxyContext.getProxy(), "Proxy required");
+        okHttpClient = okHttpClient.newBuilder().proxy(proxy).build();
+
+        final Optional<String> proxyUsername = proxyContext.getUsername();
+        if (proxyUsername.isPresent()) {
+            final String username = proxyUsername.get();
+            final String password = proxyContext.getPassword().orElseThrow(() -> new IllegalArgumentException("Proxy password required"));
+            final String credentials = Credentials.basic(username, password);
+            final BasicProxyAuthenticator proxyAuthenticator = new BasicProxyAuthenticator(credentials);
+            okHttpClient = okHttpClient.newBuilder().proxyAuthenticator(proxyAuthenticator).build();
+        }
+    }
+
+    /**
+     * Set Redirect Handling strategy
+     *
+     * @param redirectHandling Redirect Handling strategy
+     */
+    public void setRedirectHandling(final RedirectHandling redirectHandling) {
+        Objects.requireNonNull(redirectHandling, "Redirect Handling required");
+        final boolean followRedirects = RedirectHandling.FOLLOWED == redirectHandling;
+        okHttpClient = okHttpClient.newBuilder().followRedirects(followRedirects).followSslRedirects(followRedirects).build();
+    }
+
+    /**
+     * Set TLS Context overrides system default TLS settings for HTTPS communication
+     *
+     * @param tlsContext TLS Context
+     */
+    public void setTlsContext(final TlsContext tlsContext) {
+        Objects.requireNonNull(tlsContext, "TLS Context required");
+        final X509TrustManager trustManager = Objects.requireNonNull(tlsContext.getTrustManager(), "Trust Manager required");
+        final SSLSocketFactory sslSocketFactory = sslSocketFactoryProvider.getSocketFactory(tlsContext);
+        okHttpClient = okHttpClient.newBuilder().sslSocketFactory(sslSocketFactory, trustManager).build();
+    }
+
+    /**
+     * Create HTTP Request builder starting with specified HTTP Request Method
+     *
+     * @param httpRequestMethod HTTP Request Method required
+     * @return HTTP Request URI Specification builder
+     */
+    @Override
+    public HttpRequestUriSpec method(final HttpRequestMethod httpRequestMethod) {
+        Objects.requireNonNull(httpRequestMethod, "HTTP Request Method required");
+        return new StandardHttpRequestUriSpec(httpRequestMethod);
+    }
+
+    /**
+     * Create HTTP Request builder starting with HTTP DELETE
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    @Override
+    public HttpRequestUriSpec delete() {
+        return method(StandardHttpRequestMethod.DELETE);
+    }
+
+    /**
+     * Create HTTP Request builder starting with HTTP GET
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    @Override
+    public HttpRequestUriSpec get() {
+        return method(StandardHttpRequestMethod.GET);
+    }
+
+    /**
+     * Create HTTP Request builder starting with HTTP PATCH
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    @Override
+    public HttpRequestUriSpec patch() {
+        return method(StandardHttpRequestMethod.PATCH);
+    }
+
+    /**
+     * Create HTTP Request builder starting with HTTP POST
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    public HttpRequestUriSpec post() {
+        return method(StandardHttpRequestMethod.POST);
+    }
+
+    /**
+     * Create HTTP Request builder starting with HTTP PUT
+     *
+     * @return HTTP Request URI Specification builder
+     */
+    public HttpRequestUriSpec put() {
+        return method(StandardHttpRequestMethod.PUT);
+    }
+
+    class StandardHttpRequestUriSpec implements HttpRequestUriSpec {
+        private final HttpRequestMethod httpRequestMethod;
+
+        StandardHttpRequestUriSpec(final HttpRequestMethod httpRequestMethod) {
+            this.httpRequestMethod = httpRequestMethod;
+        }
+
+        @Override
+        public HttpRequestBodySpec uri(final URI uri) {
+            Objects.requireNonNull(uri, "URI required");
+            return new StandardHttpRequestBodySpec(httpRequestMethod, uri);
+        }
+    }
+
+    class StandardHttpRequestBodySpec implements HttpRequestBodySpec {
+        private static final long UNKNOWN_CONTENT_LENGTH = -1;
+
+        private final HttpRequestMethod httpRequestMethod;
+
+        private final URI uri;
+
+        private final Headers.Builder headersBuilder;
+
+        private long contentLength = UNKNOWN_CONTENT_LENGTH;
+
+        private InputStream body;
+
+        StandardHttpRequestBodySpec(final HttpRequestMethod httpRequestMethod, final URI uri) {
+            this.httpRequestMethod = httpRequestMethod;
+            this.uri = uri;
+            this.headersBuilder = new Headers.Builder();
+        }
+
+        @Override
+        public HttpRequestHeadersSpec body(final InputStream body, final OptionalLong contentLength) {
+            this.body = Objects.requireNonNull(body, "Body required");
+            this.contentLength = Objects.requireNonNull(contentLength, "Content Length required").orElse(UNKNOWN_CONTENT_LENGTH);
+            return this;
+        }
+
+        @Override
+        public HttpRequestBodySpec header(final String headerName, final String headerValue) {
+            Objects.requireNonNull(headerName, "Header Name required");
+            Objects.requireNonNull(headerValue, "Header Value required");
+            headersBuilder.add(headerName, headerValue);
+            return this;
+        }
+
+        @Override
+        public HttpResponseEntity retrieve() {
+            final Request request = getRequest();
+            final Call call = okHttpClient.newCall(request);
+            final Response response = execute(call);
+
+            final int code = response.code();
+            final Headers responseHeaders = response.headers();
+            final HttpEntityHeaders headers = new StandardHttpEntityHeaders(responseHeaders.toMultimap());
+            final ResponseBody responseBody = response.body();
+            final InputStream body = responseBody == null ? new ByteArrayInputStream(EMPTY_BYTES) : responseBody.byteStream();
+
+            return new StandardHttpResponseEntity(code, headers, body);
+        }
+
+        private Response execute(final Call call) {
+            try {
+                return call.execute();
+            } catch (final IOException e) {
+                throw new WebClientServiceException("Request execution failed", e, uri, httpRequestMethod);
+            }
+        }
+
+        private Request getRequest() {
+            final HttpUrl url = HttpUrl.get(uri);
+            Objects.requireNonNull(url, "HTTP Request URI required");
+
+            final Headers headers = headersBuilder.build();
+            final RequestBody requestBody = getRequestBody();
+
+            return new Request.Builder()
+                    .method(httpRequestMethod.getMethod(), requestBody)
+                    .url(url)
+                    .headers(headers)
+                    .build();
+        }
+
+        private RequestBody getRequestBody() {
+            final RequestBody requestBody;
+
+            if (body == null) {
+                requestBody = null;
+            } else {
+                requestBody = new InputStreamRequestBody(body, contentLength);
+            }
+
+            return requestBody;
+        }
+    }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java
new file mode 100644
index 0000000000..570e587189
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.proxy;
+
+import java.net.Proxy;
+import java.util.Optional;
+
+/**
+ * Proxy Context provides information necessary to access sites through a Proxy with or without authentication
+ */
+public interface ProxyContext {
+    /**
+     * Get Proxy including Proxy Type and Proxy Server
+     *
+     * @return Proxy
+     */
+    Proxy getProxy();
+
+    /**
+     * Get Username for Proxy Authentication
+     *
+     * @return Username or empty when not configured
+     */
+    Optional<String> getUsername();
+
+    /**
+     * Get Password for Proxy Authentication
+     *
+     * @return Password or empty when not configured
+     */
+    Optional<String> getPassword();
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java
new file mode 100644
index 0000000000..f7f2d23a98
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.redirect;
+
+/**
+ * HTTP redirect handling strategy
+ */
+public enum RedirectHandling {
+    /** Follow HTTP location returned from an HTTP 300 series status */
+    FOLLOWED,
+
+    /** Ignore HTTP location returned from an HTTP 300 series status */
+    IGNORED
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java
new file mode 100644
index 0000000000..a8a0538199
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.ssl;
+
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * SSLSocketFactory Provider
+ */
+public interface SSLSocketFactoryProvider {
+    /**
+     * Get SSLSocketFactory using provided TLS Context configuration
+     *
+     * @param tlsContext TLS Context configuration
+     * @return SSLSocketFactory
+     */
+    SSLSocketFactory getSocketFactory(TlsContext tlsContext);
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java
new file mode 100644
index 0000000000..efa312d214
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.ssl;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Standard implementation of SSLSocketFactory Provider
+ */
+public class StandardSSLSocketFactoryProvider implements SSLSocketFactoryProvider {
+    /**
+     * Get SSLSocketFactory defaults to system Trust Manager and allows an empty Key Manager
+     *
+     * @param tlsContext TLS Context configuration
+     * @return SSLSocketFactory
+     */
+    @Override
+    public SSLSocketFactory getSocketFactory(final TlsContext tlsContext) {
+        Objects.requireNonNull(tlsContext, "TLS Context required");
+        final SSLContext sslContext = getSslContext(tlsContext);
+
+        try {
+            final Optional<X509KeyManager> keyManager = tlsContext.getKeyManager();
+            final KeyManager[] keyManagers = keyManager.map(x509KeyManager -> new KeyManager[]{x509KeyManager}).orElse(null);
+
+            final X509TrustManager trustManager = tlsContext.getTrustManager();
+            final TrustManager[] trustManagers = trustManager == null ? null : new TrustManager[]{trustManager};
+
+            final SecureRandom secureRandom = new SecureRandom();
+            sslContext.init(keyManagers, trustManagers, secureRandom);
+
+            return sslContext.getSocketFactory();
+        } catch (final KeyManagementException e) {
+            throw new IllegalArgumentException("SSLContext initialization failed", e);
+        }
+    }
+
+    private SSLContext getSslContext(final TlsContext tlsContext) {
+        final String protocol = Objects.requireNonNull(tlsContext.getProtocol(), "TLS Protocol required");
+        try {
+            return SSLContext.getInstance(protocol);
+        } catch (final NoSuchAlgorithmException e) {
+            throw new IllegalArgumentException(String.format("SSLContext protocol [%s] not supported", protocol), e);
+        }
+    }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java
new file mode 100644
index 0000000000..604bfaff21
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.ssl;
+
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+import java.util.Optional;
+
+/**
+ * TLS Context provides components necessary for TLS communication
+ */
+public interface TlsContext {
+    /**
+     * Get TLS Protocol
+     *
+     * @return TLS Protocol
+     */
+    String getProtocol();
+
+    /**
+     * Get X.509 Trust Manager
+     *
+     * @return X.509 Trust Manager
+     */
+    X509TrustManager getTrustManager();
+
+    /**
+     * Get X.509 Key Manager
+     *
+     * @return X.509 Key Manager or empty when not configured
+     */
+    Optional<X509KeyManager> getKeyManager();
+}
diff --git a/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java
new file mode 100644
index 0000000000..73cb81f0b7
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client;
+
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class StandardHttpUriBuilderTest {
+    private static final String HTTP_SCHEME = "http";
+
+    private static final String LOCALHOST = "localhost";
+
+    private static final int PORT = 8080;
+
+    private static final String ENCODED_PATH = "/resources/search";
+
+    private static final String RESOURCES_PATH_SEGMENT = "resources";
+
+    private static final String PARAMETER_NAME = "search";
+
+    private static final String PARAMETER_VALUE = "terms";
+
+    private static final URI HTTP_LOCALHOST_URI = URI.create(String.format("%s://%s/", HTTP_SCHEME, LOCALHOST));
+
+    private static final URI HTTP_LOCALHOST_PORT_URI = URI.create(String.format("%s://%s:%d/", HTTP_SCHEME, LOCALHOST, PORT));
+
+    private static final URI HTTP_LOCALHOST_PORT_ENCODED_PATH_URI = URI.create(String.format("%s://%s:%d%s", HTTP_SCHEME, LOCALHOST, PORT, ENCODED_PATH));
+
+    private static final URI HTTP_LOCALHOST_RESOURCES_URI = URI.create(String.format("%s%s", HTTP_LOCALHOST_URI, RESOURCES_PATH_SEGMENT));
+
+    private static final URI HTTP_LOCALHOST_QUERY_URI = URI.create(String.format("%s?%s=%s", HTTP_LOCALHOST_RESOURCES_URI, PARAMETER_NAME, PARAMETER_VALUE));
+
+    private static final URI HTTP_LOCALHOST_QUERY_EMPTY_VALUE_URI = URI.create(String.format("%s?%s", HTTP_LOCALHOST_RESOURCES_URI, PARAMETER_NAME));
+
+    @Test
+    void testBuildIllegalStateException() {
+        final HttpUriBuilder builder = new StandardHttpUriBuilder();
+
+        assertThrows(IllegalStateException.class, builder::build);
+    }
+
+    @Test
+    void testBuildSchemeHost() {
+        final HttpUriBuilder builder = new StandardHttpUriBuilder()
+                .scheme(HTTP_SCHEME)
+                .host(LOCALHOST);
+
+        final URI uri = builder.build();
+
+        assertEquals(HTTP_LOCALHOST_URI, uri);
+    }
+
+    @Test
+    void testBuildSchemeHostPort() {
+        final HttpUriBuilder builder = new StandardHttpUriBuilder()
+                .scheme(HTTP_SCHEME)
+                .host(LOCALHOST)
+                .port(PORT);
+
+        final URI uri = builder.build();
+
+        assertEquals(HTTP_LOCALHOST_PORT_URI, uri);
+    }
+
+    @Test
+    void testBuildSchemeHostPortEncodedPath() {
+        final HttpUriBuilder builder = new StandardHttpUriBuilder()
+                .scheme(HTTP_SCHEME)
+                .host(LOCALHOST)
+                .port(PORT)
+                .encodedPath(ENCODED_PATH);
+
+        final URI uri = builder.build();
+
+        assertEquals(HTTP_LOCALHOST_PORT_ENCODED_PATH_URI, uri);
+    }
+
+    @Test
+    void testBuildSchemeHostPathSegment() {
+        final HttpUriBuilder builder = new StandardHttpUriBuilder()
+                .scheme(HTTP_SCHEME)
+                .host(LOCALHOST)
+                .addPathSegment(RESOURCES_PATH_SEGMENT);
+
+        final URI uri = builder.build();
+
+        assertEquals(HTTP_LOCALHOST_RESOURCES_URI, uri);
+    }
+
+    @Test
+    void testBuildSchemeHostPathSegmentQueryParameter() {
+        final HttpUriBuilder builder = new StandardHttpUriBuilder()
+                .scheme(HTTP_SCHEME)
+                .host(LOCALHOST)
+                .addPathSegment(RESOURCES_PATH_SEGMENT)
+                .addQueryParameter(PARAMETER_NAME, PARAMETER_VALUE);
+
+        final URI uri = builder.build();
+
+        assertEquals(HTTP_LOCALHOST_QUERY_URI, uri);
+    }
+
+    @Test
+    void testBuildSchemeHostPathSegmentQueryParameterNullValue() {
+        final HttpUriBuilder builder = new StandardHttpUriBuilder()
+                .scheme(HTTP_SCHEME)
+                .host(LOCALHOST)
+                .addPathSegment(RESOURCES_PATH_SEGMENT)
+                .addQueryParameter(PARAMETER_NAME, null);
+
+        final URI uri = builder.build();
+
+        assertEquals(HTTP_LOCALHOST_QUERY_EMPTY_VALUE_URI, uri);
+    }
+}
diff --git a/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java
new file mode 100644
index 0000000000..ae0a80fa6f
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java
@@ -0,0 +1,434 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client;
+
+import okhttp3.Credentials;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.apache.nifi.web.client.api.HttpEntityHeaders;
+import org.apache.nifi.web.client.api.HttpRequestMethod;
+import org.apache.nifi.web.client.api.HttpRequestUriSpec;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpResponseStatus;
+import org.apache.nifi.web.client.api.StandardHttpRequestMethod;
+import org.apache.nifi.web.client.api.WebClientServiceException;
+import org.apache.nifi.web.client.proxy.ProxyContext;
+import org.apache.nifi.web.client.redirect.RedirectHandling;
+import org.apache.nifi.web.client.ssl.TlsContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.net.ssl.X509TrustManager;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Proxy;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalLong;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class StandardWebClientServiceTest {
+    private static final String ROOT_PATH = "/";
+
+    private static final String LOCALHOST = "localhost";
+
+    private static final byte[] EMPTY_BODY = new byte[0];
+
+    private static final String RESPONSE_BODY = String.class.getSimpleName();
+
+    private static final byte[] TEXT_BODY = RESPONSE_BODY.getBytes(StandardCharsets.UTF_8);
+
+    private static final Duration FAILURE_TIMEOUT = Duration.ofMillis(100);
+
+    private static final String ACCEPT_HEADER = "Accept";
+
+    private static final String ACCEPT_ANY_TYPE = "*/*";
+
+    private static final String CONTENT_LENGTH_HEADER = "content-length";
+
+    private static final String CONTENT_LENGTH_ZERO = "0";
+
+    private static final String LOCATION_HEADER = "Location";
+
+    private static final String PROXY_AUTHENTICATE_HEADER = "Proxy-Authenticate";
+
+    private static final String PROXY_AUTHENTICATE_BASIC_REALM = "Basic realm=\"Authentication Required\"";
+
+    private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
+
+    private static final String TLS_PROTOCOL = "TLS";
+
+    private static final String TLS_PROTOCOL_UNSUPPORTED = "TLSv0";
+
+    private static final X509Certificate[] TRUSTED_ISSUERS = new X509Certificate[0];
+
+    @Mock
+    TlsContext tlsContext;
+
+    @Mock
+    ProxyContext proxyContext;
+
+    @Mock
+    X509TrustManager trustManager;
+
+    MockWebServer mockWebServer;
+
+    StandardWebClientService service;
+
+    @BeforeEach
+    void setServer() {
+        mockWebServer = new MockWebServer();
+        service = new StandardWebClientService();
+    }
+
+    @AfterEach
+    void shutdownServer() throws IOException {
+        mockWebServer.shutdown();
+    }
+
+    @Test
+    void testSetTlsContext() {
+        when(tlsContext.getProtocol()).thenReturn(TLS_PROTOCOL);
+        when(tlsContext.getTrustManager()).thenReturn(trustManager);
+        when(trustManager.getAcceptedIssuers()).thenReturn(TRUSTED_ISSUERS);
+
+        service.setTlsContext(tlsContext);
+    }
+
+    @Test
+    void testSetTlsContextProtocolNotSupported() {
+        when(tlsContext.getProtocol()).thenReturn(TLS_PROTOCOL_UNSUPPORTED);
+        when(tlsContext.getTrustManager()).thenReturn(trustManager);
+
+        assertThrows(IllegalArgumentException.class, () -> service.setTlsContext(tlsContext));
+    }
+
+    @Test
+    void testSocketTimeoutException() throws IOException {
+        mockWebServer.shutdown();
+
+        service.setConnectTimeout(FAILURE_TIMEOUT);
+        service.setReadTimeout(FAILURE_TIMEOUT);
+        service.setWriteTimeout(FAILURE_TIMEOUT);
+
+        when(proxyContext.getProxy()).thenReturn(Proxy.NO_PROXY);
+        service.setProxyContext(proxyContext);
+
+        final WebClientServiceException exception = assertThrows(WebClientServiceException.class, () ->
+                service.method(StandardHttpRequestMethod.GET)
+                        .uri(getRootUri())
+                        .retrieve()
+        );
+
+        assertInstanceOf(SocketTimeoutException.class, exception.getCause());
+    }
+
+    @Test
+    void testProxyAuthorization() throws IOException, InterruptedException {
+        final Proxy proxy = mockWebServer.toProxyAddress();
+        when(proxyContext.getProxy()).thenReturn(proxy);
+        final String username = String.class.getSimpleName();
+        final String password = String.class.getName();
+        when(proxyContext.getUsername()).thenReturn(Optional.of(username));
+        when(proxyContext.getPassword()).thenReturn(Optional.of(password));
+        service.setProxyContext(proxyContext);
+
+        mockWebServer.enqueue(new MockResponse()
+                .setResponseCode(HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED.getCode())
+                .setHeader(PROXY_AUTHENTICATE_HEADER, PROXY_AUTHENTICATE_BASIC_REALM)
+        );
+
+        runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.OK);
+
+        final RecordedRequest proxyAuthorizationRequest = mockWebServer.takeRequest();
+        final String proxyAuthorization = proxyAuthorizationRequest.getHeader(PROXY_AUTHORIZATION_HEADER);
+        final String credentials = Credentials.basic(username, password);
+        assertEquals(credentials, proxyAuthorization);
+    }
+
+    @Test
+    void testRedirectHandlingFollowed() throws InterruptedException {
+        service.setRedirectHandling(RedirectHandling.FOLLOWED);
+
+        final String location = mockWebServer.url(ROOT_PATH).newBuilder().host(LOCALHOST).build().toString();
+
+        final MockResponse movedResponse = new MockResponse()
+                .setResponseCode(HttpResponseStatus.MOVED_PERMANENTLY.getCode())
+                .setHeader(LOCATION_HEADER, location);
+        mockWebServer.enqueue(movedResponse);
+
+        final HttpResponseStatus httpResponseStatus = HttpResponseStatus.OK;
+        enqueueResponseStatus(httpResponseStatus);
+
+        final HttpResponseEntity httpResponseEntity = service.get()
+                .uri(getRootUri())
+                .retrieve();
+
+        assertRecordedRequestResponseStatus(httpResponseEntity, StandardHttpRequestMethod.GET, httpResponseStatus);
+    }
+
+    @Test
+    void testRedirectHandlingIgnored() throws InterruptedException {
+        service.setRedirectHandling(RedirectHandling.IGNORED);
+
+        final HttpResponseStatus httpResponseStatus = HttpResponseStatus.MOVED_PERMANENTLY;
+
+        enqueueResponseStatusBody(httpResponseStatus);
+
+        final HttpResponseEntity httpResponseEntity = service.get()
+                .uri(getRootUri())
+                .retrieve();
+
+        assertRecordedRequestResponseStatus(httpResponseEntity, StandardHttpRequestMethod.GET, httpResponseStatus);
+    }
+
+    @Test
+    void testDelete() throws InterruptedException, IOException {
+        runRequestMethod(service.delete(), StandardHttpRequestMethod.DELETE, HttpResponseStatus.NO_CONTENT);
+    }
+
+    @Test
+    void testDeleteMethodNotAllowed() throws InterruptedException, IOException {
+        runRequestMethod(service.delete(), StandardHttpRequestMethod.DELETE, HttpResponseStatus.METHOD_NOT_ALLOWED);
+    }
+
+    @Test
+    void testGet() throws InterruptedException, IOException {
+        runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.OK);
+    }
+
+    @Test
+    void testGetNotFound() throws InterruptedException, IOException {
+        runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.NOT_FOUND);
+    }
+
+    @Test
+    void testGetInternalServerError() throws InterruptedException, IOException {
+        runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.INTERNAL_SERVER_ERROR);
+    }
+
+    @Test
+    void testGetServiceUnavailable() throws InterruptedException, IOException {
+        runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.SERVICE_UNAVAILABLE);
+    }
+
+    @Test
+    void testPatch() throws InterruptedException, IOException {
+        runRequestMethodRequestBody(service.patch(), StandardHttpRequestMethod.PATCH, HttpResponseStatus.OK);
+    }
+
+    @Test
+    void testPatchBadRequest() throws InterruptedException, IOException {
+        runRequestMethodRequestBody(service.patch(), StandardHttpRequestMethod.PATCH, HttpResponseStatus.BAD_REQUEST);
+    }
+
+    @Test
+    void testPost() throws InterruptedException, IOException {
+        runRequestMethodRequestBody(service.post(), StandardHttpRequestMethod.POST, HttpResponseStatus.CREATED);
+    }
+
+    @Test
+    void testPostUnauthorized() throws InterruptedException, IOException {
+        runRequestMethodRequestBody(service.post(), StandardHttpRequestMethod.POST, HttpResponseStatus.UNAUTHORIZED);
+    }
+
+    @Test
+    void testPut() throws InterruptedException, IOException {
+        runRequestMethodRequestBody(service.put(), StandardHttpRequestMethod.PUT, HttpResponseStatus.OK);
+    }
+
+    @Test
+    void testPutForbidden() throws InterruptedException, IOException {
+        runRequestMethodRequestBody(service.put(), StandardHttpRequestMethod.PUT, HttpResponseStatus.FORBIDDEN);
+    }
+
+    @ParameterizedTest
+    @EnumSource(value = StandardHttpRequestMethod.class, names = {"DELETE", "GET"})
+    void testHttpRequestMethod(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException {
+        runRequestMethod(service.method(httpRequestMethod), httpRequestMethod, HttpResponseStatus.NO_CONTENT);
+    }
+
+    @ParameterizedTest
+    @EnumSource(value = StandardHttpRequestMethod.class, names = {"DELETE", "GET"})
+    void testHttpRequestMethodResponseBody(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException {
+        final HttpResponseStatus httpResponseStatus = HttpResponseStatus.ACCEPTED;
+        enqueueResponseStatusBody(httpResponseStatus);
+
+        final HttpResponseEntity httpResponseEntity = service.method(httpRequestMethod)
+                .uri(getRootUri())
+                .retrieve();
+
+        assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+
+        try (final InputStream body = httpResponseEntity.body()) {
+            final byte[] responseBody = new byte[TEXT_BODY.length];
+            final int bytesRead = body.read(responseBody);
+            assertEquals(TEXT_BODY.length, bytesRead);
+            assertArrayEquals(TEXT_BODY, responseBody);
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(value = StandardHttpRequestMethod.class, names = {"PATCH", "POST", "PUT"})
+    void testHttpRequestMethodRequestBodyEmpty(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException {
+        final HttpResponseStatus httpResponseStatus = HttpResponseStatus.ACCEPTED;
+        enqueueResponseStatus(httpResponseStatus);
+
+        final InputStream body = new ByteArrayInputStream(EMPTY_BODY);
+
+        try (final HttpResponseEntity httpResponseEntity = service.method(httpRequestMethod)
+                .uri(getRootUri())
+                .body(body, OptionalLong.empty())
+                .retrieve()
+        ) {
+            assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+            assertContentLengthHeaderFound(httpResponseEntity.headers());
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(value = StandardHttpRequestMethod.class, names = {"PATCH", "POST", "PUT"})
+    void testHttpRequestMethodRequestBody(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException {
+        final HttpResponseStatus httpResponseStatus = HttpResponseStatus.ACCEPTED;
+        enqueueResponseStatus(httpResponseStatus);
+
+        final InputStream body = new ByteArrayInputStream(TEXT_BODY);
+
+        try (final HttpResponseEntity httpResponseEntity = service.method(httpRequestMethod)
+                .uri(getRootUri())
+                .header(ACCEPT_HEADER, ACCEPT_ANY_TYPE)
+                .body(body, OptionalLong.of(TEXT_BODY.length))
+                .retrieve()
+        ) {
+
+            final HttpEntityHeaders headers = httpResponseEntity.headers();
+            assertContentLengthHeaderFound(headers);
+
+            final RecordedRequest recordedRequest = assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+
+            assertEquals(TEXT_BODY.length, recordedRequest.getBodySize());
+            final byte[] requestBody = recordedRequest.getBody().readByteArray();
+            assertArrayEquals(TEXT_BODY, requestBody);
+
+            final String acceptHeader = recordedRequest.getHeader(ACCEPT_HEADER);
+            assertEquals(ACCEPT_ANY_TYPE, acceptHeader);
+        }
+    }
+
+    private void runRequestMethod(
+            final HttpRequestUriSpec httpRequestUriSpec,
+            final HttpRequestMethod httpRequestMethod,
+            final HttpResponseStatus httpResponseStatus
+    ) throws IOException, InterruptedException {
+        enqueueResponseStatus(httpResponseStatus);
+
+        try (final HttpResponseEntity httpResponseEntity = httpRequestUriSpec
+                .uri(getRootUri())
+                .retrieve()
+        ) {
+            assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+            assertContentLengthHeaderFound(httpResponseEntity.headers());
+        }
+    }
+
+
+    private void runRequestMethodRequestBody(
+            final HttpRequestUriSpec httpRequestUriSpec,
+            final HttpRequestMethod httpRequestMethod,
+            final HttpResponseStatus httpResponseStatus
+    ) throws IOException, InterruptedException {
+        enqueueResponseStatus(httpResponseStatus);
+
+        final InputStream body = new ByteArrayInputStream(EMPTY_BODY);
+
+        try (final HttpResponseEntity httpResponseEntity = httpRequestUriSpec
+                .uri(getRootUri())
+                .body(body, OptionalLong.empty())
+                .retrieve()
+        ) {
+            assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+            assertContentLengthHeaderFound(httpResponseEntity.headers());
+        }
+    }
+
+    private RecordedRequest assertRecordedRequestResponseStatus(
+            final HttpResponseEntity httpResponseEntity,
+            final HttpRequestMethod httpRequestMethod,
+            final HttpResponseStatus httpResponseStatus
+    ) throws InterruptedException {
+        assertNotNull(httpResponseEntity);
+
+        final RecordedRequest recordedRequest = mockWebServer.takeRequest();
+        assertEquals(httpRequestMethod.getMethod(), recordedRequest.getMethod());
+
+        assertEquals(httpResponseStatus.getCode(), httpResponseEntity.statusCode());
+
+        return recordedRequest;
+    }
+
+    private void assertContentLengthHeaderFound(final HttpEntityHeaders headers) {
+        final Optional<String> contentLengthHeader = headers.getFirstHeader(CONTENT_LENGTH_HEADER);
+        assertTrue(contentLengthHeader.isPresent());
+        assertEquals(CONTENT_LENGTH_ZERO, contentLengthHeader.get());
+
+        final List<String> contentLengthHeaders = headers.getHeader(CONTENT_LENGTH_HEADER);
+        assertFalse(contentLengthHeaders.isEmpty());
+        assertEquals(Collections.singletonList(CONTENT_LENGTH_ZERO), contentLengthHeaders);
+
+        final Collection<String> headerNames = headers.getHeaderNames();
+        assertTrue(headerNames.contains(CONTENT_LENGTH_HEADER));
+    }
+
+    private void enqueueResponseStatus(final HttpResponseStatus httpResponseStatus) {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(httpResponseStatus.getCode()));
+    }
+
+    private void enqueueResponseStatusBody(final HttpResponseStatus httpResponseStatus) {
+        mockWebServer.enqueue(new MockResponse()
+                .setResponseCode(httpResponseStatus.getCode())
+                .setBody(RESPONSE_BODY)
+        );
+    }
+
+    private URI getRootUri() {
+        return mockWebServer.url(ROOT_PATH).newBuilder().host(LOCALHOST).build().uri();
+    }
+}
diff --git a/nifi-commons/pom.xml b/nifi-commons/pom.xml
index 9c16bdc2be..1789e2fe4f 100644
--- a/nifi-commons/pom.xml
+++ b/nifi-commons/pom.xml
@@ -64,6 +64,8 @@
         <module>nifi-utils</module>
         <module>nifi-uuid5</module>
         <module>nifi-vault-utils</module>
+        <module>nifi-web-client</module>
+        <module>nifi-web-client-api</module>
         <module>nifi-web-utils</module>
         <module>nifi-write-ahead-log</module>
         <module>nifi-xml-processing</module>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml
index 0e76307c87..acbd7e46b3 100644
--- a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml
@@ -127,5 +127,11 @@
             <version>1.18.0-SNAPSHOT</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-client-provider-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml
new file mode 100644
index 0000000000..57b93c5b62
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-web-client-provider-bundle</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-web-client-provider-api</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-client-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-api</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java
new file mode 100644
index 0000000000..76382053f5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.provider.api;
+
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.api.WebClientService;
+
+/**
+ * Web Client Service Provider abstracts configuration of Web Client Service instances
+ */
+public interface WebClientServiceProvider extends ControllerService {
+    /**
+     * Get new HTTP URI Builder
+     *
+     * @return New instance of HTTP URI Builder
+     */
+    HttpUriBuilder getHttpUriBuilder();
+
+    /**
+     * Get Web Client Service based on current configuration
+     *
+     * @return Configured Web Client Service
+     */
+    WebClientService getWebClientService();
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml
new file mode 100644
index 0000000000..f7ee3f865c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-web-client-provider-bundle</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-web-client-provider-service-nar</artifactId>
+    <packaging>nar</packaging>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-client-provider-service</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-standard-services-api-nar</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <type>nar</type>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml
new file mode 100644
index 0000000000..e4aa47b172
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-web-client-provider-bundle</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-web-client-provider-service</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-client-provider-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-ssl-context-service-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-proxy-configuration-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-client-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-client</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-utils</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-mock</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>mockwebserver</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java
new file mode 100644
index 0000000000..9d6dddff45
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.provider.service;
+
+import org.apache.nifi.ssl.SSLContextService;
+
+import javax.net.ssl.X509KeyManager;
+import java.util.Optional;
+
+/**
+ * Provider abstraction for loading a Key Manager
+ */
+interface KeyManagerProvider {
+    /**
+     * Get X.509 Key Manager
+     *
+     * @param sslContextService SSL Context Service
+     * @return X.509 Key Manager or empty when not configured
+     */
+    Optional<X509KeyManager> getKeyManager(SSLContextService sslContextService);
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java
new file mode 100644
index 0000000000..3db552494b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.provider.service;
+
+import org.apache.nifi.ssl.SSLContextService;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.X509KeyManager;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * Standard implementation of Key Manager Provider
+ */
+class StandardKeyManagerProvider implements KeyManagerProvider {
+    /**
+     * Get X.509 Key Manager using SSL Context Service configuration properties
+     *
+     * @param sslContextService SSL Context Service
+     * @return X.509 Key Manager or empty when not configured
+     */
+    @Override
+    public Optional<X509KeyManager> getKeyManager(final SSLContextService sslContextService) {
+        final X509KeyManager keyManager;
+
+        if (sslContextService.isKeyStoreConfigured()) {
+            final KeyManagerFactory keyManagerFactory = getKeyManagerFactory();
+            final KeyStore keyStore = getKeyStore(sslContextService);
+            final char[] keyPassword = getKeyPassword(sslContextService);
+            try {
+                keyManagerFactory.init(keyStore, keyPassword);
+            } catch (final KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
+                throw new IllegalStateException("Key Manager Factory initialization failed", e);
+            }
+
+            final KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
+            final Optional<KeyManager> firstKeyManager = Arrays.stream(keyManagers).findFirst();
+            final KeyManager configuredKeyManager = firstKeyManager.orElse(null);
+            keyManager = configuredKeyManager instanceof X509KeyManager ? (X509KeyManager) configuredKeyManager : null;
+        } else {
+            keyManager = null;
+        }
+
+        return Optional.ofNullable(keyManager);
+    }
+
+    private KeyStore getKeyStore(final SSLContextService sslContextService) {
+        final String keyStoreType = sslContextService.getKeyStoreType();
+        final KeyStore keyStore = getKeyStore(keyStoreType);
+        final char[] keyStorePassword = sslContextService.getKeyStorePassword().toCharArray();
+        final String keyStoreFile = sslContextService.getKeyStoreFile();
+        try {
+            try (final InputStream inputStream = new FileInputStream(keyStoreFile)) {
+                keyStore.load(inputStream, keyStorePassword);
+            }
+            return keyStore;
+        } catch (final IOException e) {
+            throw new IllegalStateException(String.format("Key Store File [%s] reading failed", keyStoreFile), e);
+        } catch (final NoSuchAlgorithmException | CertificateException e) {
+            throw new IllegalStateException(String.format("Key Store File [%s] loading failed", keyStoreFile), e);
+        }
+    }
+
+    private KeyStore getKeyStore(final String keyStoreType) {
+        try {
+            return KeyStore.getInstance(keyStoreType);
+        } catch (final KeyStoreException e) {
+            throw new IllegalStateException(String.format("Key Store Type [%s] creation failed", keyStoreType), e);
+        }
+    }
+
+    private char[] getKeyPassword(final SSLContextService sslContextService) {
+        final String keyPassword = sslContextService.getKeyPassword();
+        final String keyStorePassword = sslContextService.getKeyStorePassword();
+        final String password = keyPassword == null ? keyStorePassword : keyPassword;
+        return password.toCharArray();
+    }
+
+    private KeyManagerFactory getKeyManagerFactory() {
+        try {
+            return KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        } catch (final NoSuchAlgorithmException e) {
+            throw new IllegalArgumentException("Key Manager Factory creation failed", e);
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java
new file mode 100644
index 0000000000..fc51ee8f1a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.provider.service;
+
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.proxy.ProxyConfiguration;
+import org.apache.nifi.proxy.ProxyConfigurationService;
+import org.apache.nifi.ssl.SSLContextService;
+import org.apache.nifi.web.client.StandardHttpUriBuilder;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.proxy.ProxyContext;
+import org.apache.nifi.web.client.StandardWebClientService;
+import org.apache.nifi.web.client.redirect.RedirectHandling;
+import org.apache.nifi.web.client.ssl.TlsContext;
+import org.apache.nifi.web.client.api.WebClientService;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+import java.net.Proxy;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.nifi.proxy.ProxyConfigurationService.PROXY_CONFIGURATION_SERVICE;
+
+@CapabilityDescription("Web Client Service Provider with support for configuring standard HTTP connection properties")
+@Tags({ "HTTP", "Web", "Client" })
+public class StandardWebClientServiceProvider extends AbstractControllerService implements WebClientServiceProvider {
+
+    static final PropertyDescriptor CONNECT_TIMEOUT = new PropertyDescriptor.Builder()
+            .name("connect-timeout")
+            .displayName("Connect Timeout")
+            .description("Maximum amount of time to wait before failing during initial socket connection")
+            .required(true)
+            .defaultValue("10 secs")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .build();
+
+    static final PropertyDescriptor READ_TIMEOUT = new PropertyDescriptor.Builder()
+            .name("read-timeout")
+            .displayName("Read Timeout")
+            .description("Maximum amount of time to wait before failing while reading socket responses")
+            .required(true)
+            .defaultValue("10 secs")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .build();
+
+    static final PropertyDescriptor WRITE_TIMEOUT = new PropertyDescriptor.Builder()
+            .name("write-timeout")
+            .displayName("Write Timeout")
+            .description("Maximum amount of time to wait before failing while writing socket requests")
+            .required(true)
+            .defaultValue("10 secs")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .build();
+
+    static final PropertyDescriptor REDIRECT_HANDLING_STRATEGY = new PropertyDescriptor.Builder()
+            .name("redirect-handling-strategy")
+            .displayName("Redirect Handling Strategy")
+            .description("Handling strategy for responding to HTTP 301 or 302 redirects received with a Location header")
+            .required(true)
+            .defaultValue(RedirectHandling.FOLLOWED.name())
+            .allowableValues(RedirectHandling.values())
+            .build();
+
+    static final PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder()
+            .name("ssl-context-service")
+            .displayName("SSL Context Service")
+            .description("SSL Context Service overrides system default TLS settings for HTTPS communication")
+            .required(false)
+            .identifiesControllerService(SSLContextService.class)
+            .build();
+
+    static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = Arrays.asList(
+            CONNECT_TIMEOUT,
+            READ_TIMEOUT,
+            WRITE_TIMEOUT,
+            REDIRECT_HANDLING_STRATEGY,
+            SSL_CONTEXT_SERVICE,
+            PROXY_CONFIGURATION_SERVICE
+    );
+
+    private static final KeyManagerProvider keyManagerProvider = new StandardKeyManagerProvider();
+
+    private WebClientService webClientService;
+
+    @OnEnabled
+    public void onEnabled(final ConfigurationContext context) {
+        final StandardWebClientService standardWebClientService = new StandardWebClientService();
+
+        final Duration connectTimeout = getDuration(context, CONNECT_TIMEOUT);
+        standardWebClientService.setConnectTimeout(connectTimeout);
+
+        final Duration readTimeout = getDuration(context, READ_TIMEOUT);
+        standardWebClientService.setReadTimeout(readTimeout);
+
+        final Duration writeTimeout = getDuration(context, WRITE_TIMEOUT);
+        standardWebClientService.setReadTimeout(writeTimeout);
+
+        final String redirectHandlingStrategy = context.getProperty(REDIRECT_HANDLING_STRATEGY).getValue();
+        final RedirectHandling redirectHandling = RedirectHandling.valueOf(redirectHandlingStrategy);
+        standardWebClientService.setRedirectHandling(redirectHandling);
+
+        final PropertyValue sslContextServiceProperty = context.getProperty(SSL_CONTEXT_SERVICE);
+        if (sslContextServiceProperty.isSet()) {
+            final SSLContextService sslContextService = sslContextServiceProperty.asControllerService(SSLContextService.class);
+            final TlsContext tlsContext = getTlsContext(sslContextService);
+            standardWebClientService.setTlsContext(tlsContext);
+        }
+
+        final PropertyValue proxyConfigurationServiceProperty = context.getProperty(PROXY_CONFIGURATION_SERVICE);
+        if (proxyConfigurationServiceProperty.isSet()) {
+            final ProxyConfigurationService proxyConfigurationService = context.getProperty(PROXY_CONFIGURATION_SERVICE).asControllerService(ProxyConfigurationService.class);
+            final ProxyConfiguration proxyConfiguration = proxyConfigurationService.getConfiguration();
+            final ProxyContext proxyContext = getProxyContext(proxyConfiguration);
+            standardWebClientService.setProxyContext(proxyContext);
+        }
+
+        webClientService = standardWebClientService;
+    }
+
+    @Override
+    public HttpUriBuilder getHttpUriBuilder() {
+        return new StandardHttpUriBuilder();
+    }
+
+    @Override
+    public WebClientService getWebClientService() {
+        return webClientService;
+    }
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    private Duration getDuration(final ConfigurationContext context, final PropertyDescriptor propertyDescriptor) {
+        final long millis = context.getProperty(propertyDescriptor).asTimePeriod(TimeUnit.MILLISECONDS);
+        return Duration.ofMillis(millis);
+    }
+
+    private TlsContext getTlsContext(final SSLContextService sslContextService) {
+        final X509TrustManager trustManager = sslContextService.createTrustManager();
+        final Optional<X509KeyManager> keyManager = keyManagerProvider.getKeyManager(sslContextService);
+
+        return new TlsContext() {
+            @Override
+            public String getProtocol() {
+                return sslContextService.getSslAlgorithm();
+            }
+
+            @Override
+            public X509TrustManager getTrustManager() {
+                return trustManager;
+            }
+
+            @Override
+            public Optional<X509KeyManager> getKeyManager() {
+                return keyManager;
+            }
+        };
+    }
+
+    private ProxyContext getProxyContext(final ProxyConfiguration proxyConfiguration) {
+        return new ProxyContext() {
+            @Override
+            public Proxy getProxy() {
+                return proxyConfiguration.createProxy();
+            }
+
+            @Override
+            public Optional<String> getUsername() {
+                return Optional.ofNullable(proxyConfiguration.getProxyUserName());
+            }
+
+            @Override
+            public Optional<String> getPassword() {
+                return Optional.ofNullable(proxyConfiguration.getProxyUserPassword());
+            }
+        };
+    }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
new file mode 100644
index 0000000000..53db7395ea
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -0,0 +1,15 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+org.apache.nifi.web.client.provider.service.StandardWebClientServiceProvider
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java
new file mode 100644
index 0000000000..ccb6601263
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.provider.service;
+
+import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
+import org.apache.nifi.security.util.TlsConfiguration;
+import org.apache.nifi.ssl.SSLContextService;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.net.ssl.X509KeyManager;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class StandardKeyManagerProviderTest {
+    static TlsConfiguration tlsConfiguration;
+
+    @Mock
+    SSLContextService sslContextService;
+
+    StandardKeyManagerProvider provider;
+
+    @BeforeAll
+    static void setTlsConfiguration() {
+        tlsConfiguration = new TemporaryKeyStoreBuilder().build();
+    }
+
+    @BeforeEach
+    void setProvider() {
+        provider = new StandardKeyManagerProvider();
+    }
+
+    @Test
+    void testGetKeyManagerNotConfigured() {
+        when(sslContextService.isKeyStoreConfigured()).thenReturn(false);
+
+        final Optional<X509KeyManager> keyManager = provider.getKeyManager(sslContextService);
+
+        assertFalse(keyManager.isPresent());
+    }
+
+    @Test
+    void testGetKeyManager() {
+        when(sslContextService.isKeyStoreConfigured()).thenReturn(true);
+        when(sslContextService.getKeyStoreType()).thenReturn(tlsConfiguration.getKeystoreType().getType());
+        when(sslContextService.getKeyStoreFile()).thenReturn(tlsConfiguration.getKeystorePath());
+        when(sslContextService.getKeyStorePassword()).thenReturn(tlsConfiguration.getKeystorePassword());
+
+        final Optional<X509KeyManager> keyManager = provider.getKeyManager(sslContextService);
+
+        assertTrue(keyManager.isPresent());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java
new file mode 100644
index 0000000000..2c74b10e44
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.web.client.provider.service;
+
+import okhttp3.Credentials;
+import okhttp3.HttpUrl;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.apache.nifi.proxy.ProxyConfiguration;
+import org.apache.nifi.proxy.ProxyConfigurationService;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.util.SslContextFactory;
+import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
+import org.apache.nifi.security.util.TlsConfiguration;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.ssl.SSLContextService;
+import org.apache.nifi.util.NoOpProcessor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpResponseStatus;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.api.WebClientService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class StandardWebClientServiceProviderTest {
+    private static final String SERVICE_ID = StandardWebClientServiceProvider.class.getSimpleName();
+
+    private static final String SSL_CONTEXT_SERVICE_ID = SSLContextService.class.getSimpleName();
+
+    private static final String PROXY_SERVICE_ID = ProxyConfigurationService.class.getSimpleName();
+
+    private static final String LOCALHOST = "localhost";
+
+    private static final String HTTPS = "https";
+
+    private static final int PORT = 8443;
+
+    private static final String PATH_SEGMENT = "resources";
+
+    private static final String PARAMETER_NAME = "search";
+
+    private static final String PARAMETER_VALUE = "search";
+
+    private static final String ROOT_PATH = "/";
+
+    private static final URI LOCALHOST_URI = URI.create(String.format("%s://%s:%d/%s?%s=%s", HTTPS, LOCALHOST, PORT, PATH_SEGMENT, PARAMETER_NAME, PARAMETER_VALUE));
+
+    private static final String PROXY_AUTHENTICATE_HEADER = "Proxy-Authenticate";
+
+    private static final String PROXY_AUTHENTICATE_BASIC_REALM = "Basic realm=\"Authentication Required\"";
+
+    private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
+
+    private static final boolean TUNNEL_PROXY_DISABLED = false;
+
+    static TlsConfiguration tlsConfiguration;
+
+    static SSLContext sslContext;
+
+    static X509TrustManager trustManager;
+
+    @Mock
+    SSLContextService sslContextService;
+
+    @Mock
+    ProxyConfigurationService proxyConfigurationService;
+
+    TestRunner runner;
+
+    MockWebServer mockWebServer;
+
+    StandardWebClientServiceProvider provider;
+
+    @BeforeAll
+    static void setTlsConfiguration() throws TlsException {
+        tlsConfiguration = new TemporaryKeyStoreBuilder().build();
+        sslContext = SslContextFactory.createSslContext(tlsConfiguration);
+        trustManager = SslContextFactory.getX509TrustManager(tlsConfiguration);
+    }
+
+    @BeforeEach
+    void setRunner() throws InitializationException {
+        mockWebServer = new MockWebServer();
+
+        runner = TestRunners.newTestRunner(NoOpProcessor.class);
+
+        provider = new StandardWebClientServiceProvider();
+        runner.addControllerService(SERVICE_ID, provider);
+    }
+
+    @AfterEach
+    void shutdownServer() throws IOException {
+        mockWebServer.shutdown();
+    }
+
+    @Test
+    void testEnable() {
+        runner.enableControllerService(provider);
+    }
+
+    @Test
+    void testGetHttpUriBuilder() {
+        runner.enableControllerService(provider);
+
+        final HttpUriBuilder httpUriBuilder = provider.getHttpUriBuilder();
+
+        final URI uri = httpUriBuilder.scheme(HTTPS)
+                .host(LOCALHOST)
+                .port(PORT)
+                .addPathSegment(PATH_SEGMENT)
+                .addQueryParameter(PARAMETER_NAME, PARAMETER_VALUE)
+                .build();
+
+        assertEquals(LOCALHOST_URI, uri);
+    }
+
+    @Test
+    void testGetWebServiceClientGetUri() throws InterruptedException {
+        runner.enableControllerService(provider);
+
+        final WebClientService webClientService = provider.getWebClientService();
+
+        assertNotNull(webClientService);
+
+        assertGetUriCompleted(webClientService);
+    }
+
+    @Test
+    void testGetWebServiceClientSslContextServiceConfiguredGetUri() throws InitializationException, InterruptedException {
+        when(sslContextService.getIdentifier()).thenReturn(SSL_CONTEXT_SERVICE_ID);
+        when(sslContextService.getSslAlgorithm()).thenReturn(tlsConfiguration.getProtocol());
+        when(sslContextService.createTrustManager()).thenReturn(trustManager);
+
+        runner.addControllerService(SSL_CONTEXT_SERVICE_ID, sslContextService);
+        runner.enableControllerService(sslContextService);
+
+        runner.setProperty(provider, StandardWebClientServiceProvider.SSL_CONTEXT_SERVICE, SSL_CONTEXT_SERVICE_ID);
+        runner.enableControllerService(provider);
+
+        final WebClientService webClientService = provider.getWebClientService();
+
+        assertNotNull(webClientService);
+
+        final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+        mockWebServer.useHttps(sslSocketFactory, TUNNEL_PROXY_DISABLED);
+
+        assertGetUriCompleted(webClientService);
+    }
+
+    @Test
+    void testGetWebServiceClientProxyConfigurationGetUri() throws InitializationException, InterruptedException {
+        final Proxy proxy = mockWebServer.toProxyAddress();
+        final InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
+
+        final ProxyConfiguration proxyConfiguration = new ProxyConfiguration();
+        proxyConfiguration.setProxyType(Proxy.Type.HTTP);
+        proxyConfiguration.setProxyServerHost(proxyAddress.getHostName());
+        proxyConfiguration.setProxyServerPort(proxyAddress.getPort());
+
+        final String username = String.class.getSimpleName();
+        final String password = String.class.getName();
+        proxyConfiguration.setProxyUserName(username);
+        proxyConfiguration.setProxyUserPassword(password);
+
+        when(proxyConfigurationService.getIdentifier()).thenReturn(PROXY_SERVICE_ID);
+        when(proxyConfigurationService.getConfiguration()).thenReturn(proxyConfiguration);
+
+        mockWebServer.enqueue(new MockResponse()
+                .setResponseCode(HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED.getCode())
+                .setHeader(PROXY_AUTHENTICATE_HEADER, PROXY_AUTHENTICATE_BASIC_REALM)
+        );
+
+        runner.addControllerService(PROXY_SERVICE_ID, proxyConfigurationService);
+        runner.enableControllerService(proxyConfigurationService);
+
+        runner.setProperty(provider, ProxyConfigurationService.PROXY_CONFIGURATION_SERVICE, PROXY_SERVICE_ID);
+        runner.enableControllerService(provider);
+
+        final WebClientService webClientService = provider.getWebClientService();
+
+        assertNotNull(webClientService);
+
+        assertGetUriCompleted(webClientService);
+
+        final RecordedRequest proxyAuthorizationRequest = mockWebServer.takeRequest();
+        final String proxyAuthorization = proxyAuthorizationRequest.getHeader(PROXY_AUTHORIZATION_HEADER);
+        final String credentials = Credentials.basic(username, password);
+        assertEquals(credentials, proxyAuthorization);
+    }
+
+    private void assertGetUriCompleted(final WebClientService webClientService) throws InterruptedException {
+        final URI uri = mockWebServer.url(ROOT_PATH).newBuilder().host(LOCALHOST).build().uri();
+
+        final HttpResponseStatus httpResponseStatus = HttpResponseStatus.OK;
+        final MockResponse mockResponse = new MockResponse().setResponseCode(httpResponseStatus.getCode());
+        mockWebServer.enqueue(mockResponse);
+
+        final HttpResponseEntity httpResponseEntity = webClientService.get().uri(uri).retrieve();
+
+        assertNotNull(httpResponseEntity);
+        assertEquals(httpResponseStatus.getCode(), httpResponseEntity.statusCode());
+
+        final RecordedRequest request = mockWebServer.takeRequest();
+        final HttpUrl requestUrl = request.getRequestUrl();
+        assertNotNull(requestUrl);
+
+        final URI requestUri = requestUrl.uri();
+        assertEquals(uri.getPort(), requestUri.getPort());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml
new file mode 100644
index 0000000000..6db83509db
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-standard-services</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-web-client-provider-bundle</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>nifi-web-client-provider-api</module>
+        <module>nifi-web-client-provider-service</module>
+        <module>nifi-web-client-provider-service-nar</module>
+    </modules>
+</project>
diff --git a/nifi-nar-bundles/nifi-standard-services/pom.xml b/nifi-nar-bundles/nifi-standard-services/pom.xml
index 524fc4fbb3..6e210e8f7d 100644
--- a/nifi-nar-bundles/nifi-standard-services/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-services/pom.xml
@@ -54,5 +54,6 @@
         <module>nifi-hadoop-dbcp-service-bundle</module>
         <module>nifi-kerberos-user-service-api</module>
         <module>nifi-kerberos-user-service-bundle</module>
+        <module>nifi-web-client-provider-bundle</module>
     </modules>
 </project>