From 31d69cc0e3ed1904fbd7ca7f6f42b90275c38efa Mon Sep 17 00:00:00 2001 From: Sriniketh24 Date: Thu, 25 Jun 2026 15:32:36 +0530 Subject: [PATCH] Fix FTPDownloadHandler not closing connection after download download_request() created an FTPClient but never called client.quit() to close the control connection. On the CommandFailed path, protocol.close() was also missing, which could leave an open file handle for local-filename downloads. Refactored to try/except/else/finally so protocol.close() is called in both success and CommandFailed paths, and client.quit() is always awaited in the finally block, with its own exception suppressed to avoid masking the original error. Fixes #7602 Co-authored-by: Claude --- scrapy/core/downloader/handlers/ftp.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scrapy/core/downloader/handlers/ftp.py b/scrapy/core/downloader/handlers/ftp.py index 6258067c1..6c075152b 100644 --- a/scrapy/core/downloader/handlers/ftp.py +++ b/scrapy/core/downloader/handlers/ftp.py @@ -113,15 +113,22 @@ class FTPDownloadHandler(BaseDownloadHandler): try: await maybe_deferred_to_future(client.retrieveFile(filepath, protocol)) except CommandFailed as e: + protocol.close() message = str(e) if m := _CODE_RE.search(message): ftpcode = m.group() httpcode = self.CODE_MAPPING.get(ftpcode, self.CODE_MAPPING["default"]) return Response(url=request.url, status=httpcode, body=message.encode()) raise - protocol.close() - headers = {"local filename": protocol.filename or b"", "size": protocol.size} - body = protocol.filename or protocol.body.read() - respcls = responsetypes.from_args(url=request.url, body=body) - # hints for Headers-related types may need to be fixed to not use AnyStr - return respcls(url=request.url, status=200, body=body, headers=headers) # type: ignore[arg-type] + else: + protocol.close() + headers = {"local filename": protocol.filename or b"", "size": protocol.size} + body = protocol.filename or protocol.body.read() + respcls = responsetypes.from_args(url=request.url, body=body) + # hints for Headers-related types may need to be fixed to not use AnyStr + return respcls(url=request.url, status=200, body=body, headers=headers) # type: ignore[arg-type] + finally: + try: + await maybe_deferred_to_future(client.quit()) + except Exception: + pass