春江暮客

春江暮客的个人学习分享网站

Python HTTP JSON Requests: Timeouts, Bounded Retries, and Local Tests

2026-10-12 Technology
Python HTTP JSON Requests: Timeouts, Bounded Retries, and Local Tests

A scheduled script fetches a JSON endpoint, but an occasional 503 stops the batch. Unlimited retries can leave it waiting indefinitely. This tutorial builds a small GET client with Python’s standard library: a timeout, a bounded attempt count, and local success and failure tests.

The example targets single-process automation on Linux and also works on other systems with Python. It reads at most 1 MiB of UTF-8 JSON, requires no API key, and uses no third-party packages.

1. Decide which failures to retry

Use an explicit policy:

Result Action
HTTP 200 with parseable JSON Return the data.
502, 503, 504 Try up to the specified count; wait 0.5, 1, then 2 seconds, continuing to double with an 8-second cap.
Connection or read timeout Apply the same bounded retry policy.
Other HTTP status, invalid JSON, oversized response Fail immediately.
Retryable status with Retry-After Stop this job and let the caller defer it according to the service’s instructions.
DNS, certificate, or other connection error Fail immediately and inspect the configuration.

--attempts 3 includes the first request, so it permits at most two retries. This helper only makes GET requests. Applying the same logic to orders, transfers, or other writes can repeat an operation. See RFC 9110 section 9.2.2 for idempotent methods and automatic retries.

2. Prepare the scripts and start the local server

You need Python 3.10 or newer and curl. The examples were tested on Linux with Python 3.11.4; the commands work in bash or zsh. Use a new practice directory:

python3 --version
mkdir http-json-retry-demo
cd http-json-retry-demo
curl -fS https://www.bobobk.com/downloads/http-json-retry/get_json.py -o get_json.py
curl -fS https://www.bobobk.com/downloads/http-json-retry/demo_server.py -o demo_server.py
python3 get_json.py --help
python3 demo_server.py --port 8765

The server listens only on 127.0.0.1. Keep this terminal running, then enter the same http-json-retry-demo directory in a second terminal. Press Ctrl+C in the server terminal when you finish.

The client uses the system’s proxy configuration by default. If local test requests also go through a proxy, set this in the second terminal:

export no_proxy=127.0.0.1,localhost
export NO_PROXY=127.0.0.1,localhost

See Python’s ProxyHandler documentation for proxy bypass rules. These settings affect only this terminal and its child processes.

3. Complete client implementation

The download contains this code. You can also save it directly as get_json.py:

"""Fetch a small HTTP JSON document with bounded GET retries. Python 3.10+."""
import argparse
import json
import math
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import Request, urlopen

MAX_BYTES = 1024 * 1024
RETRY_STATUS = {502, 503, 504}


