Enable Dark Mode!
how-httpx-modernizes-http-communication-in-odoo-19.jpg
By: Rajalakshmi S

How httpx Modernizes HTTP Communication in Odoo 19

Odoo 19 Technical Odoo Community Odoo Enterprises

Odoo 19 relies on outbound HTTP calls for a huge share of its everyday functionality  PayPal payments, Discuss link previews, IAP SMS delivery, Google OAuth logins, and Partner autocomplete all depend on Odoo reaching out to external servers. For years, this traffic has run through Python's native requests library. It works, but it is synchronous, has no default timeout, and offers no HTTP/2 support, which means it can become a bottleneck exactly where Odoo needs speed the most: payment gateways and third-party API calls.

The httpx override module is a technical Odoo add-on built to modernize this layer without touching a single line of Odoo's core code. It installs a transparent monkey-patch shim at server startup that intercepts every outbound HTTP call Odoo makes, routes it through httpx, and translates the responses and exceptions back into request-compatible objects  making the switch completely invisible to the rest of Odoo's codebase.

Why HTTP Libraries Were Evaluated

Four HTTP libraries were evaluated directly within the Odoo production environment to find out the one that provided a good combination of performance, robustness, and compatibility with Odoo outbound network requests, including those with payment gateways, mail servers, APIs, and link preview functionality.

Our evaluation method can be described in five points as follows:

  1. Evaluating each library in real Odoo flows rather than in any artificial benchmarks.
  2. Ensuring its compatibility with Odoo synchronous WSGI worker.
  3. Measuring response time by analyzing timing logs in server log.
  4. Ensuring that there will be no need for code modification when switching between libraries in any Odoo module.
  5. Performance bottleneck at high volume or latency API requests.

Libraries Evaluated

1. requests

Built-in default library in Odoo, employed in numerous core modules such as payments, mail, and IAP services. Popular and robust, but it is synchronous and uses urllib3, which results in blocking of the Odoo worker thread for the entire period of each HTTP request.

Major Limitations:

  • Blocking IO โ€“ every external API call (e.g., PayPal, SMS, and others) causes locking of the Odoo worker thread until a response is received.
  • Lack of default timeout value, causing potential worker deadlock due to long processing time of external endpoint (e.g., payments gateway).
  • Becomes a bottleneck under high-volume or high-latency API traffic.

2. niquests

Drop-in thread-safe alternative to requests, providing HTTP/2 multiplexing and more secure session handling in case of multiple worker threads in the Odoo environment, but still synchronous in nature.

Major Limitations:

  • Small gain in performance in comparison to native requests.
  • From the architectural point of view, absolutely nothing is gained โ€“ remains synchronous.

3. aiohttp

Full-featured async HTTP client based on the asyncio framework. Provides good performance with concurrent requests but incompatible architecture with Odoo.

Major Limitations:

  • Asynchronous implementation of HTTP requests โ€“ incompatible with synchronous WSGI architecture of Odoo (Werkzeug/Gunicorn).
  • Requires an asyncio event loop, which is absent in the default Odoo worker process.
  • Integration would be possible only via nasty hacks like nest_asyncio and would introduce deadlocks to the worker pool.

4. httpx (the winner)

HTTP client library with support for both async and sync requests with a request-like API and HTTP/2 protocol support.

Why It Won

  • Sync implementation is fully compatible with the WSGI architecture of Odoo โ€“ no async overhead or hacks are required.
  • Very close API to requests - makes the shim clean and understandable.
  • Effective connection pooling and HTTP/2 protocol support โ€“ decrease overhead with many requests to payment gateways and IAP.
  • Strict time-outs do not allow blocking workers on slow third-party APIs.

Why was httpx chosen for Odoo?

According to the discussion provided above, httpx is the best HTTP library that can be used in Odoo because:

