1
0
mirror of https://github.com/apache/httpd.git synced 2026-10-23 22:49:01 +03:00
Files
apache/test/modules/proxy/env.py
Arturo Bernal 6da0450e00 test: Fix a flaky test_proxy_07_cache_control.py (seen on Windows CI).
The cache entry for the first request is committed by the CACHE_SAVE
filter on the EOS-only pass that follows the body, which can still be
running server side for an instant after the client has already read
the complete body. Two back-to-back requests with no delay can race
this, making the second one miss the cache and hit the backend again.

Give the commit room to land before relying on it, instead of
asserting immediately after the first request returns. Reproduced
locally (about 1 in 5 runs failing, on different parametrized cases
each time, none of them involving a comma) and confirmed fixed (0
failures in 55 runs) before and after this change.

Also harden the TCPFaker backend used by this test to close its
socket with an orderly shutdown() instead of an abrupt close(), which
removes one more way for a slow platform to see a reset instead of a
clean end of response.

GitHub: closes #864


git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1939305 13f79535-47bb-0310-9956-ffa450edef68
2026-10-21 05:12:57 +00:00

124 lines
3.8 KiB
Python

import inspect
import logging
import os
import socket
import sys
from threading import Thread
from pyhttpd.certs import CertificateSpec
from pyhttpd.env import HttpdTestEnv, HttpdTestSetup
log = logging.getLogger(__name__)
class TCPFaker:
# tcp backend for custom responses
def __init__(self, host, port):
self._thread = None
self._socket = None
self._host = host
self._port = port
self._done = False
self._request = None
def start(self):
def process():
self._socket.listen(1)
self._socket.settimeout(0.5)
self._process()
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._socket.bind((self._host, self._port))
self._thread = Thread(target=process, daemon=True)
self._thread.start()
def stop(self):
self._done = True
self._thread.join(timeout=5)
self._socket.close()
def _make_response(self, data):
return """HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 5
Hello""".encode()
def _process(self):
_read_buffer = 8192 if sys.platform == "win32" else 4096
while not self._done:
try:
c, client_address = self._socket.accept()
try:
data = c.recv(_read_buffer)
# capture request to backend
self._request = data
c.sendall(self._make_response(data))
# Half-close for an orderly FIN instead of letting the
# OS tear the socket down under us. An abrupt close()
# right after sendall() can, depending on the platform's
# TCP stack and buffering, surface to the peer as a
# reset rather than a clean end of the response, which
# httpd's proxy and cache code treat as an aborted
# connection and a reason to discard the cache entry.
try:
c.shutdown(socket.SHUT_WR)
except OSError:
pass
finally:
c.close()
except socket.timeout:
pass
except ConnectionAbortedError:
self._done = True
class ProxyTestSetup(HttpdTestSetup):
def __init__(self, env: 'HttpdTestEnv'):
super().__init__(env=env)
self.add_source_dir(os.path.dirname(inspect.getfile(ProxyTestSetup)))
self.add_modules(["proxy", "proxy_http", "proxy_ajp", "proxy_balancer",
"proxy_connect", "proxy_uwsgi", "lbmethod_byrequests",
"remoteip"])
class ProxyTestEnv(HttpdTestEnv):
def __init__(self, pytestconfig=None):
super().__init__(pytestconfig=pytestconfig)
self.add_httpd_conf([
])
self._d_reverse = f"reverse.{self.http_tld}"
self._d_forward = f"forward.{self.http_tld}"
self._d_mixed = f"mixed.{self.http_tld}"
self.add_httpd_log_modules(
["proxy", "proxy_http", "proxy_balancer", "lbmethod_byrequests",
"ssl"])
self.add_cert_specs([
CertificateSpec(domains=[
self._d_forward, self._d_reverse, self._d_mixed
]),
CertificateSpec(domains=[f"noh2.{self.http_tld}"],
key_type='rsa2048'),
])
def setup_httpd(self, setup: HttpdTestSetup = None):
super().setup_httpd(setup=ProxyTestSetup(env=self))
@property
def d_forward(self):
return self._d_forward
@property
def d_reverse(self):
return self._d_reverse
@property
def d_mixed(self):
return self._d_mixed