Модуль 11 · Урок 42 із 58

HTTP client у Python: URL, timeout, errors і JSON

Надійний HTTP client не починається з одного urlopen. Він визначає дозволені endpoints, кодує URL, ставить timeout, відрізняє HTTP response від transport failure і перевіряє representation до того, як довірити дані application code.

urllibTimeoutErrorsJSON

Безпечна HTTP-практика модуля 11

Пакет містить synthetic fixtures, loopback-only HTTP server, bounded urllib client, Problem Details і contract tests. Тести виконують реальні request/response лише на 127.0.0.1 без Internet, персональних чи production-даних, credentials і production endpoints.

Завантажити практичний пакет →

URL збирають компонентами

User input не вставляють у URL через concatenation. Path segment percent-encode окремо, query створюють через urlencode(), а результат перевіряють через urlsplit(). Важливо: parser розбирає структуру, але сам по собі не доводить, що URL безпечний.

from urllib.parse import urlencode, urlsplit

query = urlencode({"status": "open", "tag": "HTTP basics"})
url = f"http://127.0.0.1:8080/tasks?{query}"
parts = urlsplit(url)
if parts.hostname not in {"127.0.0.1", "localhost"}:
    raise ValueError("endpoint_not_allowed")

У реальному service allowlist може включати лише configured origin, scheme і port. Redirect target перевіряють повторно, інакше початково дозволений URL може привести до внутрішньої адреси.

Request фіксує intent і metadata

urllib.request.Request приймає URL, bytes content, headers і method. JSON серіалізують детерміновано, кодують UTF-8 та позначають Content-Type. Authentication secret надходить зі secret store й ніколи не потрапляє до lab, URL query, exception message або debug log.

import json
from urllib.request import Request

payload = json.dumps({"title": "Synthetic task"}).encode("utf-8")
request = Request(
    url,
    data=payload,
    method="POST",
    headers={"Content-Type": "application/json", "Accept": "application/json"},
)

Timeout завжди явний

Без timeout worker може чекати довше за свій request budget. Один numeric timeout у urlopen не моделює всі production phases так гнучко, як спеціалізований client, але він кращий за необмежене очікування. Загальний budget має враховувати DNS, connect, TLS, response headers, body і retries.

Retry — policy, а не except: continue

Повторюйте лише визначені transient failures, у межах малого budget, із backoff та jitter. POST без idempotency contract не можна автоматично повторювати після невідомого результату.

HTTPError теж містить response

urllib.error.HTTPError виникає для HTTP non-success і водночас має code, headers та body. URLError описує transport-level failure. Їх не варто зливати в один except Exception: 404, timeout і malformed JSON потребують різних рішень та observability.

HTTPStatus + bounded problem body.
TransportReason без витоку credentials.
ParseMedia type, charset і JSON shape.
DomainRequired fields та type checks.

Response перевіряють до використання

Client обмежує body size, перевіряє Content-Type, декодує bytes і валідовує JSON structure. Навіть correct JSON є недовіреним input: потрібні expected object/list shape, required fields, numeric/string boundaries і policy для unknown fields.

  1. Закріпіть allowed origin і redirect policy.
  2. Складіть URL без string injection.
  3. Додайте explicit timeout та bounded body.
  4. Розділіть HTTP, transport, parse і domain failures.
  5. Логуйте operation, status, duration і request ID без secrets/content.

Методичні джерела

Урок, сценарії, пояснення й вправи створені SEOWORK. Посилання ведуть на стандарти та авторитетну офіційну документацію.

Практична перевірка · урок 42 з 47

Закріпіть матеріал уроку

Три сценарні питання. Для зарахування уроку потрібно дати щонайменше дві правильні відповіді.

1. Потрібно передати query tag='HTTP basics'. Чому urlencode кращий за ручне f'?tag={tag}'?
2. Code викликає urlsplit(user_url), а потім вважає URL безпечним лише тому, що parsing не впав. Яка проблема лишається?
3. Worker має загальний budget 3 секунди, але urlopen викликається без timeout. Який ризик найпряміший?