Query string là cổng vào đầu tiên của API: ?page=abc, ?per_page=10000 hay ?sort=ten_cot_nhay_cam đều có thể làm API lỗi 500, trả quá nhiều dữ liệu hoặc sắp xếp theo cột không mong muốn. Cách gọn nhất là validate ngay khi đọc tham số bằng request.args.get(type=...), kèm giá trị mặc định, giới hạn khoảng hợp lệ và whitelist cho trường sắp xếp — sai thì trả 400 kèm thông báo rõ ràng. Cách này áp dụng cho mọi endpoint danh sách có phân trang, tìm kiếm hoặc sắp xếp.
Vấn đề
Nhiều API viết theo kiểu lấy thẳng tham số rồi ép kiểu bằng int(): khi client gửi page=abc, int() ném ValueError và Flask trả lỗi 500 thay vì 400. Tệ hơn, nếu lấy sort rồi đưa thẳng vào câu lệnh sắp xếp, client có thể yêu cầu sắp xếp theo cột nội bộ hoặc gây lỗi truy vấn. Lỗi này khó phát hiện vì với tham số đúng, mọi thứ vẫn chạy tốt — chỉ khi client gửi sai, API mới “nổ”.
@app.get("/products")def list_products_bad(): page = int(request.args["page"]) # KeyError nếu thiếu, ValueError nếu sai sort = request.args.get("sort", "name") items = sorted(PRODUCTS, key=lambda p: p[sort]) # KeyError nếu sort lạ return {"items": items[(page - 1) * 10:page * 10]}
Giải pháp
request.args là một MultiDict của Werkzeug, và get() của nó nhận tham số type để ép kiểu. Nhưng có một cạm bẫy: khi ép kiểu thất bại, nó lặng lẽ trả về giá trị mặc định thay vì báo lỗi — page=abc sẽ âm thầm thành page=1. Vì vậy, trước khi lấy giá trị, hãy kiểm tra: nếu tham số có mặt mà ép kiểu ra None thì đó là giá trị sai, trả 400 ngay. Sau đó mới kiểm tra khoảng hợp lệ và whitelist cho sort.
from flask import Flask, abort, requestapp = Flask(__name__)PRODUCTS = [ {"name": "Bàn phím", "price": 850000}, {"name": "Chuột", "price": 450000}, {"name": "Tai nghe", "price": 1200000},]ALLOWED_SORTS = {"name", "price"}def parse_list_params(): for name in ("page", "per_page"): if name in request.args and request.args.get(name, type=int) is None: abort(400, f"{name} phải là số nguyên") page = request.args.get("page", 1, type=int) per_page = request.args.get("per_page", 10, type=int) sort = request.args.get("sort", "name") if page < 1 or not 1 <= per_page <= 100: abort(400, "page >= 1 và 1 <= per_page <= 100") if sort not in ALLOWED_SORTS: abort(400, f"sort chỉ nhận: {', '.join(sorted(ALLOWED_SORTS))}") return page, per_page, sort@app.get("/products")def list_products(): page, per_page, sort = parse_list_params() q = request.args.get("q", "").strip().lower() items = [p for p in PRODUCTS if q in p["name"].lower()] items = sorted(items, key=lambda p: p[sort]) start = (page - 1) * per_page return {"page": page, "per_page": per_page, "total": len(items), "items": items[start:start + per_page]}
Lưu ý hai điểm: thứ nhất, type=int chỉ xử lý lỗi ép kiểu, còn kiểm tra khoảng giá trị vẫn phải viết tay — page=-5 ép kiểu thành công nhưng vẫn vô nghĩa. Thứ hai, sort không bao giờ được đưa thẳng vào truy vấn SQL hay getattr không kiểm soát; whitelist là bắt buộc. Tách hàm parse_list_params() ra để tái sử dụng cho mọi endpoint danh sách.
Áp dụng vào dự án của bạn
Rà lại các endpoint danh sách trong dự án và áp dụng cùng mẫu: đặt mặc định hợp lý cho page và per_page, chặn per_page quá lớn để tránh một request “hút” cả bảng, và whitelist mọi tham số điều khiển hành vi như sort hay order. Khi API trả 400 rõ ràng cho tham số sai, frontend và người tích hợp sẽ tự sửa được mà không cần bạn đọc log hộ.
Bài viết liên quan
- Flask: đọc và validate JSON body đúng cách — hết cảnh API trả lỗi HTML với request.get_json() trong 5 phút
- Flask: trả response tùy chỉnh bằng make_response — status, header và cookie trong 5 phút
- Flask: trả JSON đúng định dạng cho datetime và Decimal bằng custom JSON provider trong 5 phút
Câu hỏi thường gặp
request.args.get(type=int) khác gì int(request.args[“page”])?
request.args["page"] ném KeyError khi thiếu tham số và int() ném ValueError khi sai định dạng. get("page", 1, type=int) trả về mặc định 1 trong cả hai trường hợp — tiện nhưng che mất lỗi, nên cần kiểm tra chuỗi gốc như trong bài.
Vì sao page=-5 vẫn cần kiểm tra tay dù đã dùng type=int?
type=int chỉ đảm bảo giá trị là số nguyên, không đảm bảo hợp lệ về nghiệp vụ. -5 là số nguyên nhưng không phải số trang hợp lệ, nên vẫn cần page >= 1 và giới hạn per_page.
Có nên dùng abort(400) hay tự trả jsonify kèm status 400?
Cả hai đều được, nhưng abort(400) ngắn hơn và thống nhất với errorhandler tập trung nếu bạn đã có. Nếu cần định dạng lỗi JSON riêng, hãy đăng ký errorhandler cho 400.
Whitelist sort có cần thiết khi chỉ sắp xếp list Python trong bộ nhớ?
Có. Dù không chạm tới SQL, khóa sort lạ vẫn gây KeyError và lỗi 500. Whitelist biến lỗi đó thành 400, đồng thời chặn client khám phá tên trường nội bộ.
💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.
Nguồn tham khảo
- Flask — Accessing Request Data: https://flask.palletsprojects.com/en/stable/quickstart/#accessing-request-data
- Werkzeug — MultiDict.get: https://werkzeug.palletsprojects.com/en/stable/datastructures/#werkzeug.datastructures.MultiDict.get