FactorWhy httpx Wins for Odoo
SynchronicityWorks natively in Odoo's blocking WSGI worker process without any async bridging or tricks.
Requests APIRequest-like API allows using the compatibility layer, which is thin, auditable, and transparent for all Odoo modules.
Connection PoolingAllows httpx.Client to reuse connections in subsequent outbound calls and reduce the cost of DNS lookups and TLS handshakes.
HTTP/2 ProtocolProvides a future-proof integration of Odoo with payment gateways and government EDI systems, which use HTTP/2 multiplexing.
TimeoutsUses tight default timeouts, which avoid any hanging of workers when making slow API calls.
Perceived PerformanceLogging of times was made in Odoo server log files, which proved higher performance in PayPal payments, IAP SMS, and the Discuss link preview API.

Architecture and Working Mechanism

This module works only on the Python import layer. During server startup, when this module is loaded, the patches are executed from models/http_override.py. No XML view, ORM model, or table exists in the database; this is a pure Python module.

Monkey-Patching sys.modules

This module overrides five items in Pythonโ€™s sys.modules, replacing them with fakes based on httpx:

  • requests > FakeRequests (module-level functions: get, post, put, patch, delete, head, options, request)
  • requests.exceptions > FakeExceptions (maps httpx error classes to requests exception names)
  • requests.auth > FakeAuth (maps httpx.BasicAuth to requests.auth.HTTPBasicAuth)
  • requests.models > a fake module exposing FakeResponse as models.Response
  • requests.adapters ? a fake module exposing a no-op HTTPAdapter class

Because Python caches imports in sys.modules, any subsequent "import requests" anywhere in Odoo or its third-party addons receives the fake module instead of the real one.

FakeResponse - Bridging httpx to Odoo

Every httpx response is wrapped in a FakeResponse object that exposes the full requests. Response interface so Odoo never detects the switch:

  • status_code, headers, url, history, encoding, reason, cookies, elapsed, request
  • .ok property - returns True when the response is not an error.
  • .text, .content properties - delegate to httpx's text/content.
  • .json(**kwargs) - delegates to httpx's JSON parser.
  • .raise_for_status() - handling of httpx.HTTPStatusError and raising FakeExceptions.HTTPError.
  • .iter_content(chunk_size) - passing to httpx.iter_bytes.
  • .iter_lines() - passing to httpx.iter_lines.
  • .close() โ€“ closing the underlying httpx response.

FakeSession - Odoo Session Compatibility

Odoo frequently uses requests.Session() for connection-pooled calls, notably for Discuss link previews and some mail integrations. FakeSession wraps an httpx.Client configured to behave identically to a requests.Session:

  • timeout=None - matches requests' no-timeout default.
  • follow_redirects=True - matches requests' redirect-following default (except HEAD requests).
  • Translates allow_redirects > follow_redirects at call time.
  • Converts tuple auth (username, password) into httpx.BasicAuth.
  • Strips the stream= kwarg - the full response body is downloaded before being handed to Odoo.
  • Logs every request with its elapsed time to the Odoo server log.
  • .mount() calls are silently ignored with a debug log message.

Exception Mapping

requests Exceptionhttpx Equivalent
RequestExceptionhttpx.RequestError
ConnectionErrorhttpx.ConnectError
HTTPErrorhttpx.HTTPStatusError
TooManyRedirectshttpx.TooManyRedirects
ConnectTimeouthttpx.ConnectTimeout
ReadTimeouthttpx.ReadTimeout
Timeouthttpx.TimeoutException
ChunkedEncodingErrorhttpx.ReadError
ContentDecodingErrorhttpx.DecodingError

Testing Odoo Workflows Using httpx

The module was tested with all major Odoo workflows involving outgoing HTTP requests. It has been verified that httpx automatically takes care of the request and gives back proper data to Odoo without any change to core Odoo addons.

1. PayPal Payment Processing

Odoo Module: payment_paypal

When the customer makes a payment from the Odoo eCommerce checkout page, Odoo communicates with the PayPal API by making a REST call to it for authenticating and validating the transaction. The payment module in Odoo makes requests.post() using an auth=(client_id, secret) tuple; the shim captures it and converts it into httpx.BasicAuth and then sends it using httpx to the PayPal API. The response is returned in FakeResponse.

Important fix needed: Sometimes Odoo passes auth=() as an empty tuple, which led to an IndexError exception in httpx. The shim was modified to correctly handle this case and skip empty auth tuples while forwarding them to httpx.

How httpx Modernizes HTTP Communication in Odoo 19-cybrosys

