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.

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:

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, đừngreturn 'not found'(đó là 200). Client, crawler, monitoring dựa vào mã HTTP. codecủ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=Truelo, hoặc kiểm URL nằm trong danh sách trắng.
Ba ý mang về
request.redirect(url, code=303)trả redirect Werkzeug (headerLocation); chọncodeđúng (303 sau-POST, 301 vĩnh viễn cho SEO). Trả lỗi bằngraise werkzeug.exceptions.NotFound()/request.not_found()— đúng mã HTTP, không phải chuỗi 200.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=Falsecho domain ngoài đã tin.request.httprequestlà Werkzeug Request thô (method, headers, args, form, files, remote_addr, cookies);request.make_responselà Response (set_cookie, header, status) — nhớX-Forwarded-Forkhi 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.