From 7b45498de67ccdf10271e61577c66b5e216bc6db Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 10 Jun 2024 08:15:29 +1200
Subject: [PATCH] [http_request] Add esp-idf and rp2040 support (#3256)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Implement http_request component for esp-idf

* Fix ifdefs

* Lint

* clang

* Set else to fail with error message

* Use unique_ptr

* Fix

* Tidy up casting, explicit HttpResponse lifetime (#3265)

Co-authored-by: Daniel Cousens <dcousens@users.noreply.github.com>

* Remove unique_ptr wrapper

* Fix

* Use reference

* Add duration code into new split files

* Add config for tx/rx buffer on idf

* Fix

* Try reserve response data with rx buffer size

* Update http_request.h

* Move client cleanup to be earlier

* Move capture_response to bool on struct and remove global

* Fix returns

* Change quotes to brackets

* Rework http request

* Remove http request from old test yamls

* Update component tests

* Validate md5 length when hardcoded string

* Linting

* Add duration_ms to container

* More lint

* const

* Remove default arguments and add helper functions for get and post

* Add virtual destructor to HttpContainer

* Undo const HEADER_KEYS

* 🤦

* Update esphome/components/http_request/ota/ota_http_request.cpp

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>

* Update esphome/components/http_request/ota/ota_http_request.cpp

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>

* lint

* Move header keys inline

* Add missing WatchdogManagers

* CAPS

* Fix "follow redirects" string in config dump

* IDF 5+ fix

---------

Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com>
Co-authored-by: Daniel Cousens <dcousens@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
---
 esphome/codegen.py                            |   1 +
 esphome/components/http_request/__init__.py   | 137 +++++++++++----
 .../components/http_request/http_request.cpp  | 132 +-------------
 .../components/http_request/http_request.h    | 141 +++++++++------
 .../http_request/http_request_arduino.cpp     | 161 ++++++++++++++++++
 .../http_request/http_request_arduino.h       |  40 +++++
 .../http_request/http_request_idf.cpp         | 155 +++++++++++++++++
 .../http_request/http_request_idf.h           |  34 ++++
 .../components/http_request/ota/__init__.py   | 109 ++----------
 .../http_request/ota/ota_http_request.cpp     | 110 +++++-------
 .../http_request/ota/ota_http_request.h       |  24 +--
 .../ota/ota_http_request_arduino.cpp          | 134 ---------------
 .../ota/ota_http_request_arduino.h            |  42 -----
 .../http_request/ota/ota_http_request_idf.cpp |  86 ----------
 .../http_request/ota/ota_http_request_idf.h   |  24 ---
 .../http_request/{ota => }/watchdog.cpp       |  19 ++-
 .../http_request/{ota => }/watchdog.h         |   5 +-
 esphome/cpp_types.py                          |   1 +
 tests/components/http_request/common.yaml     |  75 ++++++++
 .../http_request/common_http_request.yaml     |  33 ----
 tests/components/http_request/common_ota.yaml |  36 ----
 .../http_request/test-nossl.esp8266.yaml      |  40 +----
 .../http_request/test.esp32-c3-idf.yaml       |   2 +-
 .../http_request/test.esp32-c3.yaml           |   3 +-
 .../http_request/test.esp32-idf.yaml          |   2 +-
 tests/components/http_request/test.esp32.yaml |   3 +-
 .../components/http_request/test.esp8266.yaml |   3 +-
 .../components/http_request/test.rp2040.yaml  |   2 +-
 tests/test1.yaml                              |  29 ----
 tests/test3.1.yaml                            |  24 ---
 tests/test7.yaml                              |  23 +--
 31 files changed, 748 insertions(+), 882 deletions(-)
 create mode 100644 esphome/components/http_request/http_request_arduino.cpp
 create mode 100644 esphome/components/http_request/http_request_arduino.h
 create mode 100644 esphome/components/http_request/http_request_idf.cpp
 create mode 100644 esphome/components/http_request/http_request_idf.h
 delete mode 100644 esphome/components/http_request/ota/ota_http_request_arduino.cpp
 delete mode 100644 esphome/components/http_request/ota/ota_http_request_arduino.h
 delete mode 100644 esphome/components/http_request/ota/ota_http_request_idf.cpp
 delete mode 100644 esphome/components/http_request/ota/ota_http_request_idf.h
 rename esphome/components/http_request/{ota => }/watchdog.cpp (79%)
 rename esphome/components/http_request/{ota => }/watchdog.h (84%)
 create mode 100644 tests/components/http_request/common.yaml
 delete mode 100644 tests/components/http_request/common_http_request.yaml
 delete mode 100644 tests/components/http_request/common_ota.yaml

diff --git a/esphome/codegen.py b/esphome/codegen.py
index dc17f28a03..b552490129 100644
--- a/esphome/codegen.py
+++ b/esphome/codegen.py
@@ -58,6 +58,7 @@ from esphome.cpp_types import (  # noqa
     bool_,
     int_,
     std_ns,
+    std_shared_ptr,
     std_string,
     std_vector,
     uint8,
diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py
index 0c3e249512..37487ec9a7 100644
--- a/esphome/components/http_request/__init__.py
+++ b/esphome/components/http_request/__init__.py
@@ -1,9 +1,8 @@
-import urllib.parse as urlparse
-
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
 from esphome.const import (
+    __version__,
     CONF_ID,
     CONF_TIMEOUT,
     CONF_METHOD,
@@ -12,67 +11,91 @@ from esphome.const import (
     CONF_ESP8266_DISABLE_SSL_SUPPORT,
 )
 from esphome.core import Lambda, CORE
+from esphome.components import esp32
 
 DEPENDENCIES = ["network"]
 AUTO_LOAD = ["json"]
 
 http_request_ns = cg.esphome_ns.namespace("http_request")
 HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component)
+HttpRequestArduino = http_request_ns.class_("HttpRequestArduino", HttpRequestComponent)
+HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent)
+
+HttpContainer = http_request_ns.class_("HttpContainer")
+
 HttpRequestSendAction = http_request_ns.class_(
     "HttpRequestSendAction", automation.Action
 )
 HttpRequestResponseTrigger = http_request_ns.class_(
-    "HttpRequestResponseTrigger", automation.Trigger
+    "HttpRequestResponseTrigger",
+    automation.Trigger.template(
+        cg.std_shared_ptr.template(HttpContainer), cg.std_string
+    ),
 )
 
-CONF_HEADERS = "headers"
+CONF_HTTP_REQUEST_ID = "http_request_id"
+
 CONF_USERAGENT = "useragent"
-CONF_BODY = "body"
-CONF_JSON = "json"
 CONF_VERIFY_SSL = "verify_ssl"
-CONF_ON_RESPONSE = "on_response"
 CONF_FOLLOW_REDIRECTS = "follow_redirects"
 CONF_REDIRECT_LIMIT = "redirect_limit"
+CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
+
+CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size"
+CONF_ON_RESPONSE = "on_response"
+CONF_HEADERS = "headers"
+CONF_BODY = "body"
+CONF_JSON = "json"
+CONF_CAPTURE_RESPONSE = "capture_response"
 
 
 def validate_url(value):
-    value = cv.string(value)
-    try:
-        parsed = list(urlparse.urlparse(value))
-    except Exception as err:
-        raise cv.Invalid("Invalid URL") from err
-
-    if not parsed[0] or not parsed[1]:
-        raise cv.Invalid("URL must have a URL scheme and host")
-
-    if parsed[0] not in ["http", "https"]:
-        raise cv.Invalid("Scheme must be http or https")
-
-    if not parsed[2]:
-        parsed[2] = "/"
-
-    return urlparse.urlunparse(parsed)
+    value = cv.url(value)
+    if value.startswith("http://") or value.startswith("https://"):
+        return value
+    raise cv.Invalid("URL must start with 'http://' or 'https://'")
 
 
-def validate_secure_url(config):
-    url_ = config[CONF_URL]
+def validate_ssl_verification(config):
+    error_message = ""
+
+    if CORE.is_esp32:
+        if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]:
+            error_message = "ESPHome supports certificate verification only via ESP-IDF"
+
+    if CORE.is_rp2040 and config[CONF_VERIFY_SSL]:
+        error_message = "ESPHome does not support certificate verification on RP2040"
+
     if (
-        config.get(CONF_VERIFY_SSL)
-        and not isinstance(url_, Lambda)
-        and url_.lower().startswith("https:")
+        CORE.is_esp8266
+        and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]
+        and config[CONF_VERIFY_SSL]
     ):
+        error_message = "ESPHome does not support certificate verification on ESP8266"
+
+    if len(error_message) > 0:
         raise cv.Invalid(
-            "Currently ESPHome doesn't support SSL verification. "
-            "Set 'verify_ssl: false' to make insecure HTTPS requests."
+            f"{error_message}. Set '{CONF_VERIFY_SSL}: false' to skip certificate validation and allow less secure HTTPS connections."
         )
+
     return config
 
 
