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:
- Evaluating each library in real Odoo flows rather than in any artificial benchmarks.
- Ensuring its compatibility with Odoo synchronous WSGI worker.
- Measuring response time by analyzing timing logs in server log.
- Ensuring that there will be no need for code modification when switching between libraries in any Odoo module.
- 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:
| Factor | Why httpx Wins for Odoo |
| Synchronicity | Works natively in Odoo's blocking WSGI worker process without any async bridging or tricks. |
| Requests API | Request-like API allows using the compatibility layer, which is thin, auditable, and transparent for all Odoo modules. |
| Connection Pooling | Allows httpx.Client to reuse connections in subsequent outbound calls and reduce the cost of DNS lookups and TLS handshakes. |
| HTTP/2 Protocol | Provides a future-proof integration of Odoo with payment gateways and government EDI systems, which use HTTP/2 multiplexing. |
| Timeouts | Uses tight default timeouts, which avoid any hanging of workers when making slow API calls. |
| Perceived Performance | Logging 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 Exception | httpx Equivalent |
| RequestException | httpx.RequestError |
| ConnectionError | httpx.ConnectError |
| HTTPError | httpx.HTTPStatusError |
| TooManyRedirects | httpx.TooManyRedirects |
| ConnectTimeout | httpx.ConnectTimeout |
| ReadTimeout | httpx.ReadTimeout |
| Timeout | httpx.TimeoutException |
| ChunkedEncodingError | httpx.ReadError |
| ContentDecodingError | httpx.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.

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.

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.

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.

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.

6. Other Validated Areas
Beyond the five core workflows above, the module was confirmed compatible with the following Odoo network-dependent areas:
| Odoo Feature / Module | External 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
| File | Purpose |
| __manifest__.py | Odoo module descriptor - name, version, dependencies, license. |
| __init__.py | Entry point: imports the models package. |
| models/__init__.py | Imports http_override to trigger the patch at load time. |
| models/http_override.py | Core 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:

Subsequent HTTP calls from Odoo will log their timing at the INFO level.
Known Limitations & Risks
| Risk | Details |
| Streaming responses | stream=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 uploads | Large streaming upload behaviour may differ slightly from native requests. |
| Session.mount() adapters | Completely ignored. Any Odoo code relying on custom transport adapters (retries, proxy routing) will silently receive no-op behaviour. |
| SSL verify per-request | Changing verify=False on individual calls does not dynamically reconfigure the httpx.Client: the client-level SSL setting applies. |
| Third-party library compatibility | Some 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 cases | Subtle differences in header handling or redirect behaviour between httpx and requests may surface in rarely-tested payment gateway integrations. |
Configuration Reference
| Setting | Default / Description |
| httpx.Client timeout | None by default - no timeout, matching requests. Override per-call by passing timeout= in kwargs. |
| follow_redirects | True 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.