diff --git a/src/ResponseParser.cpp b/src/ResponseParser.cpp index bf7ac95..9560bb3 100644 --- a/src/ResponseParser.cpp +++ b/src/ResponseParser.cpp @@ -245,10 +245,12 @@ ResponseParser::Status ResponseParser::parse(Buffer* buf, int& pos) SString<64> bufHex; bufHex.printHex(buf->data() + pos, buf->length() - pos); SString<16> errHex; - errHex.printHex(cursor - 1, end - cursor + 1); + // Clamp errStart to the buffer head to avoid underflow on first-byte errors. + const char* errStart = cursor > buf->data() ? cursor - 1 : cursor; + errHex.printHex(errStart, end - errStart); logError("response parse error %d state %d buf:%s errpos %d err:%s", error, mState, bufHex.data(), - cursor - 1 - buf->data() - pos, errHex.data()); + (errStart - buf->data()) - pos, errHex.data()); return ParseError; } pos = cursor + 1 - buf->data(); diff --git a/test/response_parser_error_log.py b/test/response_parser_error_log.py new file mode 100644 index 0000000..f4c0dd8 --- /dev/null +++ b/test/response_parser_error_log.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# +# Start a temporary predixy instance with a fake backend that returns +# invalid responses to exercise response parser error logging safely. +# + +import os +import socket +import subprocess +import tempfile +import threading +import time + +from test_util import parse_args, exit_with_result + + +def wait_for_port(host, port, timeout=5.0): + deadline = time.time() + timeout + while time.time() < deadline: + try: + with socket.create_connection((host, port), timeout=0.5): + return True + except Exception: + time.sleep(0.05) + return False + + +def start_fake_backend(host="127.0.0.1"): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.bind((host, 0)) + server.listen(1) + port = server.getsockname()[1] + + def handler(): + try: + conn, _ = server.accept() + conn.recv(1024) + conn.sendall(b"!") + conn.close() + finally: + server.close() + + thread = threading.Thread(target=handler, daemon=True) + thread.start() + return port + + +def start_predixy(root, backend_port): + predixy_bin = os.path.join(root, "src", "predixy") + if not os.path.exists(predixy_bin): + raise RuntimeError("predixy binary not found") + + listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listen_sock.bind(("127.0.0.1", 0)) + listen_port = listen_sock.getsockname()[1] + listen_sock.close() + + tmp_dir = tempfile.TemporaryDirectory() + conf_path = os.path.join(tmp_dir.name, "predixy_test.conf") + with open(conf_path, "w") as f: + f.write( + "Name PredixyRespParserTest\n" + f"Bind 127.0.0.1:{listen_port}\n" + "WorkerThreads 1\n" + "ClientTimeout 3\n" + "LogVerbSample 0\n" + "LogDebugSample 0\n" + "LogInfoSample 10000\n" + "LogNoticeSample 1\n" + "LogWarnSample 1\n" + "LogErrorSample 1\n" + "\n" + "StandaloneServerPool {\n" + " RefreshMethod fixed\n" + " Group test {\n" + f" + 127.0.0.1:{backend_port}\n" + " }\n" + "}\n" + ) + + proc = subprocess.Popen([predixy_bin, conf_path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + if not wait_for_port("127.0.0.1", listen_port, timeout=5.0): + proc.terminate() + tmp_dir.cleanup() + raise RuntimeError("predixy did not start") + + return proc, listen_port, tmp_dir + + +def run_test(project_root): + backend_port = start_fake_backend() + proc, predixy_port, tmp_dir = start_predixy(project_root, backend_port) + try: + sock = socket.create_connection(("127.0.0.1", predixy_port), timeout=1.0) + sock.sendall(b"*1\r\n$4\r\nping\r\n") + try: + sock.recv(16) + except Exception: + pass + sock.close() + + time.sleep(0.2) + if proc.poll() is not None: + print("FAIL: predixy exited after invalid backend response") + return False + + try: + with socket.create_connection(("127.0.0.1", predixy_port), timeout=1.0): + pass + except Exception as exc: + print("FAIL: predixy not accepting connections:", exc) + return False + finally: + proc.terminate() + try: + proc.wait(timeout=2.0) + except Exception: + proc.kill() + tmp_dir.cleanup() + + return True + + +if __name__ == "__main__": + args = parse_args("Response parser error logging test") + root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + success = run_test(root) + exit_with_result(success, "response parser error logging", + "response parser error logging") diff --git a/test/run.sh b/test/run.sh index 9336250..4870d39 100755 --- a/test/run.sh +++ b/test/run.sh @@ -142,6 +142,7 @@ TESTS=( "test/null_response_handling.py" "test/request_parser_boundary.py" "test/request_parser_error_log.py" + "test/response_parser_error_log.py" "test/pubsub_long_name.py" "test/pubsub_large_message.py" "test/transaction_forbid.py"