+def _declare_request_class(value):
+    if CORE.using_esp_idf:
+        return cv.declare_id(HttpRequestIDF)(value)
+    if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040:
+        return cv.declare_id(HttpRequestArduino)(value)
+    return NotImplementedError
+
+
 CONFIG_SCHEMA = cv.All(
     cv.Schema(
         {
-            cv.GenerateID(): cv.declare_id(HttpRequestComponent),
-            cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string,
+            cv.GenerateID(): _declare_request_class,
+            cv.Optional(
+                CONF_USERAGENT, f"ESPHome/{__version__} (https://esphome.io)"
+            ): cv.string,
             cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
             cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
             cv.Optional(
@@ -81,12 +104,21 @@ CONFIG_SCHEMA = cv.All(
             cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All(
                 cv.only_on_esp8266, cv.boolean
             ),
+            cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
+            cv.Optional(CONF_WATCHDOG_TIMEOUT): cv.All(
+                cv.Any(cv.only_on_esp32, cv.only_on_rp2040),
+                cv.positive_not_null_time_period,
+                cv.positive_time_period_milliseconds,
+            ),
         }
     ).extend(cv.COMPONENT_SCHEMA),
     cv.require_framework_version(
         esp8266_arduino=cv.Version(2, 5, 1),
         esp32_arduino=cv.Version(0, 0, 0),
+        esp_idf=cv.Version(0, 0, 0),
+        rp2040_arduino=cv.Version(0, 0, 0),
     ),
+    validate_ssl_verification,
 )
 
 
@@ -100,11 +132,30 @@ async def to_code(config):
     if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
         cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
 
+    if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT):
+        cg.add(var.set_watchdog_timeout(timeout_ms))
+
     if CORE.is_esp32:
-        cg.add_library("WiFiClientSecure", None)
-        cg.add_library("HTTPClient", None)
+        if CORE.using_esp_idf:
+            esp32.add_idf_sdkconfig_option(
+                "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE",
+                config.get(CONF_VERIFY_SSL),
+            )
+            esp32.add_idf_sdkconfig_option(
+                "CONFIG_ESP_TLS_INSECURE",
+                not config.get(CONF_VERIFY_SSL),
+            )
+            esp32.add_idf_sdkconfig_option(
+                "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY",
+                not config.get(CONF_VERIFY_SSL),
+            )
+        else:
+            cg.add_library("WiFiClientSecure", None)
+            cg.add_library("HTTPClient", None)
     if CORE.is_esp8266:
         cg.add_library("ESP8266HTTPClient", None)
+    if CORE.is_rp2040 and CORE.using_arduino:
+        cg.add_library("HTTPClient", None)
 
     await cg.register_component(var, config)
 
@@ -116,12 +167,16 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
         cv.Optional(CONF_HEADERS): cv.All(
             cv.Schema({cv.string: cv.templatable(cv.string)})
         ),
-        cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
+        cv.Optional(CONF_VERIFY_SSL): cv.invalid(
+            f"{CONF_VERIFY_SSL} has moved to the base component configuration."
+        ),
+        cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
         cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(
             {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)}
         ),
+        cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
     }
-).add_extra(validate_secure_url)
+)
 HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf(
     CONF_URL,
     HTTP_REQUEST_ACTION_SCHEMA.extend(
@@ -173,6 +228,9 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
     template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
     cg.add(var.set_url(template_))
     cg.add(var.set_method(config[CONF_METHOD]))
+    cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE]))
+    cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))
+
     if CONF_BODY in config:
         template_ = await cg.templatable(config[CONF_BODY], args, cg.std_string)
         cg.add(var.set_body(template_))
@@ -196,7 +254,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
         cg.add(var.register_response_trigger(trigger))
         await automation.build_automation(
-            trigger, [(int, "status_code"), (cg.uint32, "duration_ms")], conf
+            trigger,
+            [
+                (cg.std_shared_ptr.template(HttpContainer), "response"),
+                (cg.std_string, "body"),
+            ],
+            conf,
         )
 
     return var
diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp
index 46894a9afd..be8bef006e 100644
--- a/esphome/components/http_request/http_request.cpp
+++ b/esphome/components/http_request/http_request.cpp
@@ -1,9 +1,8 @@
-#ifdef USE_ARDUINO
-
 #include "http_request.h"
-#include "esphome/core/defines.h"
+
 #include "esphome/core/log.h"
-#include "esphome/components/network/util.h"
+
+#include <cinttypes>
 
 namespace esphome {
 namespace http_request {
@@ -14,131 +13,12 @@ void HttpRequestComponent::dump_config() {
   ESP_LOGCONFIG(TAG, "HTTP Request:");
   ESP_LOGCONFIG(TAG, "  Timeout: %ums", this->timeout_);
   ESP_LOGCONFIG(TAG, "  User-Agent: %s", this->useragent_);
-  ESP_LOGCONFIG(TAG, "  Follow Redirects: %d", this->follow_redirects_);
+  ESP_LOGCONFIG(TAG, "  Follow redirects: %s", YESNO(this->follow_redirects_));
   ESP_LOGCONFIG(TAG, "  Redirect limit: %d", this->redirect_limit_);
-}
-
-void HttpRequestComponent::set_url(std::string url) {
-  this->url_ = std::move(url);
-  this->secure_ = this->url_.compare(0, 6, "https:") == 0;
-
-  if (!this->last_url_.empty() && this->url_ != this->last_url_) {
-    // Close connection if url has been changed
-    this->client_.setReuse(false);
-    this->client_.end();
+  if (this->watchdog_timeout_ > 0) {
+    ESP_LOGCONFIG(TAG, "  Watchdog Timeout: %" PRIu32 "ms", this->watchdog_timeout_);
   }
-  this->client_.setReuse(true);
-}
-
-void HttpRequestComponent::send(const std::vector<HttpRequestResponseTrigger *> &response_triggers) {
-  if (!network::is_connected()) {
-    this->client_.end();
-    this->status_set_warning();
-    ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
-    return;
-  }
-
-  bool begin_status = false;
-  const String url = this->url_.c_str();
-#if defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0))
-#if defined(USE_ESP32) || USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
-  if (this->follow_redirects_) {
-    this->client_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
-  } else {
-    this->client_.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
-  }
-#else
-  this->client_.setFollowRedirects(this->follow_redirects_);
-#endif
-  this->client_.setRedirectLimit(this->redirect_limit_);
-#endif
-#if defined(USE_ESP32)
-  begin_status = this->client_.begin(url);
-#elif defined(USE_ESP8266)
-  begin_status = this->client_.begin(*this->get_wifi_client_(), url);
-#endif
-
-  if (!begin_status) {
-    this->client_.end();
-    this->status_set_warning();
-    ESP_LOGW(TAG, "HTTP Request failed at the begin phase. Please check the configuration");
-    return;
-  }
-
-  this->client_.setTimeout(this->timeout_);
-#if defined(USE_ESP32)
-  this->client_.setConnectTimeout(this->timeout_);
-#endif
-  if (this->useragent_ != nullptr) {
-    this->client_.setUserAgent(this->useragent_);
-  }
-  for (const auto &header : this->headers_) {
-    this->client_.addHeader(header.name, header.value, false, true);
-  }
-
-  uint32_t start_time = millis();
-  int http_code = this->client_.sendRequest(this->method_, this->body_.c_str());
-  uint32_t duration = millis() - start_time;
-  for (auto *trigger : response_triggers)
-    trigger->process(http_code, duration);
-
-  if (http_code < 0) {
-    ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s; Duration: %u ms", this->url_.c_str(),
-             HTTPClient::errorToString(http_code).c_str(), duration);
-    this->status_set_warning();
-    return;
-  }
-
-  if (http_code < 200 || http_code >= 300) {
-    ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration);
-    this->status_set_warning();
-    return;
-  }
-
-  this->status_clear_warning();
-  ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration);
-}
-
-#ifdef USE_ESP8266
-std::shared_ptr<WiFiClient> HttpRequestComponent::get_wifi_client_() {
-#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
-  if (this->secure_) {
-    if (this->wifi_client_secure_ == nullptr) {
-      this->wifi_client_secure_ = std::make_shared<BearSSL::WiFiClientSecure>();
-      this->wifi_client_secure_->setInsecure();
-      this->wifi_client_secure_->setBufferSizes(512, 512);
-    }
-    return this->wifi_client_secure_;
-  }
-#endif
-
-  if (this->wifi_client_ == nullptr) {
-    this->wifi_client_ = std::make_shared<WiFiClient>();
-  }
-  return this->wifi_client_;
-}
-#endif
-
-void HttpRequestComponent::close() {
-  this->last_url_ = this->url_;
-  this->client_.end();
-}
-
-const char *HttpRequestComponent::get_string() {
-#if defined(ESP32)
-  // The static variable is here because HTTPClient::getString() returns a String on ESP32,
-  // and we need something to keep a buffer alive.
-  static String str;
-#else
-  // However on ESP8266, HTTPClient::getString() returns a String& to a member variable.
-  // Leaving this the default so that any new platform either doesn't copy, or encounters a compilation error.
-  auto &
-#endif
-  str = this->client_.getString();
-  return str.c_str();
 }
 
 }  // namespace http_request
 }  // namespace esphome
-
-#endif  // USE_ARDUINO
diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h
index b885de18e6..df6bc7dea7 100644
--- a/esphome/components/http_request/http_request.h
+++ b/esphome/components/http_request/http_request.h
@@ -1,27 +1,18 @@
 #pragma once
 
-#ifdef USE_ARDUINO
-
-#include "esphome/components/json/json_util.h"
-#include "esphome/core/automation.h"
-#include "esphome/core/component.h"
-#include "esphome/core/defines.h"
-
 #include <list>
 #include <map>
 #include <memory>
 #include <utility>
 #include <vector>
 
