← Lập trình Python nâng cao

Bài 11 · Nâng cao · 24 phút· Cập nhật 11/06/2026

Internet & HTTP

Biên soạn bởi Nguyễn Anh Tuấn

Gọi và dựng API trong Python: HTTP request/response, REST & JSON, mã trạng thái, thư viện requests/httpx - tới FastAPI đầu tiên của bạn.

Giao tiếp web theo mô hình request → response: bạn gửi yêu cầu (method + path + headers + body), server trả về mã trạng thái + dữ liệu. Gửi thử vài request:

Gửi thử một request tới API tasks (giả lập):

→ request
GET /tasks
← response
200 OK · 2xx · Thành công
[ { "id": 1, "tieu_de": "Hoc HTTP" } ]
2xxThành công (200 OK, 201 Created, 204 No Content)
3xxChuyển hướng (301 Moved, 304 Not Modified)
4xxLỗi phía CLIENT (400 Bad Request, 401/403, 404 Not Found)
5xxLỗi phía SERVER (500 Internal Error, 503 Unavailable)
  • Request: METHOD + PATH + HEADERS (vd Authorization) + đôi khi BODY.
  • Response: MÃ TRẠNG THÁI (200/404…) + headers + body (thường JSON).
  • Mỗi request độc lập (stateless): đăng nhập thường gửi kèm token ở header mỗi lần.

requests là thư viện gọi HTTP phổ biến nhất. Khi gọi NHIỀU lần tới một API, dùng Session để tái dùng kết nối & đặt header mặc định:

goi_api.py

# Cai: uv add requests
import requests

s = requests.Session()                    # tai dung ket noi + header mac dinh
s.headers["Authorization"] = "Bearer <token>"

# jsonplaceholder: API gia-lap MIEN PHI, chay that - /todos = danh sach "viec can lam"
r = s.get("https://jsonplaceholder.typicode.com/todos", timeout=10)
r.raise_for_status()                      # 4xx/5xx -> nem ngoai le
data = r.json()                           # body JSON -> list/dict Python
print(r.status_code, len(data))           # -> 200 200
  • requests.get(url, timeout=...) → response; r.json() đổi body JSON thành dict/list.
  • Session: tái dùng kết nối TCP (nhanh hơn) + header/auth mặc định cho mọi request.
  • LUÔN đặt timeout (requests không có mặc định!) và gọi raise_for_status().

API để tập gọi thật

Các ví dụ dưới dùng jsonplaceholder.typicode.com - một API JSON miễn phí, không cần đăng nhập, dữ liệu giả nhưng chạy thật. /todos (việc cần làm), /posts, /users… Bạn copy & chạy được ngay để theo dõi. (POST/PUT/DELETE chỉ giả lập: trả mã & dữ liệu đúng nhưng không lưu thật.)

requests hay httpx?

requests đủ cho phần lớn việc. httpx mới hơn, hỗ trợ HTTP/2 và có async (await client.get(...)) - hợp khi gọi nhiều API đồng thời (bài async/await sẽ dùng).

status.py

r = s.get(url, timeout=10)

if r.status_code == 200:
    dung(r.json())
elif r.status_code == 404:
    print("Khong tim thay")
elif r.status_code >= 500:
    print("Loi phia server - co the retry + backoff")
else:
    r.raise_for_status()
  • 2xx thành công · 3xx chuyển hướng · 4xx lỗi CLIENT · 5xx lỗi SERVER.
  • 4xx = "lỗi của bạn" (sai dữ liệu/chưa đăng nhập); 5xx = "lỗi của họ" (nên retry có backoff).
  • raise_for_status() để không lỡ xử lý dữ liệu lỗi như thành công.

post.py

r = s.post(
    "https://jsonplaceholder.typicode.com/todos",
    json={"title": "Hoc HTTP"},            # tu dat Content-Type: application/json
    headers={"Idempotency-Key": "abc-123"},# vd: chong tao trung khi retry
    timeout=10,
)
r.raise_for_status()
print(r.status_code, r.json())             # vi du: 201 {'title': 'Hoc HTTP', 'id': 201}
  • json=... gửi body JSON & tự đặt header Content-Type đúng.
  • POST tạo mới (201); PUT/PATCH cập nhật; DELETE xoá (204).
  • POST không idempotent → cân nhắc Idempotency-Key khi có thể retry (nhắc bài chuẩn nghề).

Mạng sẽ lỗi và dữ liệu thường chia trang. Hai mẫu thực chiến: retry có backoff cho lỗi tạm thời, và generator duyệt hết các trang:

resilience.py - retry + pagination

import time, requests

def get_retry(s, url, lan=3):
    loi = None
    for i in range(lan):
        try:
            r = s.get(url, timeout=10)
            if r.status_code < 500:        # 5xx -> thu lai; con lai tra ve
                return r
            loi = None                     # nhan duoc phan hoi (5xx)
        except requests.RequestException as e:
            loi = e                        # loi mang -> ghi nho de bao lai
        time.sleep(2 ** i)                 # backoff: 1s, 2s, 4s...
    if loi is not None:
        raise loi                          # mang dut ca 3 lan
    r.raise_for_status()                   # het luot voi 5xx -> nem loi HTTP

def duyet_tat_ca(s, base):                 # base = ".../todos"
    page = 1
    while True:                            # phan trang: xin tung trang
        # ten tham so tuy API; jsonplaceholder dung _page & _limit
        items = s.get(f"{base}?_page={page}&_limit=20", timeout=10).json()
        if not items:                      # trang rong -> het
            return
        yield from items                   # tra tung item ra ngoai (luoi)
        page += 1
  • Retry CHỈ cho lỗi tạm thời (5xx/timeout/mất mạng), backoff giãn dần, có giới hạn.
  • Thao tác retry nên idempotent (GET an toàn; POST cần Idempotency-Key).
  • Phân trang gói thành generator (yield từng item) → duyệt lười, tự dừng khi hết, ít RAM.