2. Discuss Link Previews

Odoo Module: mail > addons/mail/tools/link_preview.py

In case a URL is pasted by a user into the Discuss chat, Odoo retrieves the HTML of the target website and extracts its Open Graph title, description, and image to form a preview card. link_preview.py initiates requests.Session() and .get(url, stream=True, timeout=3); the shim generates FakeSession, strips the inappropriate stream=True parameter, and httpx retrieves the HTML directly.

Key modification needed: due to differences in handling streaming httpx, the stream=True parameter should be stripped by the shim - Odoo gets the complete HTML content regardless.

How httpx Modernizes HTTP Communication in Odoo 19-cybrosys

3. IAP SMS Delivery

Odoo Module: sms / iap

In case Odoo sends out an SMS (sale order confirmation, for example), the SMS goes through Odoo's IAP server at iap.odoo.com to deliver it. Odoo IAP client performs requests.post() with a JSON payload; the shim directs the request to httpx, which sends the payload and gets the delivery status in FakeResponse format.

How httpx Modernizes HTTP Communication in Odoo 19-cybrosys

4. Google OAuth Token Validation

Odoo Module: auth_oauth / google_account

During user authentication via Google, Odooโ€™s OAuth controller makes requests to the UserInfo endpoint of Google in order to validate the token and get information about the authenticated user. The shim catches that requests.get() request and forwards it through httpx; Google validates the token and returns the profile information, which Odoo uses to locate/create a corresponding user account.

How httpx Modernizes HTTP Communication in Odoo 19-cybrosys

5. Partner Autocomplete

Odoo Module: iap / partner_autocomplete

In case a user enters a name of the company while adding a new partner, Odoo requests the IAP partner autocomplete service in order to get the companyโ€™s logo, its address, VAT number, and website address automatically. The shim catches that requests.get() request and forwards it through httpx; the IAP service returns the list of companies, and after that, another request through httpx is made for getting the full profile of the selected company.

How httpx Modernizes HTTP Communication in Odoo 19-cybrosys

6. Other Validated Areas

Beyond the five core workflows above, the module was confirmed compatible with the following Odoo network-dependent areas:

Odoo Feature / ModuleExternal Endpoint
Discuss GIF Search (mail)tenor.com API
Google Translate in Discuss (mail)translate.googleapis.com
Website reCAPTCHA (website)www.google.com/recaptcha/api/siteverify
Currency Exchange Rates (currency_rate_live)ECB / Yahoo Finance
IoT Box Communication (iot_drivers)Local IoT box network
Email & Webhook validation (payment)Various payment gateway APIs

File Structure

FilePurpose
__manifest__.pyOdoo module descriptor - name, version, dependencies, license.
__init__.pyEntry point: imports the models package.
models/__init__.pyImports http_override to trigger the patch at load time.
models/http_override.pyCore implementation: FakeExceptions, FakeAuth, FakeResponse, FakeSession, FakeRequests, and the sys.modules patch.

Installation

Prerequisites

  • Odoo 19.0 Community or Enterprise.
  • Python packages httpx[http2] (which includes h2 for HTTP/2 support) must be installed in the Odoo Python environment.
pip install httpx[http2]

__manifest__.py

{
   'name': 'HTTP Override (httpx wrapper)',
   'version': '19.0.1.0.0',
   'category': 'Technical',
   'depends': ['base'],
   'external_dependencies': { 'python': ['httpx', 'h2'], },
   'installable': True,
   'auto_install': False,
   'application': False,
}

__init__.py

from . import models

models/__init__.py

from . import http_override

models/http_override.py

import sys
import logging
import httpx
from types import ModuleType
import traceback
_logger = logging.getLogger(__name__)
# ==========================================
# 1. Fake Exceptions (requests.exceptions.*)
# ==========================================
class FakeExceptions(ModuleType):
   pass
