Errors, retries and errbacks

Scrapy · Advanced · 12 min

Networks fail, servers return 500s and pages vanish, and a crawler must cope without silently losing data.

By default, responses with non-2xx status codes are dropped before reaching your callback by the HttpError spider middleware. To receive them anyway, set handle_httpstatus_list = [404] on the spider, meta={"handle_httpstatus_list": [404]} on a request, or HTTPERROR_ALLOWED_CODES globally.

The RetryMiddleware retries connection errors, timeouts and the codes in RETRY_HTTP_CODES (500, 502, 503, 504, 522, 524, 408, 429) up to RETRY_TIMES (default 2) extra times. Adjust per request with meta={"max_retry_times": 5} or disable with dont_retry. To retry from your own code (an error page served with status 200), use get_retry_request(response.request, spider=self, reason="empty page"), which respects the same limits and stats.

An errback is a function called when a request fails for good: scrapy.Request(url, callback=..., errback=self.on_error). It receives a Twisted Failure:

  • failure.check(HttpError), DNSLookupError, TimeoutError identify the cause.
  • failure.request is the failed request; for HttpError, failure.value.response holds the response.

Use errbacks to log, record failed URLs as items for later reprocessing, or yield alternative requests.

At the end of a crawl, check the stats (downloader/response_status_count/404, retry/max_reached, log_count/ERROR). A crawl that "finished" with thousands of errors is a failure; alert on these numbers.

Example

import scrapy
from scrapy.downloadermiddlewares.retry import get_retry_request
from scrapy.spidermiddlewares.httperror import HttpError
from twisted.internet.error import DNSLookupError, TimeoutError

class RobustSpider(scrapy.Spider):
    name = "robust"
    handle_httpstatus_list = [404]
    custom_settings = {"RETRY_TIMES": 3}
    async def start(self):
        for url in ["https://quotes.toscrape.com/", "https://quotes.toscrape.com/nope"]:
            yield scrapy.Request(url, callback=self.parse, errback=self.on_error)
    def parse(self, response):
        if response.status == 404:
            yield {"url": response.url, "status": "missing"}; return
        if not response.css("div.quote"):
            retry = get_retry_request(response.request, spider=self, reason="empty")
            if retry: yield retry
            return
        yield {"url": response.url, "quotes": len(response.css("div.quote"))}
    def on_error(self, failure):
        if failure.check(HttpError):
            self.logger.error("HTTP %s on %s", failure.value.response.status, failure.request.url)
        elif failure.check(DNSLookupError, TimeoutError):
            self.logger.error("Network error on %s", failure.request.url)

Remember

  • Non-2xx responses are filtered unless allowed via handle_httpstatus_list
  • RetryMiddleware retries network errors and 5xx/429; tune RETRY_TIMES
  • get_retry_request retries from callbacks with the same limits
  • errback receives a Failure; check its type and log or record it

Watch out for

  • Expecting a 404 page in the callback without handle_httpstatus_list, and never seeing it.

Check yourself

  1. Your callback never sees 404 responses. Why?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scrapy.org/en/latest/topics/request-response.html#errbacks