Không chỉ GỌI - bạn cũng dựng API được. FastAPI rất gọn:

app.py - chạy: uv run --with fastapi --with uvicorn uvicorn app:app

from fastapi import FastAPI
app = FastAPI()

@app.get("/tasks")                       # GET /tasks -> 200
def liet_ke():
    return [{"id": 1, "tieu_de": "Hoc HTTP"}]

@app.post("/tasks", status_code=201)      # POST /tasks -> 201
def tao(task: dict):
    return {"id": 2, **task}
  • Mỗi route = method + path + hàm trả dữ liệu (FastAPI tự đổi sang JSON).
  • FastAPI dùng type hint (bài type-hints!) để kiểm dữ liệu vào & sinh tài liệu API tự động.
  • REST: map tài nguyên (tasks) với method (GET/POST/PUT/DELETE) + status hợp lý.

Hết phần “thế giới thật”

Bạn đã làm việc với tệp, cơ sở dữ liệumạng (kể cả chịu lỗi & phân trang). Ngay sau đây là phần khó nhất của khoá - mạch đồng thời (GIL, đa luồng/tiến trình, free-threading) - rồi khép lại bằng async/await - phục vụ hàng nghìn tác vụ I/O trên một luồng.

Câu hỏi thường gặp

Theo mô hình REQUEST → RESPONSE: client gửi request gồm METHOD (GET/POST/…), PATH (/tasks/1), HEADERS (metadata như Authorization, Content-Type) và đôi khi BODY; server trả RESPONSE gồm MÃ TRẠNG THÁI (200, 404…), headers, và body (thường JSON). Mỗi request độc lập (stateless).

Cả hai là thư viện gọi HTTP với API gần giống nhau (requests.get(...), r.json()). httpx mới hơn, hỗ trợ HTTP/2 và quan trọng là CÓ async (await client.get(...)) - hợp khi gọi nhiều API đồng thời (bài async). Người mới dùng requests là đủ.

requests.Session() TÁI DÙNG kết nối TCP (connection pooling) → nhanh hơn rõ khi gọi nhiều request tới cùng host; và cho đặt headers/auth MẶC ĐỊNH một lần cho mọi request. Gọi lẻ requests.get mỗi lần mở kết nối mới - phí. Dùng Session khi gọi cùng một API nhiều lần.

timeout= ngăn chương trình TREO vô hạn khi mạng/đối tác chậm (requests KHÔNG có timeout mặc định!). r.raise_for_status() biến status 4xx/5xx thành ngoại lệ để bạn không vô tình xử lý dữ liệu lỗi như thành công. Đây là hai thói quen "chịu lỗi" tối thiểu khi gọi mạng.

Chỉ retry lỗi TẠM THỜI (timeout, mất mạng, 5xx, 429) - KHÔNG retry 4xx do dữ liệu sai. Dùng exponential backoff + jitter (giãn dần + ngẫu nhiên) để khỏi "bão retry"; giới hạn số lần. Thao tác được retry nên IDEMPOTENT. (Thư viện như tenacity, hoặc urllib3 Retry gắn vào Session, lo việc này cho bạn.)

API trả dữ liệu lớn theo TỪNG TRANG (vd ?page=1, hoặc trả link "next"). Để lấy hết, lặp xin trang kế tiếp tới khi hết. Gói thành GENERATOR (yield từng item/trang) để gọi bên ngoài duyệt lười - không nạp tất cả vào RAM, và tự dừng khi hết trang.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Một trao đổi HTTP theo mô hình nào?

  1. 1

    Đọc một response

    Dùng widget Bước 1: gửi "Tạo (chưa đăng nhập)" và "Tạo (đã đăng nhập)". Hai mã trạng thái là gì, họ nào, nghĩa gì?

    Hoàn thành khi: 401 Unauthorized (4xx - chưa đăng nhập) và 201 Created (2xx - tạo thành công).

  2. 2

    Gọi API + Session

    Cài requests (uv add requests). Tạo một Session đặt header mặc định; gọi GET https://jsonplaceholder.typicode.com/todos/1; in status_code và một trường r.json().

    Hoàn thành khi: Dùng Session (tái dùng kết nối + header); lấy được dữ liệu JSON; đọc status_code.

  3. 3

    Chịu lỗi

    Thêm timeout=r.raise_for_status(); viết một vòng retry có backoff cho lỗi tạm thời (5xx/timeout).

    Hoàn thành khi: Lời gọi có timeout; status lỗi ném ngoại lệ; retry chỉ cho lỗi tạm thời, giãn dần, có giới hạn.

  4. 4

    Phân trang bằng generator

    Viết generator duyet_tat_ca() lặp xin từng trang (vd /todos?_page=1,2,…&_limit=20) và yield từng item, dừng khi trang rỗng.

    Hoàn thành khi: Bên ngoài chỉ for item in duyet_tat_ca(); không nạp hết vào RAM; tự dừng đúng lúc.

  5. 5

    Phác một API

    Thiết kế 3 route cho "tasks": liệt kê, tạo, xoá - với method & mã trạng thái phù hợp.

    Hoàn thành khi: GET /tasks → 200; POST /tasks → 201; DELETE /tasks/{id} → 204. Hợp REST.