Python urllib.error: Handle HTTP Failures

Published on: August 21, 2026
Reading time: 5 minutes
Error code over binary data representing failures handled with Python urllib.error

The urllib.error module contains the exceptions used by urllib.request when a network operation fails. Distinguishing an HTTP error from DNS failure, timeout, certificate verification, connection refusal, or an incomplete download helps you build reliable clients, useful logs, and retry policies that do not amplify incidents.

This guide focuses on exception handling in the standard-library client. To create requests, headers, proxy handlers, and bounded downloads, read Python urllib.request. To split and validate URL components, see Python urllib.parse.

The basic hierarchy

URLError is the base exception and inherits from OSError. HTTPError inherits from URLError. Consequently, the order of your except blocks matters: catch HTTPError before URLError, otherwise the broader handler hides the specific one.

from urllib.error import HTTPError, URLError
from urllib.request import urlopen

try:
    with urlopen("https://example.com/resource", timeout=10) as response:
        data = response.read(100_000)
except HTTPError as error:
    print("HTTP status:", error.code)
except URLError as error:
    print("Transport failure:", error.reason)

A 404 or 500 status means that a valid HTTP response arrived, even though it reports failure. A plain URLError can represent name resolution, connection refusal, timeout, TLS, or another step before an HTTP response exists.

Understanding URLError

The reason attribute may be a string or another exception. Avoid making operational decisions by comparing message text alone. Inspect the underlying type when the distinction matters.

import socket
from urllib.error import URLError
from urllib.request import urlopen

try:
    urlopen("https://example.invalid", timeout=5)
except URLError as error:
    if isinstance(error.reason, socket.timeout):
        print("The connection timed out")
    else:
        print(type(error.reason).__name__, error.reason)

Depending on the platform and call path, a timeout may appear as TimeoutError, socket.timeout, or a nested exception. Tests should model the deployment environment instead of turning unstable error strings into a strict API.

HTTPError is both exception and response

An HTTPError exposes url, code, reason, headers, and a file-like object in fp. The error itself can also be read like the response. This is useful when a server sends a JSON or text explanation.

import json
from urllib.error import HTTPError
from urllib.request import Request, urlopen

request = Request("https://api.example.com/items/999")

try:
    with urlopen(request, timeout=10) as response:
        payload = json.load(response)
except HTTPError as error:
    body = error.read(64_000)
    media_type = error.headers.get_content_type()
    if media_type == "application/json":
        detail = json.loads(body.decode("utf-8"))
    else:
        detail = body.decode("utf-8", errors="replace")
    print(error.code, detail)

Always bound the error body. An untrusted remote server can return megabytes or keep the connection busy. An error status does not make the body safe for logs or HTML output.

Classify status codes

A practical policy often groups responses:

  • 400–499 usually indicate a request, authentication, authorization, or resource issue.
  • 500–599 often represent a temporary or internal server failure.
  • 429 requests lower traffic and may include Retry-After.
  • 401 and 403 should not trigger endless retries with the same credentials.

Do not retry every status automatically. Repeating a non-idempotent POST can duplicate orders, charges, messages, or database records.

Selective retries with backoff

import time
from urllib.error import HTTPError, URLError
from urllib.request import urlopen

RETRYABLE = {429, 500, 502, 503, 504}

def download(url: str, attempts: int = 3) -> bytes:
    for index in range(attempts):
        try:
            with urlopen(url, timeout=10) as response:
                return response.read(1_000_000)
        except HTTPError as error:
            if error.code not in RETRYABLE or index == attempts - 1:
                raise
        except URLError:
            if index == attempts - 1:
                raise
        time.sleep(2 ** index)
    raise RuntimeError("unreachable")

Production code should add random jitter so many clients do not retry at the same instant. Respect Retry-After when appropriate and set a total time budget. A series of attempts should not each consume the full user-facing deadline.

A timeout is not a complete limit

The timeout argument limits blocking operations, but robust clients also bound body size, redirect count, and overall duration. Reading until end-of-file without a size limit can take a long time after a connection has succeeded.

When a user controls the destination URL, add SSRF protections: allowed schemes and ports, DNS and IP checks, redirect validation, and strict response limits. Exception handling does not validate the destination.

TLS failures

Certificate problems commonly arrive as a URLError whose reason is an SSL exception. Do not fix them by disabling verification.

import ssl
from urllib.error import URLError

try:
    # HTTPS call
    pass
except URLError as error:
    if isinstance(error.reason, ssl.SSLCertVerificationError):
        print("The certificate could not be verified")
        raise

Correct the clock, trust chain, hostname, or CA bundle. The guide to Python ssl explains secure contexts and certificate validation.

ContentTooShortError