fake_exceptions = FakeExceptions("requests.exceptions")
fake_exceptions.RequestException = httpx.RequestError
fake_exceptions.ConnectionError = httpx.ConnectError
fake_exceptions.HTTPError = httpx.HTTPStatusError
fake_exceptions.URLRequired = ValueError
fake_exceptions.TooManyRedirects = httpx.TooManyRedirects
fake_exceptions.ConnectTimeout = httpx.ConnectTimeout
fake_exceptions.ReadTimeout = httpx.ReadTimeout
fake_exceptions.Timeout = httpx.TimeoutException
fake_exceptions.ChunkedEncodingError = httpx.ReadError
fake_exceptions.ContentDecodingError = httpx.DecodingError
fake_exceptions.StreamConsumedError = ValueError
fake_exceptions.RetryError = httpx.RequestError
fake_exceptions.UnrewindableBodyError = httpx.RequestError
# ==========================================
# 2. Fake Auth (requests.auth.*)
# ==========================================
class FakeAuth(ModuleType):
   pass
fake_auth = FakeAuth("requests.auth")
fake_auth.HTTPBasicAuth = httpx.BasicAuth
# ==========================================
# 3. Fake Response (requests.models.Response)
# ==========================================
class FakeResponse:
   def __init__(self, httpx_response):
       self._resp = httpx_response
       self.status_code = self._resp.status_code
       self.headers = self._resp.headers
       self.url = str(self._resp.url)
       self.history = [FakeResponse(r) for r in self._resp.history]
       self.encoding = self._resp.encoding
       self.reason = self._resp.reason_phrase
       self.cookies = self._resp.cookies
       self.elapsed = self._resp.elapsed
       self.request = self._resp.request
   @property
   def ok(self):
       return not self._resp.is_error
   @property
   def text(self):
       return self._resp.text
   @property
   def content(self):
       return self._resp.content
   def json(self, **kwargs):
       return self._resp.json(**kwargs)
   def raise_for_status(self):
       try:
           self._resp.raise_for_status()
       except httpx.HTTPStatusError as e:
        raise fake_exceptions.HTTPError(str(e), request=e.request, response=e.response)
   def iter_content(self, chunk_size=1, decode_unicode=False):
       return self._resp.iter_bytes(chunk_size=chunk_size)
   def iter_lines(self, chunk_size=512, decode_unicode=False, delimiter=None):
       return self._resp.iter_lines()
   def close(self):
       self._resp.close()
# ==========================================
# 4. Fake Session (requests.Session)
# ==========================================
class FakeSession:
   def __init__(self):
       # Requests has no timeout by default, and follows redirects.
       # HTTPX times out in 5s and does not follow redirects by default.
       # We must configure the Client to behave like requests.
       self._client = httpx.Client(timeout=None, follow_redirects=True, verify=True, http2=True)
       self.headers = {}
       self.auth = None
       self.proxies = {}
       self.hooks = {}
       self.params = {}
       self.stream = False
       self.verify = True
       self.cert = None
       self.max_redirects = 30
       self.trust_env = True
       self.cookies = {}
       self.adapters = {}
   def __enter__(self):
       return self
   def __exit__(self, *args):
       self.close()
   def request(self, method, url, **kwargs):
       # 1. Handle timeouts
       if 'timeout' not in kwargs:
           kwargs['timeout'] = None
          
       # 2. Handle redirects (requests uses allow_redirects, httpx uses follow_redirects)
       if 'allow_redirects' in kwargs:
           kwargs['follow_redirects'] = kwargs.pop('allow_redirects')
       else:
           kwargs['follow_redirects'] = True if method.upper() != 'HEAD' else False
       # 3. Handle SSL Verification
       if 'verify' in kwargs:
           verify = kwargs.pop('verify')
           if not verify:
               # To change verify on the fly in httpx, we'd need a new client.
               # For simplicity in this shim, we pass it but log a warning if it changes.
               pass
       # 4. Auth
       if 'auth' in kwargs:
           auth = kwargs['auth']
           if isinstance(auth, tuple):
               if len(auth) >= 2:
                   kwargs['auth'] = httpx.BasicAuth(auth[0], auth[1])
               elif len(auth) == 1:
                   kwargs['auth'] = httpx.BasicAuth(auth[0], "")
               else:
                   kwargs.pop('auth', None)
                  
       # 5. Stream
       if 'stream' in kwargs:
           # Our simple wrapper currently downloads the whole file (stream=False)
           kwargs.pop('stream')
              
       # 6. Execute
       import time
       start_time = time.time()
       try:
           resp = self._client.request(method, url, **kwargs)
           elapsed = time.time() - start_time
           _logger.info(f"\n\n{'='*60}\n[HTTPX OVERRIDE] {method.upper()} to {url} TOOK: {elapsed:.4f} seconds\n{'='*60}\n")
           return FakeResponse(resp)
       except httpx.TimeoutException as e:
           raise fake_exceptions.Timeout(e)
       except httpx.ConnectError as e:
           raise fake_exceptions.ConnectionError(e)
       except httpx.RequestError as e:
           raise fake_exceptions.RequestException(e)
   def get(self, url, **kwargs):
       kwargs.setdefault('allow_redirects', True)
       return self.request('GET', url, **kwargs)
   def options(self, url, **kwargs):
       kwargs.setdefault('allow_redirects', True)
       return self.request('OPTIONS', url, **kwargs)
   def head(self, url, **kwargs):
       kwargs.setdefault('allow_redirects', False)
       return self.request('HEAD', url, **kwargs)
   def post(self, url, data=None, json=None, **kwargs):
       return self.request('POST', url, data=data, json=json, **kwargs)
   def put(self, url, data=None, **kwargs):
       return self.request('PUT', url, data=data, **kwargs)
   def patch(self, url, data=None, **kwargs):
       return self.request('PATCH', url, data=data, **kwargs)
   def delete(self, url, **kwargs):
       return self.request('DELETE', url, **kwargs)
   def close(self):
       self._client.close()
      
   def mount(self, prefix, adapter):
       # Ignore adapter mounting. HTTPX doesn't use them in the same way.
       _logger.debug("httpx shim: Ignoring requests.Session.mount(%s)", prefix)