-#ifdef USE_ESP32
-#include <HTTPClient.h>
-#endif
-#ifdef USE_ESP8266
-#include <ESP8266HTTPClient.h>
-#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
-#include <WiFiClientSecure.h>
-#endif
-#endif
+#include "esphome/components/json/json_util.h"
+#include "esphome/core/application.h"
+#include "esphome/core/automation.h"
+#include "esphome/core/component.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
 
 namespace esphome {
 namespace http_request {
@@ -31,9 +22,32 @@ struct Header {
   const char *value;
 };
 
-class HttpRequestResponseTrigger : public Trigger<int32_t, uint32_t> {
+class HttpRequestComponent;
+
+class HttpContainer : public Parented<HttpRequestComponent> {
  public:
-  void process(int32_t status_code, uint32_t duration_ms) { this->trigger(status_code, duration_ms); }
+  virtual ~HttpContainer() = default;
+  size_t content_length;
+  int status_code;
+  uint32_t duration_ms;
+
+  virtual int read(uint8_t *buf, size_t max_len) = 0;
+  virtual void end() = 0;
+
+  void set_secure(bool secure) { this->secure_ = secure; }
+
+  size_t get_bytes_read() const { return this->bytes_read_; }
+
+ protected:
+  size_t bytes_read_{0};
+  bool secure_{false};
+};
+
+class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string> {
+ public:
+  void process(std::shared_ptr<HttpContainer> container, std::string response_body) {
+    this->trigger(std::move(container), std::move(response_body));
+  }
 };
 
 class HttpRequestComponent : public Component {
@@ -41,37 +55,33 @@ class HttpRequestComponent : public Component {
   void dump_config() override;
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
 
-  void set_url(std::string url);
-  void set_method(const char *method) { this->method_ = method; }
   void set_useragent(const char *useragent) { this->useragent_ = useragent; }
   void set_timeout(uint16_t timeout) { this->timeout_ = timeout; }
+  void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
+  uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
   void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
   void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
-  void set_body(const std::string &body) { this->body_ = body; }
-  void set_headers(std::list<Header> headers) { this->headers_ = std::move(headers); }
-  void send(const std::vector<HttpRequestResponseTrigger *> &response_triggers);
-  void close();
-  const char *get_string();
+
+  std::shared_ptr<HttpContainer> get(std::string url) { return this->start(std::move(url), "GET", "", {}); }
+  std::shared_ptr<HttpContainer> get(std::string url, std::list<Header> headers) {
+    return this->start(std::move(url), "GET", "", std::move(headers));
+  }
+  std::shared_ptr<HttpContainer> post(std::string url, std::string body) {
+    return this->start(std::move(url), "POST", std::move(body), {});
+  }
+  std::shared_ptr<HttpContainer> post(std::string url, std::string body, std::list<Header> headers) {
+    return this->start(std::move(url), "POST", std::move(body), std::move(headers));
+  }
+
+  virtual std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
+                                               std::list<Header> headers) = 0;
 
  protected:
-  HTTPClient client_{};
-  std::string url_;
-  std::string last_url_;
-  const char *method_;
   const char *useragent_{nullptr};
-  bool secure_;
   bool follow_redirects_;
   uint16_t redirect_limit_;
   uint16_t timeout_{5000};
-  std::string body_;
-  std::list<Header> headers_;
-#ifdef USE_ESP8266
-  std::shared_ptr<WiFiClient> wifi_client_;
-#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
-  std::shared_ptr<BearSSL::WiFiClientSecure> wifi_client_secure_;
-#endif
-  std::shared_ptr<WiFiClient> get_wifi_client_();
-#endif
+  uint32_t watchdog_timeout_{0};
 };
 
 template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
@@ -80,6 +90,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
   TEMPLATABLE_VALUE(std::string, url)
   TEMPLATABLE_VALUE(const char *, method)
   TEMPLATABLE_VALUE(std::string, body)
+  TEMPLATABLE_VALUE(bool, capture_response)
 
   void add_header(const char *key, TemplatableValue<const char *, Ts...> value) { this->headers_.insert({key, value}); }
 
@@ -89,19 +100,22 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
 
   void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); }
 
+  void set_max_response_buffer_size(size_t max_response_buffer_size) {
+    this->max_response_buffer_size_ = max_response_buffer_size;
+  }
+
   void play(Ts... x) override {
-    this->parent_->set_url(this->url_.value(x...));
-    this->parent_->set_method(this->method_.value(x...));
+    std::string body;
     if (this->body_.has_value()) {
-      this->parent_->set_body(this->body_.value(x...));
+      body = this->body_.value(x...);
     }
     if (!this->json_.empty()) {
       auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_, this, x..., std::placeholders::_1);
-      this->parent_->set_body(json::build_json(f));
+      body = json::build_json(f);
     }
     if (this->json_func_ != nullptr) {
       auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1);
-      this->parent_->set_body(json::build_json(f));
+      body = json::build_json(f);
     }
     std::list<Header> headers;
     for (const auto &item : this->headers_) {
@@ -111,10 +125,37 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
       header.value = val.value(x...);
       headers.push_back(header);
     }
-    this->parent_->set_headers(headers);
-    this->parent_->send(this->response_triggers_);
-    this->parent_->close();
-    this->parent_->set_body("");
+
+    auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, headers);
+
+    if (container == nullptr) {
+      return;
+    }
+
+    size_t content_length = container->content_length;
+    size_t max_length = std::min(content_length, this->max_response_buffer_size_);
+
+    std::string response_body;
+    if (this->capture_response_.value(x...)) {
+      ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
+      uint8_t *buf = allocator.allocate(max_length);
+      if (buf != nullptr) {
+        size_t read_index = 0;
+        while (container->get_bytes_read() < max_length) {
+          int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
+          App.feed_wdt();
+          yield();
+          read_index += read;
+        }
+        response_body.reserve(read_index);
+        response_body.assign((char *) buf, read_index);
+      }
+    }
+
+    for (auto *trigger : this->response_triggers_) {
+      trigger->process(container, response_body);
+    }
+    container->end();
   }
 
  protected:
@@ -130,9 +171,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
   std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
   std::function<void(Ts..., JsonObject)> json_func_{nullptr};
   std::vector<HttpRequestResponseTrigger *> response_triggers_;
+
+  size_t max_response_buffer_size_{SIZE_MAX};
 };
 
 }  // namespace http_request
 }  // namespace esphome
