diff --git a/scrapy/pipelines/files.py b/scrapy/pipelines/files.py index 84d4104ed..8e457a4ca 100644 --- a/scrapy/pipelines/files.py +++ b/scrapy/pipelines/files.py @@ -704,6 +704,10 @@ class FilesPipeline(MediaPipeline): self, item: Any, info: MediaPipeline.SpiderInfo ) -> list[Request]: urls = ItemAdapter(item).get(self.files_urls_field, []) + if not isinstance(urls, list): + raise TypeError( + f"{self.files_urls_field} must be a list of URLs, got {type(urls).__name__}. " + ) return [Request(u, callback=NO_CALLBACK) for u in urls] def file_downloaded( diff --git a/scrapy/pipelines/images.py b/scrapy/pipelines/images.py index 19139b5d5..450311e18 100644 --- a/scrapy/pipelines/images.py +++ b/scrapy/pipelines/images.py @@ -241,6 +241,10 @@ class ImagesPipeline(FilesPipeline): self, item: Any, info: MediaPipeline.SpiderInfo ) -> list[Request]: urls = ItemAdapter(item).get(self.images_urls_field, []) + if not isinstance(urls, list): + raise TypeError( + f"{self.images_urls_field} must be a list of URLs, got {type(urls).__name__}. " + ) return [Request(u, callback=NO_CALLBACK) for u in urls] def item_completed( diff --git a/tests/test_pipeline_files.py b/tests/test_pipeline_files.py index 38d3b8ce5..424236401 100644 --- a/tests/test_pipeline_files.py +++ b/tests/test_pipeline_files.py @@ -261,6 +261,26 @@ class TestFilesPipeline: request = Request("http://example.com") assert file_path(request, item=item) == "full/path-to-store-file" + @pytest.mark.parametrize( + "bad_type", + [ + "http://example.com/file.pdf", + ("http://example.com/file.pdf",), + {"url": "http://example.com/file.pdf"}, + 123, + None, + ], + ) + def test_rejects_non_list_file_urls(self, tmp_path, bad_type): + pipeline = FilesPipeline.from_crawler( + get_crawler(None, {"FILES_STORE": str(tmp_path)}) + ) + item = ItemWithFiles() + item["file_urls"] = bad_type + + with pytest.raises(TypeError, match="file_urls must be a list of URLs"): + list(pipeline.get_media_requests(item, None)) + class TestFilesPipelineFieldsMixin(ABC): @property diff --git a/tests/test_pipeline_images.py b/tests/test_pipeline_images.py index 303b9596c..3acb9bf7f 100644 --- a/tests/test_pipeline_images.py +++ b/tests/test_pipeline_images.py @@ -209,6 +209,26 @@ class TestImagesPipeline: assert converted.mode == "RGB" assert converted.getcolors() == [(10000, (205, 230, 255))] + @pytest.mark.parametrize( + "bad_type", + [ + "http://example.com/file.jpg", + ("http://example.com/file.jpg",), + {"url": "http://example.com/file.jpg"}, + 123, + None, + ], + ) + def test_rejects_non_list_image_urls(self, tmp_path, bad_type): + pipeline = ImagesPipeline.from_crawler( + get_crawler(None, {"IMAGES_STORE": str(tmp_path)}) + ) + item = ImagesPipelineTestItem() + item["image_urls"] = bad_type + + with pytest.raises(TypeError, match="image_urls must be a list of URLs"): + list(pipeline.get_media_requests(item, None)) + class TestImagesPipelineFieldsMixin(ABC): @property