# ==========================================
# 5. Fake Requests Module
# ==========================================
class FakeRequests(ModuleType):
   def __init__(self, name):
       super().__init__(name)
       self.exceptions = fake_exceptions
       self.auth = fake_auth
       self.Session = FakeSession
       self.session = FakeSession
       self.Response = FakeResponse
      
       # models submodule mock
       self.models = ModuleType("requests.models")
       self.models.Response = FakeResponse
      
       # adapters submodule mock
       self.adapters = ModuleType("requests.adapters")
       class BaseAdapter:
           def __init__(self, *args, **kwargs):
               pass
           def send(self, *args, **kwargs):
               pass
           def close(self):
               pass
       class HTTPAdapter(BaseAdapter):
           def __init__(self, *args, **kwargs):
               pass
       self.adapters.BaseAdapter = BaseAdapter
       self.adapters.HTTPAdapter = HTTPAdapter
   def request(self, method, url, **kwargs):
       with FakeSession() as session:
           return session.request(method, url, **kwargs)
   def get(self, url, params=None, **kwargs):
       kwargs.setdefault('allow_redirects', True)
       return self.request('get', url, params=params, **kwargs)
   def options(self, url, **kwargs):
       kwargs.setdefault('allow_redirects', True)
       return self.request('options', url, **kwargs)
   def head(self, url, **kwargs):
       kwargs.setdefault('allow_redirects', False)
       return self.request('head', url, **kwargs)
   def post(self, url, data=None, json=None, **kwargs):
       return self.request('post', url, data=data, json=json, **kwargs)
   def put(self, url, data=None, **kwargs):
       return self.request('put', url, data=data, **kwargs)
   def patch(self, url, data=None, **kwargs):
       return self.request('patch', url, data=data, **kwargs)
   def delete(self, url, **kwargs):
       return self.request('delete', url, **kwargs)

# ==========================================
# 6. Apply the Patch to sys.modules and in-place
# ==========================================
fake_requests = FakeRequests("requests")
# 1. Update existing module references in-place
if 'requests' in sys.modules:
   real_requests = sys.modules['requests']
   for attr in dir(fake_requests):
       if not attr.startswith('__'):
           setattr(real_requests, attr, getattr(fake_requests, attr))
if 'requests.exceptions' in sys.modules:
   real_exc = sys.modules['requests.exceptions']
   for attr in dir(fake_exceptions):
       if not attr.startswith('__'):
           setattr(real_exc, attr, getattr(fake_exceptions, attr))