-
-#endif  // USE_ARDUINO
diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp
new file mode 100644
index 0000000000..248a85a439
--- /dev/null
+++ b/esphome/components/http_request/http_request_arduino.cpp
@@ -0,0 +1,161 @@
+#include "http_request_arduino.h"
+
+#ifdef USE_ARDUINO
+
+#include "esphome/components/network/util.h"
+#include "esphome/core/application.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/log.h"
+
+#include "watchdog.h"
+
+namespace esphome {
+namespace http_request {
+
+static const char *const TAG = "http_request.arduino";
+
+std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::string method, std::string body,
+                                                         std::list<Header> headers) {
+  if (!network::is_connected()) {
+    this->status_momentary_error("failed", 1000);
+    ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
+    return nullptr;
+  }
+
+  std::shared_ptr<HttpContainerArduino> container = std::make_shared<HttpContainerArduino>();
+  container->set_parent(this);
+
+  const uint32_t start = millis();
+
+  bool secure = url.find("https:") != std::string::npos;
+  container->set_secure(secure);
+
+  watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
+
+#if defined(USE_ESP8266)
+  std::unique_ptr<WiFiClient> stream_ptr;
+#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
+  if (secure) {
+    ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure");
+    stream_ptr = std::make_unique<WiFiClientSecure>();
+    WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get());
+    secure_client->setBufferSizes(512, 512);
+    secure_client->setInsecure();
+  } else {
+    stream_ptr = std::make_unique<WiFiClient>();
+  }
+#else
+  ESP_LOGV(TAG, "ESP8266 HTTP connection with WiFiClient");
+  if (secure) {
+    ESP_LOGE(TAG, "Can't use HTTPS connection with esp8266_disable_ssl_support");
+    return nullptr;
+  }
+  stream_ptr = std::make_unique<WiFiClient>();
+#endif  // USE_HTTP_REQUEST_ESP8266_HTTPS
+
+#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0)  // && USE_ARDUINO_VERSION_CODE < VERSION_CODE(?, ?, ?)
+  if (!secure) {
+    ESP_LOGW(TAG, "Using HTTP on Arduino version >= 3.1 is **very** slow. Consider setting framework version to 3.0.2 "
+                  "in your YAML, or use HTTPS");
+  }
+#endif  // USE_ARDUINO_VERSION_CODE
+
+  container->client_.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
+  bool status = container->client_.begin(*stream_ptr, url.c_str());
+
+#elif defined(USE_RP2040)
+  if (secure) {
+    container->client_.setInsecure();
+  }
+  bool status = container->client_.begin(url.c_str());
+#elif defined(USE_ESP32)
+  bool status = container->client_.begin(url.c_str());
+#endif
+
+  App.feed_wdt();
+
+  if (!status) {
+    ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url.c_str());
+    container->end();
+    this->status_momentary_error("failed", 1000);
+    return nullptr;
+  }
+
+  container->client_.setReuse(true);
+  container->client_.setTimeout(this->timeout_);
+#if defined(USE_ESP32)
+  container->client_.setConnectTimeout(this->timeout_);
+#endif
+
+  if (this->useragent_ != nullptr) {
+    container->client_.setUserAgent(this->useragent_);
+  }
+  for (const auto &header : headers) {
+    container->client_.addHeader(header.name, header.value, false, true);
+  }
+
+  // returned needed headers must be collected before the requests
+  static const char *header_keys[] = {"Content-Length", "Content-Type"};
+  static const size_t HEADER_COUNT = sizeof(header_keys) / sizeof(header_keys[0]);
+  container->client_.collectHeaders(header_keys, HEADER_COUNT);
+
+  container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
+  if (container->status_code < 0) {
+    ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
+             HTTPClient::errorToString(container->status_code).c_str());
+    this->status_momentary_error("failed", 1000);
+    container->end();
+    return nullptr;
+  }
+
+  if (container->status_code < 200 || container->status_code >= 300) {
+    ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
+    this->status_momentary_error("failed", 1000);
+    container->end();
+    return nullptr;
+  }
+
+  int content_length = container->client_.getSize();
+  ESP_LOGD(TAG, "Content-Length: %d", content_length);
+  container->content_length = (size_t) content_length;
+  container->duration_ms = millis() - start;
+
+  return container;
+}
+
+int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
+  const uint32_t start = millis();
+  watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
+
+  WiFiClient *stream_ptr = this->client_.getStreamPtr();
+  if (stream_ptr == nullptr) {
+    ESP_LOGE(TAG, "Stream pointer vanished!");
+    return -1;
+  }
+
+  int available_data = stream_ptr->available();
+  int bufsize = std::min(max_len, std::min(this->content_length - this->bytes_read_, (size_t) available_data));
+
+  if (bufsize == 0) {
+    this->duration_ms += (millis() - start);
+    return 0;
+  }
+
+  App.feed_wdt();
+  int read_len = stream_ptr->readBytes(buf, bufsize);
+  this->bytes_read_ += read_len;
+
+  this->duration_ms += (millis() - start);
+
+  return read_len;
+}
+
+void HttpContainerArduino::end() {
+  watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
+  this->client_.end();
+}
+
+}  // namespace http_request
+}  // namespace esphome
+
+#endif  // USE_ARDUINO
diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h
new file mode 100644
index 0000000000..dfdf4a35e2
--- /dev/null
+++ b/esphome/components/http_request/http_request_arduino.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "http_request.h"
+
+#ifdef USE_ARDUINO
+
+#if defined(USE_ESP32) || defined(USE_RP2040)
+#include <HTTPClient.h>
+#endif
+#ifdef USE_ESP8266
+#include <ESP8266HTTPClient.h>
+#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
+#include <WiFiClientSecure.h>
+#endif
+#endif
+
+namespace esphome {
+namespace http_request {
+
+class HttpRequestArduino;
+class HttpContainerArduino : public HttpContainer {
+ public:
+  int read(uint8_t *buf, size_t max_len) override;
+  void end() override;
+
+ protected:
+  friend class HttpRequestArduino;
+  HTTPClient client_{};
+};
+
+class HttpRequestArduino : public HttpRequestComponent {
+ public:
+  std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
+                                       std::list<Header> headers) override;
+};
+
+}  // namespace http_request
+}  // namespace esphome
+
+#endif  // USE_ARDUINO
diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp
new file mode 100644
index 0000000000..138e0438f4
--- /dev/null
+++ b/esphome/components/http_request/http_request_idf.cpp
@@ -0,0 +1,155 @@
+#include "http_request_idf.h"
+
+#ifdef USE_ESP_IDF
+
+#include "esphome/components/network/util.h"
+#include "esphome/core/application.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/log.h"
+
+#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
+#include "esp_crt_bundle.h"
+#endif
+
+#include "watchdog.h"
+
+namespace esphome {
+namespace http_request {
+
+static const char *const TAG = "http_request.idf";
+
+std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::string method, std::string body,
+                                                     std::list<Header> headers) {
+  if (!network::is_connected()) {
+    this->status_momentary_error("failed", 1000);
+    ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
+    return nullptr;
+  }
+
+  esp_http_client_method_t method_idf;
+  if (method == "GET") {
+    method_idf = HTTP_METHOD_GET;
+  } else if (method == "POST") {
+    method_idf = HTTP_METHOD_POST;
+  } else if (method == "PUT") {
+    method_idf = HTTP_METHOD_PUT;
+  } else if (method == "DELETE") {
+    method_idf = HTTP_METHOD_DELETE;
+  } else if (method == "PATCH") {
+    method_idf = HTTP_METHOD_PATCH;
+  } else {
+    this->status_momentary_error("failed", 1000);
+    ESP_LOGE(TAG, "HTTP Request failed; Unsupported method");
+    return nullptr;
+  }
+
+  bool secure = url.find("https:") != std::string::npos;
+
+  esp_http_client_config_t config = {};
+
+  config.url = url.c_str();
+  config.method = method_idf;
+  config.timeout_ms = this->timeout_;
+  config.disable_auto_redirect = !this->follow_redirects_;
+  config.max_redirection_count = this->redirect_limit_;
+#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
+  if (secure) {
+    config.crt_bundle_attach = esp_crt_bundle_attach;
+  }
+#endif
+
+  if (this->useragent_ != nullptr) {
+    config.user_agent = this->useragent_;
+  }
+
+  const uint32_t start = millis();
+  watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
+
+  esp_http_client_handle_t client = esp_http_client_init(&config);
+
+  std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
+  container->set_parent(this);
+
+  container->set_secure(secure);
+
+  for (const auto &header : headers) {
+    esp_http_client_set_header(client, header.name, header.value);
+  }
+
+  int body_len = body.length();
+
+  esp_err_t err = esp_http_client_open(client, body_len);
+  if (err != ESP_OK) {
+    this->status_momentary_error("failed", 1000);
+    ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
+    esp_http_client_cleanup(client);
+    return nullptr;
+  }
+
+  if (body_len > 0) {
+    int write_left = body_len;
+    int write_index = 0;
+    const char *buf = body.c_str();
+    while (body_len > 0) {
+      int written = esp_http_client_write(client, buf + write_index, write_left);
+      if (written < 0) {
+        err = ESP_FAIL;
+        break;
+      }
+      write_left -= written;
+      write_index += written;
+    }
+  }
+
+  if (err != ESP_OK) {
+    this->status_momentary_error("failed", 1000);
+    ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
+    esp_http_client_cleanup(client);
+    return nullptr;
+  }
+
+  container->content_length = esp_http_client_fetch_headers(client);
+  const auto status_code = esp_http_client_get_status_code(client);
+  container->status_code = status_code;
+
+  if (status_code < 200 || status_code >= 300) {
+    ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), status_code);
+    this->status_momentary_error("failed", 1000);
+    esp_http_client_cleanup(client);
+    return nullptr;
+  }
+  container->duration_ms = millis() - start;
+  return container;
+}
+
+int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
+  const uint32_t start = millis();
+  watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
+
+  int bufsize = std::min(max_len, this->content_length - this->bytes_read_);
+
+  if (bufsize == 0) {
+    this->duration_ms += (millis() - start);
+    return 0;
+  }
+
+  App.feed_wdt();
+  int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
+  this->bytes_read_ += read_len;
+
+  this->duration_ms += (millis() - start);
+
+  return read_len;
+}
+
+void HttpContainerIDF::end() {
+  watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
+
+  esp_http_client_close(this->client_);
+  esp_http_client_cleanup(this->client_);
+}
+
+}  // namespace http_request
+}  // namespace esphome
+
+#endif  // USE_ESP_IDF
diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h
new file mode 100644
index 0000000000..79f850a636
--- /dev/null
+++ b/esphome/components/http_request/http_request_idf.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "http_request.h"
+
+#ifdef USE_ESP_IDF
+
+#include <esp_event.h>
+#include <esp_http_client.h>
+#include <esp_netif.h>
+#include <esp_tls.h>
+
+namespace esphome {
+namespace http_request {
+
+class HttpContainerIDF : public HttpContainer {
+ public:
+  HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
+  int read(uint8_t *buf, size_t max_len) override;
+  void end() override;
+
+ protected:
+  esp_http_client_handle_t client_;
+};
+
+class HttpRequestIDF : public HttpRequestComponent {
+ public:
+  std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
+                                       std::list<Header> headers) override;
+};
+
+}  // namespace http_request
+}  // namespace esphome
+
+#endif  // USE_ESP_IDF
diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py
index 6a56fac83a..0ef1fc2348 100644
--- a/esphome/components/http_request/ota/__init__.py
+++ b/esphome/components/http_request/ota/__init__.py
@@ -2,92 +2,35 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
 from esphome.const import (
-    CONF_ESP8266_DISABLE_SSL_SUPPORT,
     CONF_ID,
     CONF_PASSWORD,
-    CONF_TIMEOUT,
     CONF_URL,
     CONF_USERNAME,
 )
-from esphome.components import esp32
 from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
-from esphome.core import CORE, coroutine_with_priority
-from .. import http_request_ns
+from esphome.core import coroutine_with_priority
+from .. import CONF_HTTP_REQUEST_ID, http_request_ns, HttpRequestComponent
 
 CODEOWNERS = ["@oarcher"]
 
 AUTO_LOAD = ["md5"]
-DEPENDENCIES = ["network"]
+DEPENDENCIES = ["network", "http_request"]
 
 CONF_MD5 = "md5"
 CONF_MD5_URL = "md5_url"
-CONF_VERIFY_SSL = "verify_ssl"
-CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
 
 OtaHttpRequestComponent = http_request_ns.class_(
     "OtaHttpRequestComponent", OTAComponent
 )
-OtaHttpRequestComponentArduino = http_request_ns.class_(
-    "OtaHttpRequestComponentArduino", OtaHttpRequestComponent
-)
-OtaHttpRequestComponentIDF = http_request_ns.class_(
-    "OtaHttpRequestComponentIDF", OtaHttpRequestComponent
-)
 OtaHttpRequestComponentFlashAction = http_request_ns.class_(
     "OtaHttpRequestComponentFlashAction", automation.Action
 )
 
-
-def validate_ssl_verification(config):
-    error_message = ""
-
-    if CORE.is_esp32:
-        if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]:
-            error_message = "ESPHome supports certificate verification only via ESP-IDF"
-
-    if CORE.is_rp2040 and config[CONF_VERIFY_SSL]:
-        error_message = "ESPHome does not support certificate verification in Arduino"
-
-    if (
-        CORE.is_esp8266
-        and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]
-        and config[CONF_VERIFY_SSL]
-    ):
-        error_message = "ESPHome does not support certificate verification in Arduino"
-
-    if len(error_message) > 0:
-        raise cv.Invalid(
-            f"{error_message}. Set '{CONF_VERIFY_SSL}: false' to skip certificate validation and allow less secure HTTPS connections."
-        )
-
-    return config
-
-
-def _declare_request_class(value):
-    if CORE.using_esp_idf:
-        return cv.declare_id(OtaHttpRequestComponentIDF)(value)
-
-    if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040:
-        return cv.declare_id(OtaHttpRequestComponentArduino)(value)
-    return NotImplementedError
-
-
 CONFIG_SCHEMA = cv.All(
     cv.Schema(
         {
-            cv.GenerateID(): _declare_request_class,
-            cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All(
-                cv.only_on_esp8266, cv.boolean
-            ),
-            cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
-            cv.Optional(
-                CONF_TIMEOUT, default="5min"
-            ): cv.positive_time_period_milliseconds,
-            cv.Optional(CONF_WATCHDOG_TIMEOUT): cv.All(
-                cv.Any(cv.only_on_esp32, cv.only_on_rp2040),
-                cv.positive_not_null_time_period,
-                cv.positive_time_period_milliseconds,
-            ),
+            cv.GenerateID(): cv.declare_id(OtaHttpRequestComponent),
+            cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
         }
     )
     .extend(BASE_OTA_SCHEMA)
@@ -98,7 +41,6 @@ CONFIG_SCHEMA = cv.All(
         esp_idf=cv.Version(0, 0, 0),
         rp2040_arduino=cv.Version(0, 0, 0),
     ),
-    validate_ssl_verification,
 )
 
 