ContentTooShortError is associated with urlretrieve() when the downloaded content is shorter than the expected Content-Length. Its content attribute preserves the received data, but that data must be treated as incomplete.

from urllib.error import ContentTooShortError
from urllib.request import urlretrieve

try:
    path, headers = urlretrieve(
        "https://example.com/archive.zip",
        "archive.zip",
    )
except ContentTooShortError as error:
    print("Truncated download:", len(error.content))
    raise

Do not process a partial archive silently. Remove or quarantine the temporary target, retry according to policy, and verify a hash or signature when one is available. For secure temporary locations, use Python tempfile.

Error bodies with unknown encoding

A server can declare a charset, omit it, or provide incorrect metadata. For diagnostics, use the declared charset when suitable and a replacement fallback. Strict UTF-8 decoding inside an error handler may raise a second exception and hide the original failure.

def read_error(error: HTTPError, limit: int = 64_000) -> str:
    data = error.read(limit)
    charset = error.headers.get_content_charset() or "utf-8"
    return data.decode(charset, errors="replace")

Logging without leaking secrets

Record the method, host, normalized path, status, duration, and a correlation identifier. Do not log bearer tokens, Authorization headers, cookies, URL passwords, or complete bodies containing personal data.

The url attribute may include a sensitive query string. Redact known parameters before logging. Also bound the text from reason, because externally influenced content can reach operational systems.

Expected failures versus programming bugs

Catch only exceptions you can handle. An except Exception around an entire workflow can turn a programming defect into a misleading network warning. Keep the try region narrow.

try:
    response = urlopen(request, timeout=10)
except (HTTPError, URLError) as error:
    handle_network_failure(error)
else:
    with response:
        process(response)

Errors from process() are no longer mislabeled as transport failures.

Command-line and library APIs

A command-line program can convert known failures into concise messages and stable exit codes, while preserving details in verbose mode. A reusable library should usually propagate the original exception or wrap it with raise DomainError(...) from error so callers retain the cause.

Testing without the public internet

Run a local HTTP server that deliberately returns 404, 429, 500, redirects, slow responses, and truncated bodies. The guide to Python socketserver can support controlled integration tests. Mocks are fast, but a local protocol test reveals framing and body-handling mistakes.

Test handler order, bounded reads, retry only for eligible operations, preservation of exception causes, and redaction of secrets.

Common mistakes

Frequent mistakes include catching URLError before HTTPError, retrying every response, disabling TLS verification, reading unlimited bodies, comparing unstable error messages, logging credentials, accepting truncated files, retrying POST without idempotency protection, and catching exceptions too broadly.

Conclusion

urllib.error separates HTTP responses, transport failures, and incomplete downloads. That distinction improves user messages, metrics, and recovery decisions. Use HTTPError to inspect status, headers, and body; inspect URLError.reason for the underlying transport cause; and treat ContentTooShortError data as untrusted and incomplete.

Read the official urllib.error documentation and HTTP Semantics. A safe client combines timeouts, limits, destination validation, selective retries, and secret-free logging.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    HTML keycaps representing HTML entities with Python html.entities
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    html.entities: Convert HTML Entities

    Learn Python html.entities to inspect HTML entities, convert names and code points, and avoid confusing decoding with sanitization.

    Ler mais

    Tempo de leitura: 6 minutos
    21/08/2026
    Folder with files representing MIME types identified with Python mimetypes
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    mimetypes: Detect MIME Types for Files

    Learn Python mimetypes to identify file types, validate uploads, and set safer HTTP Content-Type headers.

    Ler mais

    Tempo de leitura: 6 minutos
    20/08/2026
    HTML code on a screen representing parsing with Python html.parser
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python html.parser: Parse HTML

    Learn Python html.parser to extract text, links, and metadata, process HTML incrementally, and avoid confusing parsing with sanitization.

    Ler mais

    Tempo de leitura: 5 minutos
    20/08/2026
    Server rack representing low-level HTTP connections with Python http.client
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python http.client: Low-Level HTTP

    Learn Python http.client for low-level HTTP and HTTPS connections, streaming, headers, TLS, connection reuse, size limits, and errors.

    Ler mais

    Tempo de leitura: 4 minutos
    20/08/2026
    HTML code on a screen representing responsible crawling with Python robotparser
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python robotparser: Read robots.txt

    Learn Python urllib.robotparser to respect robots.txt, crawl-delay, request-rate, sitemaps, caching, and responsible crawling limits.

    Ler mais

    Tempo de leitura: 4 minutos
    20/08/2026
    Keyboard tiles spelling HTTP representing Python urllib.request requests
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python urllib.request: Native HTTP

    Learn Python urllib.request for GET, POST, JSON, downloads, TLS, redirects, proxies, size limits, and robust HTTP error handling.

    Ler mais

    Tempo de leitura: 4 minutos
    19/08/2026