Dưới lớp @http.route thân thiện của Odoo là Werkzeug — thư viện WSGI mà mọi request/response thật sự chạy qua. Phần lớn thời gian bạn không cần biết, nhưng khi cần chuyển hướng khách, trả đúng mã lỗi HTTP, hay đọc header/IP thô, bạn chạm tới Werkzeug qua vài API của Odoo. Làm đúng thì gọn; làm sai thì hoặc trả 200 với chữ "not found" (sai mã lỗi), hoặc — tệ hơn — mở một lỗ hổng open-redirect. Bài này mổ xẻ request.redirect, not_found, và request.httprequest, kèm một bẫy bảo mật mà Odoo âm thầm chặn giúp bạn — kiểm chứng bằng curl thật.

request.redirect: chuyển hướng đúng cách

Để đưa khách sang URL khác, return request.redirect(url, code=303). Nó tạo một response Werkzeug với header Location và mã 3xx; trình duyệt tự đi tới URL mới.

Ảnh chụp mã Python nền tối redirect 404 và request response thô của Werkzeug. Phần a chuyển hướng mặc định code 303 See Other: route qcp_wz di_toi return request redirect qcp_wz dich code 303. Phần b trả 404 đúng cách đừng return chuỗi not found: route qcp_wz khong_thay raise werkzeug exceptions NotFound Không tìm thấy thẻ, hoặc return request not_found. Phần c redirect local True mặc định bóc scheme host chỉ giữ path chống open-redirect ra site độc: hàm ngoai return request redirect https evil example com cuop, Location bị ép thành gạch chéo cuop không ra evil com. Phần d request httprequest là Werkzeug Request thô: hàm info lấy r bằng request httprequest đọc r method r path r headers get User-Agent r args, resp bằng request make_response là Werkzeug Response, resp set_cookie qcp_demo xin-chao return resp

Hình 1: Bốn tình huống Werkzeug. redirect mặc định 303 (See Other — đúng cho sau khi POST). raise werkzeug.exceptions.NotFound trả 404 chuẩn. request.httprequest là đối tượng Request thô (method, path, headers, args). request.make_response trả về Response Werkzeug — set_cookie, đặt header tuỳ ý.

Chạy curl -sD - (không tự đi theo redirect) thấy đúng mã và Location:

Ảnh chụp terminal nền tối curl gạch sD gạch thật trên Odoo 19 curl không tự đi theo redirect. Phần a redirect 303: curl qcp_wz di_toi trả về HTTP 303 SEE OTHER Location gạch chéo qcp_wz gạch chéo dich. Phần b 404 NotFound: curl qcp_wz khong_thay trả HTTP 404. Phần c redirect ngoài site bị local True chặn: curl qcp_wz ngoai redirect tới evil example com, kết quả HTTP 303 SEE OTHER Location chỉ còn gạch chéo cuop host bị bóc. Phần d đọc request thô cộng set cookie: curl với header User-Agent MyBot 1.0 tới qcp_wz info q cafe sl 2, Set-Cookie qcp_demo xin-chao Path gạch chéo, body JSON method GET path qcp_wz info user_agent MyBot 1.0 accept_lang vi-VN args q cafe sl 2

Hình 2: Kết quả thật. (a) 303 SEE OTHER + Location: /qcp_wz/dich. (b) 404. (d) đọc đúng method, User-Agent, Accept-Language, query args, và đặt được cookie. Phần (c) là điểm đáng chú ý nhất — mục sau.

Bẫy bảo mật: local=True chặn open-redirect

Nhìn kỹ chữ ký thật: request.redirect(location, code=303, local=True). Tham số local=True (mặc định) là một lớp bảo vệ âm thầm nhưng quan trọng. Khi bật, Odoo bóc bỏ scheme và host khỏi URL, chỉ giữ lại path. Nghĩa là dù bạn (hay kẻ tấn công qua một tham số) đưa vào https://evil.example.com/cuop, redirect thật chỉ tới /cuop trên chính site của bạn:

return request.redirect('https://evil.example.com/cuop')
# curl thấy:  Location: /cuop   (KHÔNG phải evil.example.com)

Đây là phòng thủ chống open-redirect — lỗ hổng kinh điển: kẻ xấu dụ người dùng bấm link .../redirect?next=https://evil.com, site của bạn "ngoan ngoãn" chuyển họ sang trang lừa đảo mang danh nghĩa domain của bạn. Vì local=True là mặc định, redirect nội bộ luôn an toàn. Chỉ đặt local=False khi bạn thật sự cần chuyển sang một domain ngoài đã tin tưởng (cổng thanh toán, SSO) — và khi đó tự chịu trách nhiệm kiểm URL.

request.httprequest: request thô của Werkzeug

request của Odoo là lớp bọc; muốn dữ liệu HTTP thô, dùng request.httprequest — chính là Werkzeug Request:

  • request.httprequest.method — GET/POST...
  • request.httprequest.headers — mọi header (.get('User-Agent')).
  • request.httprequest.args — tham số query (?q=cafe → {'q': 'cafe'}).
  • request.httprequest.form — dữ liệu form POST; request.httprequest.files — file (nhớ phần upload).
  • request.httprequest.remote_addr, .cookies, .url.

Lưu ý về IP: sau nginx/proxy, remote_addr là IP của proxy; muốn IP thật của khách phải đọc X-Forwarded-For (và cấu hình proxy chuyển tiếp đúng), nếu không mọi khách trông như cùng một IP.

make_response và Response

request.make_response(data, headers, cookies, status) trả về một Response Werkzeug — bạn kiểm soát trọn: .status_code, .headers['...'], .set_cookie(...). Trong Hình 2, Set-Cookie: qcp_demo=xin-chao là từ resp.set_cookie(...). Với các loại lỗi khác, có werkzeug.exceptions.Forbidden (403), BadRequest (400), hoặc abort(Response(...)) để dừng ngay.

Vài lưu ý

  • Trả đúng mã lỗi. raise werkzeug.exceptions.NotFound()/request.not_found() cho 404, đừng return 'not found' (đó là 200). Client, crawler, monitoring dựa vào mã HTTP.
  • code của redirect có ý nghĩa. 303 (See Other) cho sau-POST; 302 (Found) tạm thời; 301 (Moved Permanently) khi URL đổi vĩnh viễn (SEO chuyển hết "sức mạnh" sang URL mới — đừng dùng 301 cho cái tạm).
  • Đừng tin URL từ tham số. Nhận ?next=... rồi redirect thẳng là mời open-redirect; để local=True lo, hoặc kiểm URL nằm trong danh sách trắng.

Ba ý mang về

  1. request.redirect(url, code=303) trả redirect Werkzeug (header Location); chọn code đúng (303 sau-POST, 301 vĩnh viễn cho SEO). Trả lỗi bằng raise werkzeug.exceptions.NotFound()/request.not_found() — đúng mã HTTP, không phải chuỗi 200.
  2. local=True (mặc định) chặn open-redirect: mọi scheme+host bị bóc, chỉ còn path nội bộ — redirect('https://evil.com/x') thành /x; chỉ local=False cho domain ngoài đã tin.
  3. request.httprequest là Werkzeug Request thô (method, headers, args, form, files, remote_addr, cookies); request.make_response là Response (set_cookie, header, status) — nhớ X-Forwarded-For khi cần IP thật sau proxy.

Nói mãi về "danh sách" mà chưa gói gọn cách phân trang chuẩn cho một route tự viết. Phần sau đúc kết phân trang danh sách trong controller — tính offset/limit, dựng liên kết trang trước/sau, và tránh những lỗi lệch trang hay gặp.