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.
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/en/python-http-json-timeout-retry.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
相关文章
- Troubleshoot Linux Scheduled Jobs: Python Paths, Working Directories, and Environment
- Rotate Python Logs by Size with RotatingFileHandler
- Save JSON Files Safely with Atomic Writes in Python
- Ollama Structured Outputs: Turn Notes into Validated JSON with Python
- Python SIGTERM Shutdown: Finish the Current Record Before Stopping