def get_json(url, *, timeout=3.0, attempts=3):
    parts = urlsplit(url)
    if parts.scheme not in {"http", "https"} or not parts.hostname:
        raise ValueError("URL must use http:// or https:// with a host")
    if not math.isfinite(timeout) or timeout <= 0:
        raise ValueError("timeout must be finite and greater than zero")
    if not isinstance(attempts, int) or not 1 <= attempts <= 10:
        raise ValueError("attempts must be between 1 and 10")
    for attempt in range(1, attempts + 1):
        request = Request(url, headers={"Accept": "application/json"}, method="GET")
        try:
            with urlopen(request, timeout=timeout) as response:
                if response.status != 200:
                    raise ValueError(f"expected HTTP 200, got {response.status}")
                body = response.read(MAX_BYTES + 1)
                if len(body) > MAX_BYTES:
                    raise ValueError("response exceeds 1 MiB")
                return json.loads(body.decode("utf-8"))
        except HTTPError as exc:
            status = exc.code
            retry_after = exc.headers.get("Retry-After")
            exc.close()
            if status not in RETRY_STATUS or attempt == attempts:
                raise
            if retry_after is not None:
                raise ValueError(f"HTTP {status} with Retry-After; defer this job") from exc
            reason = f"HTTP {status}"
        except URLError as exc:
            if not isinstance(exc.reason, TimeoutError) or attempt == attempts:
                raise
            reason = "timeout"
        except TimeoutError:
            if attempt == attempts:
                raise
            reason = "timeout"
        delay = min(0.5 * 2 ** (attempt - 1), 8.0)
        print(
            f"RETRY attempt={attempt}/{attempts} reason={reason} delay={delay:.1f}s",
            file=sys.stderr,
            flush=True,
        )
        time.sleep(delay)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("url")
    parser.add_argument("--timeout", type=float, default=3.0, help="blocking timeout in seconds")
    parser.add_argument("--attempts", type=int, default=3, help="total attempts, including first")
    args = parser.parse_args()
    try:
        result = get_json(args.url, timeout=args.timeout, attempts=args.attempts)
    except (OSError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(json.dumps(result, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

HTTPError is a subclass of URLError, so handle HTTP status before network errors. Close error responses too. Python’s urllib.error documentation describes this relationship.

Parsing errors stay outside the retry policy. If an endpoint returns an HTML error page, an empty body, or broken JSON, inspect the endpoint and response. Accept: application/json expresses a preference; it does not guarantee JSON.

4. Recover after two 503 responses

In the second terminal, run:

python3 get_json.py http://127.0.0.1:8765/flaky --timeout 1 --attempts 3

After the server starts, /flaky returns 503 for its first two requests, then succeeds. Retry messages go to standard error and JSON goes to standard output. The terminal usually shows:

RETRY attempt=1/3 reason=HTTP 503 delay=0.5s
RETRY attempt=2/3 reason=HTTP 503 delay=1.0s
{
  "ok": true,
  "items": 3
}

Requesting /flaky again succeeds immediately. Restart the test server to reset the counter before repeating this demonstration.

Check the expected data separately from JSON parsing:

python3 get_json.py http://127.0.0.1:8765/ok > result.json
python3 -c 'import json; data=json.load(open("result.json", encoding="utf-8")); assert data == {"ok": True, "items": 3}; print("PASS: expected data")'

This output belongs in the practice directory. Redirection overwrites it and may leave an empty file on failure. In a real job, check the exit status and data before replacing an existing result. For deployed web pages, see the curl deployment verification tutorial.

5. Verify that errors stop retrying

Run these commands one at a time. Each should exit with status 1. In bash or zsh, run echo $? immediately afterward to inspect the status. Keep this demonstration out of scripts that stop on the first error.

python3 get_json.py http://127.0.0.1:8765/missing
python3 get_json.py http://127.0.0.1:8765/bad
python3 get_json.py http://127.0.0.1:8765/always503 --attempts 3
python3 get_json.py http://127.0.0.1:8765/slow --timeout 0.2 --attempts 2
Path Expected result
/missing ERROR: HTTP Error 404: Not Found, with no RETRY.
/bad A JSON parsing error, with no RETRY.
/always503 Two RETRY messages, followed by an HTTP 503 error.
/slow One timeout RETRY, then exit after the second timeout.

Timeout wording can vary by Python version and operating system. Check the exit status, retry count, and expected data rather than relying on one error string.

6. Complete local test server

Save this as demo_server.py; it matches the download:

"""Local HTTP failure fixture; never expose this as a production service."""
import argparse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import threading
import time


class Handler(BaseHTTPRequestHandler):
    flaky_count = 0
    lock = threading.Lock()

    def do_GET(self):
        status, body = 200, b'{"ok": true, "items": 3}'
        if self.path == "/flaky":
            with Handler.lock:
                Handler.flaky_count += 1
                count = Handler.flaky_count
            if count <= 2:
                status, body = 503, b'{"error": "temporary"}'
        elif self.path == "/always503":
            status, body = 503, b'{"error": "unavailable"}'
        elif self.path == "/slow":
            time.sleep(2)
        elif self.path == "/bad":
            body = b'not json'
        elif self.path != "/ok":
            status, body = 404, b'{"error": "missing"}'
        try:
            self.send_response(status)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        except (BrokenPipeError, ConnectionResetError):
            pass  # The timeout demo can close the client socket first.

    def log_message(self, format, *args):
        pass


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--port", type=int, default=8765)
    args = parser.parse_args()
    with ThreadingHTTPServer(("127.0.0.1", args.port), Handler) as server:
        print(f"Serving http://127.0.0.1:{args.port}", flush=True)
        try:
            server.serve_forever()
        except KeyboardInterrupt:
            pass

/slow waits two seconds before responding. The threaded server allows a second request to start independently of the first wait. It ignores expected disconnect errors when a timed-out client closes its socket first.

Use this server only for local practice. Python’s http.server documentation explains the module’s scope and limitations.

7. Troubleshooting and boundaries

Problem Action
Address already in use Run python3 demo_server.py --port 8766 and change the client URL to port 8766.
Local connection fails Check that the server terminal is running, ports match, and the no_proxy settings above are applied.
401, 403, 404 Correct authentication, permissions, or the URL; this client does not keep trying.
429 or Retry-After Check the service’s rate limits and schedule the job for an allowed time; this example does not parse the header.
Response exceeds 1 MiB Use pagination or streaming downloads rather than removing the guard for arbitrary responses.
A timeout still leaves the job running for a long time Check for a slow trickle of data, DNS delays, proxy behavior, or time spent in system calls.

urlopen(timeout=...) applies to blocking operations; it is not a total deadline for the job. See Python’s timeout parameter documentation. Retry waits add more time. For a hard process limit, combine this with the subprocess timeout and logging helper.

The client follows redirects supported by urllib by default and is intended for trusted endpoints. Parseable JSON still needs field and type checks for your application. This example has no persistent cache, jitter, or concurrent rate limiter; multiple workers need coordinated limits based on the service’s rules.

For recurring jobs, send standard error to your task logs and set retention using the Python log rotation tutorial. Validation covered recovery, 404, invalid JSON, exhausted retries, timeouts, and parameter and response-size boundaries.

8. Summary

Separate temporary failures from configuration errors, then set the total attempt count and wait intervals. Test failure paths locally to confirm the script stops as intended. Check the actual JSON data before recording a request as successful.

标签

1024 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena copy cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print probability protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode usdt utf-8 uv vhhbert vite wallet webp wordcloud wordpress workflow yaml zipfile zoneinfo 迅雷解析

友情链接

其它