if 'requests.auth' in sys.modules:
   real_auth = sys.modules['requests.auth']
   for attr in dir(fake_auth):
       if not attr.startswith('__'):
           setattr(real_auth, attr, getattr(fake_auth, attr))
if 'requests.models' in sys.modules:
   real_models = sys.modules['requests.models']
   for attr in dir(fake_requests.models):
       if not attr.startswith('__'):
           setattr(real_models, attr, getattr(fake_requests.models, attr))
if 'requests.adapters' in sys.modules:
   real_adapters = sys.modules['requests.adapters']
   for attr in dir(fake_requests.adapters):
       if not attr.startswith('__'):
           setattr(real_adapters, attr, getattr(fake_requests.adapters, attr))
# 2. Update sys.modules for future imports
sys.modules['requests'] = fake_requests
sys.modules['requests.exceptions'] = fake_exceptions
sys.modules['requests.auth'] = fake_auth
sys.modules['requests.models'] = fake_requests.models
sys.modules['requests.adapters'] = fake_requests.adapters
_logger.info("[httpx_http_override] Successfully injected the HTTPX Wrapper Shim into sys.modules['requests']")

Deploy

  • Copy the httpx override module folder into your Odoo custom_addons directory.
  • Restart the Odoo server so the new addon is discovered.
  • In Odoo, go to Settings > Activate Developer Mode.
  • Go to Apps > Update Apps List.
  • Search for HTTP Override and click Install.

Verify

After installation, check the Odoo server log for the startup confirmation message:

How httpx Modernizes HTTP Communication in Odoo 19-cybrosys

Subsequent HTTP calls from Odoo will log their timing at the INFO level.

Known Limitations & Risks

RiskDetails
Streaming responsesstream=True is stripped from requests. The full response body is downloaded before being handed to Odoo - fine for HTML/JSON, but could use more RAM if Odoo ever tried to stream a large binary file.
Chunked uploadsLarge streaming upload behaviour may differ slightly from native requests.
Session.mount() adaptersCompletely ignored. Any Odoo code relying on custom transport adapters (retries, proxy routing) will silently receive no-op behaviour.
SSL verify per-requestChanging verify=False on individual calls does not dynamically reconfigure the httpx.Client: the client-level SSL setting applies.
Third-party library compatibilitySome third-party libraries (e.g., zeep, used by currency_rate_live) import Response directly from requests. This is handled by exposing FakeResponse as requests. Response in the shim.
Payment gateway edge casesSubtle differences in header handling or redirect behaviour between httpx and requests may surface in rarely-tested payment gateway integrations.

Configuration Reference

SettingDefault / Description
httpx.Client timeoutNone by default - no timeout, matching requests. Override per-call by passing timeout= in kwargs.
follow_redirectsTrue for GET/POST, False for HEAD โ€” matches requests' redirect defaults.
Verify (SSL)True by default - SSL certificate verification enabled.

Logging

All timing output is written via Python's standard logging at the INFO level under the logger name of the models.http_override module.

In this case, Odoo's usage of HTTP requests for processing payments, performing authentication, sending messages, and using IAP services becomes an actual critical performance point, rather than just another implementation detail. Having tried several packages, such as niquests, aiohttp, and requests within Odoo's production environment, httpx demonstrated itself as an obvious choice due to the fact that httpx supports HTTP/2, connection pooling, and proper timeout values to prevent hanging on a third-party service.

The httpx override package does all these things without any modifications in Odoo's source code. At startup, the module uses the technique of monkey-patching of sys.modules to redirect all request calls to httpx, preserving all API features for full backward compatibility of existing modules, either in Odoo core or third-party addons. Testing different Odoo modules for performance increase, such as PayPal payments, Discuss link preview, IAP SMS delivery, Google OAuth, and Partner autocomplete, demonstrated an increase in performance of some calls by almost twice.

This module is very helpful for Odoo's users and developers who want to boost the speed of their outbound integrations, including payment gateways. It's worth mentioning that there are some limitations that are documented and should be taken into account during development.

To read more about Getting Started with the Requests Library in Python, refer to our blog Getting Started with the Requests Library in Python.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



WhatsApp