@@ -106,41 +48,8 @@ CONFIG_SCHEMA = cv.All(
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     await ota_to_code(var, config)
-
-    cg.add(var.set_timeout(config[CONF_TIMEOUT]))
-
-    if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT):
-        cg.add_define(
-            "USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT",
-            timeout_ms,
-        )
-
-    if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
-        cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
-
-    if CORE.is_esp32:
-        if CORE.using_esp_idf:
-            esp32.add_idf_sdkconfig_option(
-                "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE",
-                config.get(CONF_VERIFY_SSL),
-            )
-            esp32.add_idf_sdkconfig_option(
-                "CONFIG_ESP_TLS_INSECURE",
-                not config.get(CONF_VERIFY_SSL),
-            )
-            esp32.add_idf_sdkconfig_option(
-                "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY",
-                not config.get(CONF_VERIFY_SSL),
-            )
-        else:
-            cg.add_library("WiFiClientSecure", None)
-            cg.add_library("HTTPClient", None)
-    if CORE.is_esp8266:
-        cg.add_library("ESP8266HTTPClient", None)
-    if CORE.is_rp2040 and CORE.using_arduino:
-        cg.add_library("HTTPClient", None)
-
     await cg.register_component(var, config)
+    await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
 
 
 OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
@@ -148,7 +57,9 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
         {
             cv.GenerateID(): cv.use_id(OtaHttpRequestComponent),
             cv.Optional(CONF_MD5_URL): cv.templatable(cv.url),
-            cv.Optional(CONF_MD5): cv.templatable(cv.string),
+            cv.Optional(CONF_MD5): cv.templatable(
+                cv.All(cv.string, cv.Length(min=32, max=32))
+            ),
             cv.Optional(CONF_PASSWORD): cv.templatable(cv.string),
             cv.Optional(CONF_USERNAME): cv.templatable(cv.string),
             cv.Required(CONF_URL): cv.templatable(cv.url),
@@ -159,7 +70,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
 
 
 @automation.register_action(
-    "ota_http_request.flash",
+    "ota.http_request.flash",
     OtaHttpRequestComponentFlashAction,
     OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA,
 )
diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp
index cf0816c858..a41f552baf 100644
--- a/esphome/components/http_request/ota/ota_http_request.cpp
+++ b/esphome/components/http_request/ota/ota_http_request.cpp
@@ -1,16 +1,16 @@
 #include "ota_http_request.h"
-#include "watchdog.h"
+#include "../watchdog.h"
 
 #include "esphome/core/application.h"
 #include "esphome/core/defines.h"
 #include "esphome/core/log.h"
 
 #include "esphome/components/md5/md5.h"
+#include "esphome/components/ota/ota_backend.h"
 #include "esphome/components/ota/ota_backend_arduino_esp32.h"
 #include "esphome/components/ota/ota_backend_arduino_esp8266.h"
 #include "esphome/components/ota/ota_backend_arduino_rp2040.h"
 #include "esphome/components/ota/ota_backend_esp_idf.h"
-#include "esphome/components/ota/ota_backend.h"
 
 namespace esphome {
 namespace http_request {
@@ -21,25 +21,7 @@ void OtaHttpRequestComponent::setup() {
 #endif
 }
 
-void OtaHttpRequestComponent::dump_config() {
-  ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request:");
-  ESP_LOGCONFIG(TAG, "  Timeout: %llus", this->timeout_ / 1000);
-#ifdef USE_ESP8266
-#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
-  ESP_LOGCONFIG(TAG, "  ESP8266 SSL support: No");
-#else
-  ESP_LOGCONFIG(TAG, "  ESP8266 SSL support: Yes");
-#endif
-#endif
-#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
-  ESP_LOGCONFIG(TAG, "  TLS server verification: Yes");
-#else
-  ESP_LOGCONFIG(TAG, "  TLS server verification: No");
-#endif
-#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
-  ESP_LOGCONFIG(TAG, "  Watchdog timeout: %ds", USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT / 1000);
-#endif
-};
+void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
 
 void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
   if (!this->validate_url_(url)) {
@@ -58,20 +40,6 @@ void OtaHttpRequestComponent::set_url(const std::string &url) {
   this->url_ = url;
 }
 
-bool OtaHttpRequestComponent::check_status() {
-  // status can be -1, or HTTP status code
-  if (this->status_ < 100) {
-    ESP_LOGE(TAG, "HTTP server did not respond (error %d)", this->status_);
-    return false;
-  }
-  if (this->status_ >= 310) {
-    ESP_LOGE(TAG, "HTTP error %d", this->status_);
-    return false;
-  }
-  ESP_LOGV(TAG, "HTTP status %d", this->status_);
-  return true;
-}
-
 void OtaHttpRequestComponent::flash() {
   if (this->url_.empty()) {
     ESP_LOGE(TAG, "URL not set; cannot start update");
@@ -104,17 +72,18 @@ void OtaHttpRequestComponent::flash() {
   }
 }
 
-void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend) {
+void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend,
+                                       const std::shared_ptr<HttpContainer> &container) {
   if (this->update_started_) {
     ESP_LOGV(TAG, "Aborting OTA backend");
     backend->abort();
   }
   ESP_LOGV(TAG, "Aborting HTTP connection");
-  this->http_end();
+  container->end();
 };
 
 uint8_t OtaHttpRequestComponent::do_ota_() {
-  uint8_t buf[this->http_recv_buffer_ + 1];
+  uint8_t buf[OtaHttpRequestComponent::HTTP_RECV_BUFFER + 1];
   uint32_t last_progress = 0;
   uint32_t update_start_time = millis();
   md5::MD5Digest md5_receive;
@@ -132,9 +101,10 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
   }
   ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
   ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
-  this->http_init(url_with_auth);
-  if (!this->check_status()) {
-    this->http_end();
+
+  auto container = this->parent_->get(url_with_auth);
+
+  if (container == nullptr) {
     return OTA_CONNECTION_ERROR;
   }
 
@@ -144,18 +114,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
 
   ESP_LOGV(TAG, "OTA backend begin");
   auto backend = ota::make_ota_backend();
-  auto error_code = backend->begin(this->body_length_);
+  auto error_code = backend->begin(container->content_length);
   if (error_code != ota::OTA_RESPONSE_OK) {
     ESP_LOGW(TAG, "backend->begin error: %d", error_code);
-    this->cleanup_(std::move(backend));
+    this->cleanup_(std::move(backend), container);
     return error_code;
   }
 
-  this->bytes_read_ = 0;
-  while (this->bytes_read_ < this->body_length_) {
+  while (container->get_bytes_read() < container->content_length) {
     // read a maximum of chunk_size bytes into buf. (real read size returned)
-    int bufsize = this->http_read(buf, this->http_recv_buffer_);
-    ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", this->bytes_read_, this->body_length_, bufsize);
+    int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
+    ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
+              container->content_length, bufsize);
 
     // feed watchdog and give other tasks a chance to run
     App.feed_wdt();
@@ -163,9 +133,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
 
     if (bufsize < 0) {
       ESP_LOGE(TAG, "Stream closed");
-      this->cleanup_(std::move(backend));
+      this->cleanup_(std::move(backend), container);
       return OTA_CONNECTION_ERROR;
-    } else if (bufsize > 0 && bufsize <= this->http_recv_buffer_) {
+    } else if (bufsize > 0 && bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
       // add read bytes to MD5
       md5_receive.add(buf, bufsize);
 
@@ -176,16 +146,16 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
         // error code explanation available at
         // https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
         ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
-                 this->bytes_read_ - bufsize, this->body_length_);
-        this->cleanup_(std::move(backend));
+                 container->get_bytes_read() - bufsize, container->content_length);
+        this->cleanup_(std::move(backend), container);
         return error_code;
       }
     }
 
     uint32_t now = millis();
-    if ((now - last_progress > 1000) or (this->bytes_read_ == this->body_length_)) {
+    if ((now - last_progress > 1000) or (container->get_bytes_read() == container->content_length)) {
       last_progress = now;
-      float percentage = this->bytes_read_ * 100.0f / this->body_length_;
+      float percentage = container->get_bytes_read() * 100.0f / container->content_length;
       ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
 #ifdef USE_OTA_STATE_CALLBACK
       this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
@@ -201,13 +171,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
   this->md5_computed_ = md5_receive_str.get();
   if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
     ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
-    this->cleanup_(std::move(backend));
+    this->cleanup_(std::move(backend), container);
     return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
   } else {
     backend->set_update_md5(md5_receive_str.get());
   }
 
-  this->http_end();
+  container->end();
 
   // feed watchdog and give other tasks a chance to run
   App.feed_wdt();
@@ -217,7 +187,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
   error_code = backend->end();
   if (error_code != ota::OTA_RESPONSE_OK) {
     ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
-    this->cleanup_(std::move(backend));
+    this->cleanup_(std::move(backend), container);
     return error_code;
   }
 
@@ -256,28 +226,32 @@ bool OtaHttpRequestComponent::http_get_md5_() {
 
   ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
   ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
-  this->http_init(url_with_auth);
-  if (!this->check_status()) {
-    this->http_end();
+  auto container = this->parent_->get(url_with_auth);
+  if (container == nullptr) {
+    ESP_LOGE(TAG, "Failed to connect to MD5 URL");
     return false;
   }
-  int length = this->body_length_;
-  if (length < 0) {
-    this->http_end();
+  size_t length = container->content_length;
+  if (length == 0) {
+    container->end();
     return false;
   }
   if (length < MD5_SIZE) {
-    ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE,
-             this->body_length_);
-    this->http_end();
+    ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, length);
+    container->end();
     return false;
   }
 
-  this->bytes_read_ = 0;
   this->md5_expected_.resize(MD5_SIZE);
-  auto read_len = this->http_read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
-  this->http_end();
+  int read_len = 0;
+  while (container->get_bytes_read() < MD5_SIZE) {
+    read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
+    App.feed_wdt();
+    yield();
+  }
+  container->end();
 
+  ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
   return read_len == MD5_SIZE;
 }
 
diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h
index 9fbdf2ec25..91c7085517 100644
--- a/esphome/components/http_request/ota/ota_http_request.h
+++ b/esphome/components/http_request/ota/ota_http_request.h
@@ -1,13 +1,16 @@
 #pragma once
 
+#include "esphome/components/ota/ota_backend.h"
 #include "esphome/core/component.h"
 #include "esphome/core/defines.h"
-#include "esphome/components/ota/ota_backend.h"
+#include "esphome/core/helpers.h"
 
 #include <memory>
 #include <string>
 #include <utility>
 
+#include "../http_request.h"
+
 namespace esphome {
 namespace http_request {
 
@@ -20,7 +23,7 @@ enum OtaHttpRequestError : uint8_t {
   OTA_CONNECTION_ERROR = 0x12,
 };
 
-class OtaHttpRequestComponent : public ota::OTAComponent {
+class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
  public:
   void setup() override;
   void dump_config() override;
@@ -29,27 +32,19 @@ class OtaHttpRequestComponent : public ota::OTAComponent {
   void set_md5_url(const std::string &md5_url);
   void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
   void set_password(const std::string &password) { this->password_ = password; }
-  void set_timeout(const uint64_t timeout) { this->timeout_ = timeout; }
   void set_url(const std::string &url);
   void set_username(const std::string &username) { this->username_ = username; }
 
   std::string md5_computed() { return this->md5_computed_; }
   std::string md5_expected() { return this->md5_expected_; }
 
-  bool check_status();
-
   void flash();
 
-  virtual void http_init(const std::string &url){};
-  virtual int http_read(uint8_t *buf, size_t len) { return 0; };
-  virtual void http_end(){};
-
  protected:
-  void cleanup_(std::unique_ptr<ota::OTABackend> backend);
+  void cleanup_(std::unique_ptr<ota::OTABackend> backend, const std::shared_ptr<HttpContainer> &container);
   uint8_t do_ota_();
   std::string get_url_with_auth_(const std::string &url);
   bool http_get_md5_();
-  bool secure_() { return this->url_.find("https:") != std::string::npos; };
   bool validate_url_(const std::string &url);
 
   std::string md5_computed_{};
@@ -58,14 +53,9 @@ class OtaHttpRequestComponent : public ota::OTAComponent {
   std::string password_{};
   std::string username_{};
   std::string url_{};
-  size_t body_length_ = 0;
-  size_t bytes_read_ = 0;
   int status_ = -1;
-  uint64_t timeout_ = 0;
   bool update_started_ = false;
-  const uint16_t http_recv_buffer_ = 256;      // the firmware GET chunk size
-  const uint16_t max_http_recv_buffer_ = 512;  // internal max http buffer size must be > HTTP_RECV_BUFFER_ (TLS
-                                               // overhead) and must be a power of two from 512 to 4096
+  static const uint16_t HTTP_RECV_BUFFER = 256;  // the firmware GET chunk size
 };
 
 }  // namespace http_request
diff --git a/esphome/components/http_request/ota/ota_http_request_arduino.cpp b/esphome/components/http_request/ota/ota_http_request_arduino.cpp
deleted file mode 100644
index d1dc638d5e..0000000000
--- a/esphome/components/http_request/ota/ota_http_request_arduino.cpp
+++ /dev/null
@@ -1,134 +0,0 @@
-#include "ota_http_request.h"
-#include "watchdog.h"
-
-#ifdef USE_ARDUINO
-#include "ota_http_request_arduino.h"
-#include "esphome/core/defines.h"
-#include "esphome/core/log.h"
-#include "esphome/core/application.h"
-#include "esphome/components/network/util.h"
-#include "esphome/components/md5/md5.h"
-
-namespace esphome {
-namespace http_request {
-
-struct Header {
-  const char *name;
-  const char *value;
-};
-
-void OtaHttpRequestComponentArduino::http_init(const std::string &url) {
-  const char *header_keys[] = {"Content-Length", "Content-Type"};
-  const size_t header_count = sizeof(header_keys) / sizeof(header_keys[0]);
-  watchdog::WatchdogManager wdts;
-
-#ifdef USE_ESP8266
-  if (this->stream_ptr_ == nullptr && this->set_stream_ptr_()) {
-    ESP_LOGE(TAG, "Unable to set client");
-    return;
-  }
-#endif  // USE_ESP8266
-
-#ifdef USE_RP2040
-  this->client_.setInsecure();
-#endif
-
-  App.feed_wdt();
-
-#if defined(USE_ESP32) || defined(USE_RP2040)
-  this->status_ = this->client_.begin(url.c_str());
-#endif
-#ifdef USE_ESP8266
-  this->client_.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
-  this->status_ = this->client_.begin(*this->stream_ptr_, url.c_str());
-#endif
-
-  if (!this->status_) {
-    this->client_.end();
-    return;
-  }
-
-  this->client_.setReuse(true);
-
-  // returned needed headers must be collected before the requests
-  this->client_.collectHeaders(header_keys, header_count);
-
-  // HTTP GET
-  this->status_ = this->client_.GET();
-
-  this->body_length_ = (size_t) this->client_.getSize();
-
-#if defined(USE_ESP32) || defined(USE_RP2040)
-  if (this->stream_ptr_ == nullptr) {
-    this->set_stream_ptr_();
-  }
-#endif
-}
-
-int OtaHttpRequestComponentArduino::http_read(uint8_t *buf, const size_t max_len) {
-#ifdef USE_ESP8266
-#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0)  // && USE_ARDUINO_VERSION_CODE < VERSION_CODE(?, ?, ?)
-  if (!this->secure_()) {
-    ESP_LOGW(TAG, "Using HTTP on Arduino version >= 3.1 is **very** slow. Consider setting framework version to 3.0.2 "
-                  "in your YAML, or use HTTPS");
-  }
-#endif  // USE_ARDUINO_VERSION_CODE
-#endif  // USE_ESP8266
-
-  watchdog::WatchdogManager wdts;
-
-  // Since arduino8266 >= 3.1 using this->stream_ptr_ is broken (https://github.com/esp8266/Arduino/issues/9035)
-  WiFiClient *stream_ptr = this->client_.getStreamPtr();
-  if (stream_ptr == nullptr) {
-    ESP_LOGE(TAG, "Stream pointer vanished!");
-    return -1;
-  }
-
-  int available_data = stream_ptr->available();
-  int bufsize = std::min((int) max_len, available_data);
-  if (bufsize > 0) {
-    stream_ptr->readBytes(buf, bufsize);
-    this->bytes_read_ += bufsize;
-    buf[bufsize] = '\0';  // not fed to ota
-  }
-
-  return bufsize;
-}
-
-void OtaHttpRequestComponentArduino::http_end() {
-  watchdog::WatchdogManager wdts;
-  this->client_.end();
-}
-
-int OtaHttpRequestComponentArduino::set_stream_ptr_() {
-#ifdef USE_ESP8266
-#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
-  if (this->secure_()) {
-    ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure");
-    this->stream_ptr_ = std::make_unique<WiFiClientSecure>();
-    WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(this->stream_ptr_.get());
-    secure_client->setBufferSizes(this->max_http_recv_buffer_, 512);
-    secure_client->setInsecure();
-  } else {
-    this->stream_ptr_ = std::make_unique<WiFiClient>();
-  }
-#else
-  ESP_LOGV(TAG, "ESP8266 HTTP connection with WiFiClient");
-  if (this->secure_()) {
-    ESP_LOGE(TAG, "Can't use HTTPS connection with esp8266_disable_ssl_support");
-    return -1;
-  }
-  this->stream_ptr_ = std::make_unique<WiFiClient>();
-#endif  // USE_HTTP_REQUEST_ESP8266_HTTPS
-#endif  // USE_ESP8266
-
-#if defined(USE_ESP32) || defined(USE_RP2040)
-  this->stream_ptr_ = std::unique_ptr<WiFiClient>(this->client_.getStreamPtr());
-#endif
-  return 0;
-}
-
-}  // namespace http_request
-}  // namespace esphome
-
-#endif  // USE_ARDUINO
diff --git a/esphome/components/http_request/ota/ota_http_request_arduino.h b/esphome/components/http_request/ota/ota_http_request_arduino.h
deleted file mode 100644
index 02bc046520..0000000000
--- a/esphome/components/http_request/ota/ota_http_request_arduino.h
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma once
-
-#include "ota_http_request.h"
-
-#ifdef USE_ARDUINO
-#include "esphome/core/automation.h"
-#include "esphome/core/component.h"
-#include "esphome/core/defines.h"
-
-#include <memory>
-#include <string>
-#include <utility>
-
-#if defined(USE_ESP32) || defined(USE_RP2040)
-#include <HTTPClient.h>
-#endif
-#ifdef USE_ESP8266
-#include <ESP8266HTTPClient.h>
-#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
-#include <WiFiClientSecure.h>
-#endif
-#endif
-
-namespace esphome {
-namespace http_request {
-
-class OtaHttpRequestComponentArduino : public OtaHttpRequestComponent {
- public:
-  void http_init(const std::string &url) override;
-  int http_read(uint8_t *buf, size_t len) override;
-  void http_end() override;
-
- protected:
-  int set_stream_ptr_();
-  HTTPClient client_{};
-  std::unique_ptr<WiFiClient> stream_ptr_;
-};
-
-}  // namespace http_request
-}  // namespace esphome
-
-#endif  // USE_ARDUINO
diff --git a/esphome/components/http_request/ota/ota_http_request_idf.cpp b/esphome/components/http_request/ota/ota_http_request_idf.cpp
deleted file mode 100644
index 9fa565d9bb..0000000000
--- a/esphome/components/http_request/ota/ota_http_request_idf.cpp
+++ /dev/null
@@ -1,86 +0,0 @@
-#include "ota_http_request_idf.h"
-#include "watchdog.h"
-
-#ifdef USE_ESP_IDF
-#include "esphome/core/application.h"
-#include "esphome/core/defines.h"
-#include "esphome/core/log.h"
-#include "esphome/components/md5/md5.h"
-#include "esphome/components/network/util.h"
-
-#include "esp_event.h"
-#include "esp_http_client.h"
-#include "esp_idf_version.h"
-#include "esp_log.h"
-#include "esp_netif.h"
-#include "esp_system.h"
-#include "esp_task_wdt.h"
-#include "esp_tls.h"
-
-#include "freertos/FreeRTOS.h"
-#include "freertos/task.h"
-#include "nvs_flash.h"
-
-#include <cctype>
-#include <cinttypes>
-#include <cstdlib>
-#include <cstring>
-#include <sys/param.h>
-#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
-#include "esp_crt_bundle.h"
-#endif
-
-namespace esphome {
-namespace http_request {
-
-void OtaHttpRequestComponentIDF::http_init(const std::string &url) {
-  App.feed_wdt();
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
-  esp_http_client_config_t config = {nullptr};
-  config.url = url.c_str();
-  config.method = HTTP_METHOD_GET;
-  config.timeout_ms = (int) this->timeout_;
-  config.buffer_size = this->max_http_recv_buffer_;
-  config.auth_type = HTTP_AUTH_TYPE_BASIC;
-  config.max_authorization_retries = -1;
-#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
-  if (this->secure_()) {
-    config.crt_bundle_attach = esp_crt_bundle_attach;
-  }
-#endif
-#pragma GCC diagnostic pop
-
-  watchdog::WatchdogManager wdts;
-  this->client_ = esp_http_client_init(&config);
-  if ((this->status_ = esp_http_client_open(this->client_, 0)) == ESP_OK) {
-    this->body_length_ = esp_http_client_fetch_headers(this->client_);
-    this->status_ = esp_http_client_get_status_code(this->client_);
-  }
-}
-
-int OtaHttpRequestComponentIDF::http_read(uint8_t *buf, const size_t max_len) {
-  watchdog::WatchdogManager wdts;
-  int bufsize = std::min(max_len, this->body_length_ - this->bytes_read_);
-
-  App.feed_wdt();
-  int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
-  if (read_len > 0) {
-    this->bytes_read_ += bufsize;
-    buf[bufsize] = '\0';  // not fed to ota
-  }
-
-  return read_len;
-}
-
-void OtaHttpRequestComponentIDF::http_end() {
-  watchdog::WatchdogManager wdts;
-
-  esp_http_client_close(this->client_);
-  esp_http_client_cleanup(this->client_);
-}
-
-}  // namespace http_request
-}  // namespace esphome
-
-#endif  // USE_ESP_IDF
diff --git a/esphome/components/http_request/ota/ota_http_request_idf.h b/esphome/components/http_request/ota/ota_http_request_idf.h
deleted file mode 100644
index 9783b2a3e1..0000000000
--- a/esphome/components/http_request/ota/ota_http_request_idf.h
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-
-#include "ota_http_request.h"
-
-#ifdef USE_ESP_IDF
-#include "esp_http_client.h"
-
-namespace esphome {
-namespace http_request {
-
-class OtaHttpRequestComponentIDF : public OtaHttpRequestComponent {
- public:
-  void http_init(const std::string &url) override;
-  int http_read(uint8_t *buf, size_t len) override;
-  void http_end() override;
-
- protected:
-  esp_http_client_handle_t client_{};
-};
-
-}  // namespace http_request
-}  // namespace esphome
-
-#endif  // USE_ESP_IDF
diff --git a/esphome/components/http_request/ota/watchdog.cpp b/esphome/components/http_request/watchdog.cpp
similarity index 79%
rename from esphome/components/http_request/ota/watchdog.cpp
rename to esphome/components/http_request/watchdog.cpp
index 663c9afaac..e609feb4dd 100644
--- a/esphome/components/http_request/ota/watchdog.cpp
+++ b/esphome/components/http_request/watchdog.cpp
@@ -1,7 +1,5 @@
 #include "watchdog.h"
 
-#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
-
 #include "esphome/core/application.h"
 #include "esphome/core/log.h"
 
@@ -20,14 +18,22 @@ namespace esphome {
 namespace http_request {
 namespace watchdog {
 
-static const char *const TAG = "watchdog.http_request.ota";
+static const char *const TAG = "http_request.watchdog";
 
-WatchdogManager::WatchdogManager() {
+WatchdogManager::WatchdogManager(uint32_t timeout_ms) : timeout_ms_(timeout_ms) {
+  if (timeout_ms == 0) {
+    return;
+  }
   this->saved_timeout_ms_ = this->get_timeout_();
-  this->set_timeout_(USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT);
+  this->set_timeout_(timeout_ms);
 }
 
-WatchdogManager::~WatchdogManager() { this->set_timeout_(this->saved_timeout_ms_); }
+WatchdogManager::~WatchdogManager() {
+  if (this->timeout_ms_ == 0) {
+    return;
+  }
+  this->set_timeout_(this->saved_timeout_ms_);
+}
 
 void WatchdogManager::set_timeout_(uint32_t timeout_ms) {
   ESP_LOGV(TAG, "Adjusting WDT to %" PRIu32 "ms", timeout_ms);
@@ -68,4 +74,3 @@ uint32_t WatchdogManager::get_timeout_() {
 }  // namespace watchdog
 }  // namespace http_request
 }  // namespace esphome
-#endif
diff --git a/esphome/components/http_request/ota/watchdog.h b/esphome/components/http_request/watchdog.h
similarity index 84%
rename from esphome/components/http_request/ota/watchdog.h
rename to esphome/components/http_request/watchdog.h
index 0a09dcd6fa..9b54ae6c82 100644
--- a/esphome/components/http_request/ota/watchdog.h
+++ b/esphome/components/http_request/watchdog.h
@@ -9,9 +9,8 @@ namespace http_request {
 namespace watchdog {
 
 class WatchdogManager {
-#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
  public:
-  WatchdogManager();
+  WatchdogManager(uint32_t timeout_ms);
   ~WatchdogManager();
 
  private:
@@ -19,7 +18,7 @@ class WatchdogManager {
   void set_timeout_(uint32_t timeout_ms);
 
   uint32_t saved_timeout_ms_{0};
-#endif
+  uint32_t timeout_ms_{0};
 };
 
 }  // namespace watchdog
diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py
index 0f1b7f236b..bd79d3b2f9 100644
--- a/esphome/cpp_types.py
+++ b/esphome/cpp_types.py
@@ -8,6 +8,7 @@ double = global_ns.namespace("double")
 bool_ = global_ns.namespace("bool")
 int_ = global_ns.namespace("int")
 std_ns = global_ns.namespace("std")
+std_shared_ptr = std_ns.class_("shared_ptr")
 std_string = std_ns.class_("string")
 std_vector = std_ns.class_("vector")
 uint8 = global_ns.namespace("uint8_t")
diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml
new file mode 100644
index 0000000000..2b6996c0b9
--- /dev/null
+++ b/tests/components/http_request/common.yaml
@@ -0,0 +1,75 @@
+substitutions:
+  verify_ssl: "true"
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+esphome:
+  on_boot:
+    then:
+      - http_request.get:
+          url: https://esphome.io
+          headers:
+            Content-Type: application/json
+          on_response:
+            then:
+              - logger.log:
+                  format: "Response status: %d, Duration: %u ms"
+                  args:
+                    - response->status_code
+                    - response->duration_ms
+      - http_request.post:
+          url: https://esphome.io
+          headers:
+            Content-Type: application/json
+          json:
+            key: value
+      - http_request.send:
+          method: PUT
+          url: https://esphome.io
+          headers:
+            Content-Type: application/json
+          body: "Some data"
+
+http_request:
+  useragent: esphome/tagreader
+  timeout: 10s
+  verify_ssl: ${verify_ssl}
+
+ota:
+  - platform: http_request
+    on_begin:
+      then:
+        - logger.log: "OTA start"
+    on_progress:
+      then:
+        - logger.log:
+            format: "OTA progress %0.1f%%"
+            args: ["x"]
+    on_end:
+      then:
+        - logger.log: "OTA end"
+    on_error:
+      then:
+        - logger.log:
+            format: "OTA update error %d"
+            args: ["x"]
+    on_state_change:
+      then:
+        lambda: 'ESP_LOGD("ota", "State %d", state);'
+
+button:
+  - platform: template
+    name: Firmware update
+    on_press:
+      then:
+        - ota.http_request.flash:
+            md5_url: http://my.ha.net:8123/local/esphome/firmware.md5
+            url: http://my.ha.net:8123/local/esphome/firmware.bin
+
+        - ota.http_request.flash:
+            md5: 0123456789abcdef0123456789abcdef
+            url: http://my.ha.net:8123/local/esphome/firmware.bin
+
+        - logger.log: "This message should be not displayed (reboot)"
diff --git a/tests/components/http_request/common_http_request.yaml b/tests/components/http_request/common_http_request.yaml
deleted file mode 100644
index b00768c736..0000000000
--- a/tests/components/http_request/common_http_request.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-esphome:
-  on_boot:
-    then:
-      - http_request.get:
-          url: https://esphome.io
-          headers:
-            Content-Type: application/json
-          verify_ssl: false
-          on_response:
-            then:
-              - logger.log:
-                  format: 'Response status: %d, Duration: %u ms'
-                  args:
-                    - status_code
-                    - duration_ms
-      - http_request.post:
-          url: https://esphome.io
-          headers:
-            Content-Type: application/json
-          json:
-            key: value
-          verify_ssl: false
-      - http_request.send:
-          method: PUT
-          url: https://esphome.io
-          headers:
-            Content-Type: application/json
-          body: "Some data"
-          verify_ssl: false
-
-http_request:
-  useragent: esphome/tagreader
-  timeout: 10s
diff --git a/tests/components/http_request/common_ota.yaml b/tests/components/http_request/common_ota.yaml
deleted file mode 100644
index 10e7d54c3f..0000000000
--- a/tests/components/http_request/common_ota.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-  - platform: http_request
-    verify_ssl: ${verify_ssl}
-    on_begin:
-      then:
-        - logger.log: "OTA start"
-    on_progress:
-      then:
-        - logger.log:
-            format: "OTA progress %0.1f%%"
-            args: ["x"]
-    on_end:
-      then:
-        - logger.log: "OTA end"
-    on_error:
-      then:
-        - logger.log:
-            format: "OTA update error %d"
-            args: ["x"]
-    on_state_change:
-      then:
-        lambda: 'ESP_LOGD("ota", "State %d", state);'
-
-button:
-  - platform: template
-    name: Firmware update
-    on_press:
-      then:
-        - ota_http_request.flash:
-            md5_url: http://my.ha.net:8123/local/esphome/firmware.md5
-            url: http://my.ha.net:8123/local/esphome/firmware.bin
-        - logger.log: "This message should be not displayed (reboot)"
diff --git a/tests/components/http_request/test-nossl.esp8266.yaml b/tests/components/http_request/test-nossl.esp8266.yaml
index 65116d5550..9fc4706c89 100644
--- a/tests/components/http_request/test-nossl.esp8266.yaml
+++ b/tests/components/http_request/test-nossl.esp8266.yaml
@@ -1,38 +1,4 @@
-<<: !include common_http_request.yaml
+<<: !include common.yaml
 
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-  - platform: http_request
-    esp8266_disable_ssl_support: true
-    on_begin:
-      then:
-        - logger.log: "OTA start"
-    on_progress:
-      then:
-        - logger.log:
-            format: "OTA progress %0.1f%%"
-            args: ["x"]
-    on_end:
-      then:
-        - logger.log: "OTA end"
-    on_error:
-      then:
-        - logger.log:
-            format: "OTA update error %d"
-            args: ["x"]
-    on_state_change:
-      then:
-        lambda: 'ESP_LOGD("ota", "State %d", state);'
-
-button:
-  - platform: template
-    name: Firmware update
-    on_press:
-      then:
-        - ota_http_request.flash:
-            md5_url: http://my.ha.net:8123/local/esphome/firmware.md5
-            url: http://my.ha.net:8123/local/esphome/firmware.bin
-        - logger.log: "This message should be not displayed (reboot)"
+http_request:
+  esp8266_disable_ssl_support: true
diff --git a/tests/components/http_request/test.esp32-c3-idf.yaml b/tests/components/http_request/test.esp32-c3-idf.yaml
index da629e83a9..ee2f5aa59b 100644
--- a/tests/components/http_request/test.esp32-c3-idf.yaml
+++ b/tests/components/http_request/test.esp32-c3-idf.yaml
@@ -1,4 +1,4 @@
 substitutions:
   verify_ssl: "true"
 
-<<: !include common_ota.yaml
+<<: !include common.yaml
diff --git a/tests/components/http_request/test.esp32-c3.yaml b/tests/components/http_request/test.esp32-c3.yaml
index 1f597bb500..c1937b5a10 100644
--- a/tests/components/http_request/test.esp32-c3.yaml
+++ b/tests/components/http_request/test.esp32-c3.yaml
@@ -1,5 +1,4 @@
 substitutions:
   verify_ssl: "false"
 
-<<: !include common_http_request.yaml
-<<: !include common_ota.yaml
+<<: !include common.yaml
diff --git a/tests/components/http_request/test.esp32-idf.yaml b/tests/components/http_request/test.esp32-idf.yaml
index da629e83a9..ee2f5aa59b 100644
--- a/tests/components/http_request/test.esp32-idf.yaml
+++ b/tests/components/http_request/test.esp32-idf.yaml
@@ -1,4 +1,4 @@
 substitutions:
   verify_ssl: "true"
 
-<<: !include common_ota.yaml
+<<: !include common.yaml
diff --git a/tests/components/http_request/test.esp32.yaml b/tests/components/http_request/test.esp32.yaml
index 1f597bb500..c1937b5a10 100644
--- a/tests/components/http_request/test.esp32.yaml
+++ b/tests/components/http_request/test.esp32.yaml
@@ -1,5 +1,4 @@
 substitutions:
   verify_ssl: "false"
 
-<<: !include common_http_request.yaml
-<<: !include common_ota.yaml
+<<: !include common.yaml
diff --git a/tests/components/http_request/test.esp8266.yaml b/tests/components/http_request/test.esp8266.yaml
index 1f597bb500..c1937b5a10 100644
--- a/tests/components/http_request/test.esp8266.yaml
+++ b/tests/components/http_request/test.esp8266.yaml
@@ -1,5 +1,4 @@
 substitutions:
   verify_ssl: "false"
 
-<<: !include common_http_request.yaml
-<<: !include common_ota.yaml
+<<: !include common.yaml
diff --git a/tests/components/http_request/test.rp2040.yaml b/tests/components/http_request/test.rp2040.yaml
index 077e4d82da..c1937b5a10 100644
--- a/tests/components/http_request/test.rp2040.yaml
+++ b/tests/components/http_request/test.rp2040.yaml
@@ -1,4 +1,4 @@
 substitutions:
   verify_ssl: "false"
 
-<<: !include common_ota.yaml
+<<: !include common.yaml
diff --git a/tests/test1.yaml b/tests/test1.yaml
index c49ff307e5..2dacfda536 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -25,31 +25,6 @@ esphome:
     then:
       - lambda: >-
           ESP_LOGV("main", "ON LOOP!");
-      - http_request.get:
-          url: https://esphome.io
-          headers:
-            Content-Type: application/json
-          verify_ssl: false
-      - http_request.post:
-          url: https://esphome.io
-          verify_ssl: false
-          json:
-            key: !lambda |-
-              return id(${textname}_text).state;
-            greeting: Hello World
-      - http_request.send:
-          method: PUT
-          url: https://esphome.io
-          headers:
-            Content-Type: application/json
-          body: Some data
-          verify_ssl: false
-          on_response:
-            then:
-              - logger.log:
-                  format: "Response status: %d"
-                  args:
-                    - status_code
   build_path: build/test1
 
 packages:
@@ -84,10 +59,6 @@ network:
 mdns:
   disabled: false
 
-http_request:
-  useragent: esphome/device
-  timeout: 10s
-
 mqtt:
   broker: "192.168.178.84"
   port: 1883
diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml
index 018a4d94f3..c3b078fe67 100644
--- a/tests/test3.1.yaml
+++ b/tests/test3.1.yaml
@@ -447,26 +447,6 @@ switch:
     switches:
       - id: custom_switch
         name: Custom Switch
-        on_turn_on:
-          - http_request.get:
-              url: https://esphome.io
-              headers:
-                Content-Type: application/json
-              verify_ssl: false
-          - http_request.post:
-              url: https://esphome.io
-              verify_ssl: false
-              json:
-                key: !lambda |-
-                  return id(custom_text_sensor).state;
-                greeting: Hello World
-          - http_request.send:
-              method: PUT
-              url: https://esphome.io
-              headers:
-                Content-Type: application/json
-              body: Some data
-              verify_ssl: false
   - platform: template
     name: open_vent
     id: open_vent
@@ -722,10 +702,6 @@ display:
     lambda: |-
       it.printdigit("hello");
 
-http_request:
-  useragent: esphome/device
-  timeout: 10s
-
 button:
   - platform: output
     id: output_button
diff --git a/tests/test7.yaml b/tests/test7.yaml
index b22fbfbcb4..ac193eae4e 100644
--- a/tests/test7.yaml
+++ b/tests/test7.yaml
@@ -1,7 +1,7 @@
 # Tests for ESP32-C3 boards which use toolchain-riscv32-esp
 ---
 wifi:
-  ssid: 'ssid'
+  ssid: "ssid"
 
 network:
   enable_ipv6: true
@@ -12,31 +12,12 @@ esp32:
     type: arduino
 
 esphome:
-  name: 'on-response-test'
-  on_boot:
-    then:
-      - http_request.send:
-          method: PUT
-          url: https://esphome.io
-          headers:
-            Content-Type: application/json
-          body: Some data
-          verify_ssl: false
-          on_response:
-            then:
-              - logger.log:
-                  format: "Response status: %d"
-                  args:
-                    - status_code
+  name: test7
 
 logger:
 
 debug:
 
-http_request:
-  useragent: esphome/tagreader
-  timeout: 10s
-
 sensor:
   - platform: adc
     id: adc_sensor_p4