Ngiêm cấm sử đụng PROXY và VPS vào mục đích trái pháp luật bạn sẽ phải chịu toàn bộ trách nhiệm trước pháp luật khi sử dụng dịch vụ của chúng tôi tks!
HTTP 303 See Other sau POST: trace export API, so với 307

HTTP 303 See Other sau POST: trace export API, so với 307

Chuyên viên Kỹ thuật ProxyVN 03-26-2026 Đang tính... phút đọc

HTTP 303 See Other sau POST là cách server nói với client: yêu cầu POST đã được xử lý, kết quả nằm ở một URL khác, hãy dùng GET để lấy. Trong workflow export API, đây là chỗ hay sinh lỗi khó thấy: gửi lại POST nhầm chỗ, tạo job trùng, hoặc mất luôn yêu cầu export. ProxyVN dựng một API export lab, chạy requests, httpx, urllib và curl thật qua các mã 301, 302, 303, 307, 308, ghi trace phía server để thấy method, body và header thực sự đi tới đâu.

HTTP 303 See Other sau POST: mục tiêu và phạm vi không lặp

Phần này chốt bài toán, đầu ra và ranh giới với các bài đã có.

Bài toán cần giải

Một script gửi POST tạo export báo cáo đơn hàng. Server trả về mã 3xx kèm Location. Script dùng mặc định của thư viện, nhận được 200, ghi log thành công. Vài tuần sau mới phát hiện có ngày export không chạy, có ngày bị tạo hai job. Lý do nằm ở cách client đổi hoặc giữ method khi đi theo chuyển hướng.

Đầu ra nghiệm thu

Đầu ra là trace từng request phía server: method, đường dẫn, content type, độ dài body, có Authorization hay không, cùng số job thực sự được tạo. Trace phủ bốn client trên sáu tình huống. Kèm theo là một hàm submit export không phụ thuộc hành vi mặc định của thư viện.

Ranh giới với bài 202 polling

Bài API trả 202 và cách polling đã có phần 303 từ status endpoint: client hỏi bằng GET, job xong thì server trả 303 tới kết quả. Ở đó method vẫn là GET từ đầu tới cuối. Bài này đứng ở bước trước: POST nhận 303 ngay, client phải đổi sang GET. Bài cũng so với 307 để thấy khi nào POST bị gửi lại.

Ranh giới với bài 405 và bài redirect chung

Bài lỗi 405 Method Not Allowed khi crawl nói về method bị từ chối nói chung. Ở đây 405 chỉ xuất hiện như hậu quả của việc gửi lại POST sai chỗ. Bài không lặp lại kiến thức redirect trên trình duyệt, chỉ xét client lập trình gọi API.

Giới hạn của lab

API export là máy chủ Python tự viết bằng http.server, nghe ở 127.0.0.1:9731. Đường dẫn, mã job và cách tạo job do ProxyVN đặt. Thứ thật trong lab là bốn client và hành vi của chúng. API thật có thể dùng 201 Created, 202 Accepted hoặc 303 tùy thiết kế, không có quy định bắt buộc.

Ai nên đọc

Người viết script gọi API export, import, thanh toán, hay bất cứ thao tác POST nào có hậu quả. Người dùng curl để thử nhanh rồi chuyển sang Python. Người vận hành pipeline thấy job trùng hoặc job mất mà chưa rõ vì sao.

Kiểm tra đổi phương thức POST sang GET theo HTTP 303 See Other sau POST trong workflow export API

Phần này tóm tắt chuẩn, chỉ giữ ý ảnh hưởng tới client.

Định nghĩa 303

Theo RFC 9110 mục 15.4.4, 303 cho biết server chuyển hướng user agent tới một tài nguyên khác, nêu ở Location, để cung cấp phản hồi gián tiếp cho request gốc. User agent có thể gửi request lấy dữ liệu tới URI đó, với HTTP là GET hoặc HEAD.

Dùng chính cho POST

RFC ghi 303 áp dụng cho mọi method, nhưng chủ yếu để kết quả của một POST trở thành một tài nguyên riêng có thể nhận diện, đánh dấu và cache. Đó chính là mẫu export: POST tạo job, 303 trỏ tới tài nguyên kết quả, GET lấy kết quả.

URI mới không tương đương URI gốc

RFC nhấn mạnh URI trong Location không được coi là tương đương URI đích. Nói cách khác, không được hiểu 303 là "endpoint đã dời chỗ". Client đi theo 303 lấy kết quả của lần POST này, lần POST sau vẫn gửi tới URI cũ.

307 giữ nguyên method

303 đổi sang GET, 307 giữ POST

303 đổi sang GET, 307 giữ POST

Theo RFC 9110 mục 15.4.8, 307 cho biết tài nguyên tạm thời nằm ở URI khác, và user agent không được đổi method khi tự chuyển hướng. POST nhận 307 sẽ được gửi lại thành POST, kèm body, tới Location. 308 cũng giữ method nhưng là chuyển vĩnh viễn.

301 và 302 có lịch sử riêng

RFC có ghi chú: thời HTTP/1.0, 301 và 302 vốn giữ method, nhưng các user agent đời đầu tách làm hai hướng, rồi dần thống nhất đổi sang GET. Vì vậy RFC hiện cho phép user agent đổi POST sang GET với 301 và 302. Nếu không muốn đổi, server dùng 307 hoặc 308.

Bỏ header nội dung khi đổi sang GET

RFC liệt kê việc client nên làm khi tự chuyển hướng. Một điểm đáng chú ý: nếu method đổi sang GET hoặc HEAD, client nên bỏ các header gắn với nội dung như Content-Type, Content-Length, Content-Encoding. Phần lab sẽ kiểm điểm này với từng thư viện.

Request không an toàn cần thận trọng

RFC nói tự chuyển hướng cần cẩn thận với method không an toàn, vì người dùng có thể không muốn request đó bị chuyển đi. POST tạo export là method không an toàn. Đây là lý do bài khuyên tắt tự chuyển hướng cho bước POST và tự xử lý.

303 cũng dùng được cho GET

RFC nói 303 trả cho GET có nghĩa là server không có đại diện của tài nguyên đích để gửi qua HTTP, nhưng Location trỏ tới một tài nguyên mô tả nó. Đây là trường hợp của bài 202 polling. Bài này chỉ xét POST, nơi việc đổi method mới là chuyện chính.

Cache và tài nguyên kết quả

Vì tài nguyên kết quả có URL riêng, nó có thể được cache như mọi tài nguyên GET khác. Với export dữ liệu có thay đổi, server nên đặt Cache-Control phù hợp cho URL kết quả. Phía client, nếu đi qua proxy có cache, kiểm header Age trước khi tin rằng kết quả là mới.

Bảng ngữ nghĩa

Bảng dưới gom cách RFC 9110 mô tả từng mã khi request gốc là POST.

Bảng: Bảng ngữ nghĩa

Mã

Ý nghĩa

Method khi đi tiếp

Body

301

Chuyển vĩnh viễn

Được phép đổi sang GET

Bỏ nếu đổi

302

Chuyển tạm

Được phép đổi sang GET

Bỏ nếu đổi

303

Xem tài nguyên khác

GET hoặc HEAD

Bỏ

307

Chuyển tạm, giữ method

POST

Gửi lại

308

Chuyển vĩnh viễn, giữ method

POST

Gửi lại

Vì sao lỗi này khó thấy

Mã cuối mà client nhìn thấy hầu như luôn là 200, kể cả khi export không được tạo. Log ứng dụng ghi thành công. Chỉ log phía server hoặc phép đối soát số job mới cho thấy sự thật. Đó là lý do lab đặt trọng tâm vào trace phía server thay vì kết quả trả về client.

Gửi POST tạo export job fixture cho HTTP 303 See Other sau POST

Phần này dựng API export lab, chạy thật với Python 3.12.10.

Mô hình export trong lab

Export trong lab xong ngay khi POST, để tập trung vào chuyển hướng. API thật thường mất thời gian: POST trả 202, client hỏi status, rồi mới có 303 tới kết quả. Khi đó bài này áp cho bước POST đầu tiên, còn bài 202 polling áp cho phần hỏi trạng thái.

Hai nhóm endpoint

API có hai nhóm. Nhóm /v1/exports nhận POST, tạo một job mới, rồi trả mã theo tham số mode với Location trỏ tới /v1/exports/{id}. Nhóm /old/exports giả lập endpoint cũ đã dời: nhận POST, không tạo job, trả mã theo mode với Location trỏ tới /v1/exports?mode=201.

if p.startswith("/v1/exports") and p.count("/") == 2:
    mode = p.split("?mode=")[-1] if "?mode=" in p else "303"
    jid = next(ids); JOBS[jid] = json.loads(b or b"{}")
    if mode == "201":
        return self.send(201, {"id": jid}, loc=f"/v1/exports/{jid}")
    return self.send(int(mode), {"id": jid}, loc=f"/v1/exports/{jid}")
if p.startswith("/old/exports"):
    mode = p.split("?mode=")[-1]
    return self.send(int(mode), None, loc="/v1/exports?mode=201")

Tham số mode chỉ để lab chọn mã trả về. API thật không làm vậy.

Tài nguyên kết quả chỉ nhận GET

GET /v1/exports/{id} trả kết quả gồm id, số dòng 1240 và bộ lọc đã gửi. POST tới cùng đường dẫn trả 405, vì kết quả không phải chỗ tạo job. GET /v1/exports không có id trả danh sách job, không tạo gì.

Bộ lọc được lưu theo job

Mỗi job lưu nguyên payload đã gửi. Khi GET kết quả, bộ lọc được trả về cùng dữ liệu. Nhờ vậy lab kiểm được kết quả đọc về có đúng là của lần POST vừa gửi hay không, thay vì kết quả của một job cũ trùng đường dẫn.

Ghi trace mọi request

Máy chủ ghi mỗi request: method, đường dẫn, Content-Type, độ dài body đọc được, và có header Authorization hay không. Trace phía server là bằng chứng không phụ thuộc client tự báo cáo.

def rec(self, b):
    LOG.append({"m": self.command, "p": self.path, "ct": self.headers.get("Content-Type"),
                "len": len(b), "auth": bool(self.headers.get("Authorization"))})

Số job được tạo đếm bằng kích thước từ điển JOBS trước và sau mỗi ca.

Payload và token

Payload là bộ lọc nhỏ gồm shop và ngày bắt đầu. Client gửi kèm Authorization Bearer lab-token để xem token có đi theo chuyển hướng không. Token là chuỗi giả, không có giá trị ngoài lab.

Tại sao chọn cùng host

Mọi Location trong lab cùng host và cổng. requests và httpx có quy tắc bỏ Authorization khi chuyển hướng sang host khác. Lab không xét ca đó, để tập trung vào method và body. Nếu API thật trả Location sang host khác, cần chạy thêm một lab riêng.

Endpoint 201 cho đối chứng

Endpoint /v1/exports?mode=201 là đích của các chuyển hướng từ /old. Nó tạo job và trả 201 Created với Location tới kết quả. 201 không phải chuyển hướng, client không đi theo Location của 201. Đây là dạng phản hồi đơn giản nhất cho thao tác tạo, nhiều API dùng thay cho 303.

Đọc 303 và Location của tài nguyên kết quả cho HTTP 303 See Other sau POST

Phần này đọc 303 khi tắt tự chuyển hướng, bước nên làm trước khi tin bất kỳ mặc định nào.

requests với allow_redirects=False

requests trả về đúng 303 và header Location /v1/exports/5. Server tạo một job, chỉ thấy một POST. Đây là trạng thái gốc, chưa có gì bị thư viện quyết định thay.

POST 303 allow_redirects=False  303 Location=/v1/exports/5 | job moi=1 | POST /v1/exports?mode=303 [ct=application/json len=35]

Độ dài body 35 byte là JSON payload do requests sinh.

httpx mặc định không đi theo

httpx 0.28.1 mặc định không đi theo chuyển hướng. Gọi httpx.post nhận về 303, và thuộc tính next_request cho biết request kế tiếp mà httpx sẽ gửi nếu được phép: GET /v1/exports/10. Thuộc tính này hữu ích để kiểm tra trước khi đi tiếp.

httpx mac dinh (khong follow)   303 next_request=GET /v1/exports/10 | job moi=1

next_request đã đổi method sang GET, đúng ngữ nghĩa 303.

Không có Location thì sao

303 không có Location là phản hồi lỗi phía server, vì RFC định nghĩa 303 dựa trên Location. Client gặp ca này nên dừng, ghi body và báo người vận hành. Đừng đoán URL kết quả theo mã job trong body, vì cấu trúc URL có thể đổi mà không báo trước.

Location có thể là tương đối

Location trong lab là đường dẫn tương đối. Client phải ghép với URL request gốc. Hàm urljoin của thư viện chuẩn làm việc này. Đừng ghép chuỗi bằng tay, vì Location có thể bắt đầu bằng hai gạch chéo hoặc chứa đường dẫn tương đối kiểu ../.

Đọc body của 303

Lab trả body JSON có id cho 303. RFC nói phản hồi 303 nên có một ghi chú siêu văn bản ngắn chứa liên kết tới Location. API thật có thể trả HTML, JSON hoặc rỗng. Client không nên dựa vào body của 303 để lấy mã job. Mã job nằm trong Location hoặc trong tài nguyên kết quả.

Lưu Location trước khi đi tiếp

Sau khi nhận 303, ghi Location vào sổ job trước khi gửi GET. Nếu GET lỗi vì mạng, lần chạy sau có thể lấy kết quả bằng GET mà không phải POST lại. Đây là cách tránh tạo job trùng khi mạng chập chờn.

Kiểm mã job trong Location

Location /v1/exports/5 chứa mã job 5. Một số API dùng mã ngẫu nhiên dài, một số dùng mã tăng dần. Hàm submit không cần hiểu cấu trúc này. Nó chỉ cần lưu nguyên chuỗi Location, vì đó là định danh mà server đã cam kết trả kết quả.

Location cho thấy job đã tạo

Với export, nhận được 303 có nghĩa là server đã xử lý POST. Từ thời điểm đó, POST lại là tạo thêm job. Mã 303 vì vậy là ranh giới: trước nó còn có thể gửi lại, sau nó chỉ được GET.

Quan sát client dùng GET khi theo redirect trong HTTP 303 See Other sau POST

Phần này là kết quả chạy thật của bốn client khi để chúng tự đi theo chuyển hướng.

requests đổi sang GET và bỏ body

Trace POST rồi GET

Trace POST rồi GET

requests 2.34.2 nhận 303 rồi gửi GET /v1/exports/1, không có Content-Type, không có body, vẫn giữ Authorization vì cùng host. Kết quả cuối 200 với dữ liệu export. Một job được tạo.

POST /v1/exports -> 303   200 [303] | job moi=1 | POST /v1/exports?mode=303 [ct=application/json len=35] +auth -> GET /v1/exports/1 +auth

Đây là hành vi đúng với ngữ nghĩa 303.

httpx đổi sang GET nhưng còn Content-Type

httpx 0.28.1 với follow_redirects=True cũng đổi sang GET, body 0 byte. Khác biệt: request GET vẫn mang Content-Type: application/json. Trace phía server ghi rõ.

POST /v1/exports -> 303   200 [303] | job moi=1 | POST /v1/exports?mode=303 [ct=application/json len=32] +auth -> GET /v1/exports/6 [ct=application/json len=0] +auth

RFC khuyên bỏ các header nội dung như Content-Type khi đổi sang GET. Phần lớn server bỏ qua Content-Type trên GET không có body, nên ca này thường vô hại. Nhưng một số gateway kiểm tra chặt có thể từ chối. Đây là điều đáng biết khi gặp lỗi lạ chỉ xảy ra với httpx.

urllib đổi sang GET

urllib.request trong Python 3.12.10 cũng đổi sang GET với 303 và bỏ body, không còn Content-Type. Kết quả 200 với URL cuối /v1/exports/11, một job.

curl -L với -d

curl 8.13.0 với -L và -d gửi POST, nhận 303, rồi gửi GET tới /v1/exports/13, body 0 byte. Header Content-Type do người dùng đặt bằng -H vẫn đi theo, vì curl giữ header tự đặt qua các lần chuyển hướng. Kết quả 200, một job.

curl -L -X POST là cái bẫy

curl -X POST -L nhận 405

curl -X POST -L nhận 405

Thêm -X POST vào lệnh curl, kết quả đổi hẳn. curl gửi POST tới /v1/exports/14 sau 303, body 0 byte, và nhận 405. Lý do: -X đặt method cho mọi request, kể cả request sau chuyển hướng. Lệnh vẫn có một job được tạo ở bước đầu, nhưng người chạy chỉ thấy 405 và tưởng export thất bại.

curl -L -d (303)          200 redirects=1 | job moi=1 | POST ... -> GET /v1/exports/13 [ct=application/json len=0]
curl -L -X POST -d (303)  405 redirects=1 | job moi=1 | POST ... -> POST /v1/exports/14 [ct=application/json len=0]

Người dùng thường thêm -X POST cho rõ ràng, trong khi -d đã ngầm là POST. Với -L, thói quen này đổi kết quả.

Vì sao Content-Type còn sót

Trong lab, httpx tạo request mới cho bước chuyển hướng và bỏ body cùng Content-Length khi đổi method, nhưng Content-Type đặt từ tham số json vẫn còn trong header. Bài không đọc mã nguồn httpx để xác định nguyên nhân cụ thể, chỉ ghi lại điều trace phía server thấy với bản 0.28.1.

curl và header tự đặt

curl giữ các header đặt bằng -H qua mọi lần chuyển hướng, theo mô tả của tùy chọn -L trong tài liệu curl. Content-Type đặt tay vì vậy đi theo request GET. Nếu header tự đặt là Authorization, nó cũng có thể đi theo tới host khác. Với lệnh có token, nên tránh dùng -L với API không rõ Location.

Kiểm tra bằng server tự viết

Anh em có thể kiểm thư viện đang dùng bằng một máy chủ ghi log như lab, chạy trong vài phút. Gửi POST có body và header tự đặt, cho máy chủ trả 303, rồi đọc request thứ hai. Cách này nhanh và đáng tin hơn đọc tài liệu, vì nó cho thấy đúng phiên bản đang cài trên máy.

Bảng trace cho 303

Bảng dưới gom request thứ hai mà mỗi client gửi sau khi nhận 303.

Bảng: Bảng trace cho 303

Client

Method

Body

Content-Type

Kết quả

requests 2.34.2

GET

0

Không

200

httpx 0.28.1

GET

0

Còn application/json

200

urllib Python 3.12

GET

0

Không

200

curl -L -d

GET

0

Còn, do -H

200

curl -L -X POST -d

POST

0

Còn, do -H

405

Authorization đi theo cùng host

requests và httpx giữ Authorization khi Location cùng host. Đây là hành vi mong muốn với API export cùng tên miền. Nếu Location trỏ sang kho tệp ở tên miền khác, cả hai thư viện có cơ chế bỏ Authorization. Lab không thử ca khác host.

Đối chiếu với 307 để kiểm HTTP 303 See Other sau POST không gửi lại POST sai mục tiêu

Phần này đặt 303 cạnh 307, 301, 302, 308.

307 tới tài nguyên kết quả

Server cấu hình sai trả 307 thay vì 303 sau khi tạo job. requests gửi lại POST kèm nguyên body 35 byte tới /v1/exports/2, nhận 405. httpx làm y hệt. Job vẫn được tạo một lần ở bước đầu, nhưng client báo lỗi.

POST /v1/exports -> 307   405 [307] | job moi=1 | POST /v1/exports?mode=307 [ct=application/json len=35] +auth -> POST /v1/exports/2 [ct=application/json len=35] +auth

Nếu đích của 307 là một endpoint nhận POST, job thứ hai sẽ được tạo. Lab không dựng ca này để giữ trace gọn, nhưng nguyên lý giống ca /old với 307.

Lỗi 405 không nói lên nguyên nhân

Khi nhận 405 sau 307, người xem log dễ kết luận API không hỗ trợ POST. Thực tế POST đầu đã thành công, chỉ có POST thứ hai sai chỗ. Ghi history của phản hồi, hoặc tắt tự chuyển hướng, để thấy đủ chuỗi request thay vì chỉ mã cuối.

307 đúng chỗ: endpoint đã dời

Với /old/exports, 307 và 308 là lựa chọn đúng: endpoint cũ chuyển sang endpoint mới, POST phải tới đích với nguyên body. requests, httpx và curl đều gửi lại POST 35 byte tới /v1/exports?mode=201, nhận 201, tạo một job.

301 và 302 làm mất export

Với /old/exports trả 301 hoặc 302, requests, httpx, urllib và curl đều đổi sang GET. GET /v1/exports chỉ trả danh sách job. Kết quả cuối là 200, số job mới bằng 0. Đây là kiểu lỗi nguy hiểm nhất của lab: client báo thành công, nhưng export không hề được tạo.

POST /old/exports -> 302   200 [302] | job moi=0 | POST /old/exports?mode=302 [...] -> GET /v1/exports?mode=201 +auth

Script chỉ kiểm r.ok sẽ không bao giờ phát hiện.

curl --post302

curl có tùy chọn --post302 để giữ POST khi gặp 302. Với lựa chọn này, lab thấy curl gửi lại POST 35 byte và tạo job. curl còn có --post301 và --post303 cho các mã còn lại, mô tả trong tài liệu curl.

urllib từ chối 307 và 308 với POST

urllib.request ném HTTPError 307 và 308 khi request gốc là POST, không đi tiếp. Với /v1/exports?mode=307, job đã được tạo ở bước đầu, nhưng script nhận ngoại lệ. Với /old/exports?mode=308, không job nào được tạo. Hai ca cùng một ngoại lệ nhưng hậu quả trái ngược.

Job ở bước đầu vẫn tồn tại

Ở ca 307 tới kết quả, client báo 405 nhưng job đã được tạo ở bước POST đầu. Người chạy thấy lỗi, thường sẽ chạy lại lệnh, và tạo job thứ hai. Đọc trace phía server, hoặc ghi Location ngay khi nhận 3xx, là cách duy nhất để biết job đầu đã có.

Bảng đầy đủ

Bảng dưới gom kết quả thật của requests và httpx, hai thư viện cho kết quả giống nhau ở mọi ca.

Bảng: Bảng đầy đủ

Tình huống

Request thứ hai

Kết quả

Job mới

/v1 trả 303

GET kết quả

200

1

/v1 trả 307

POST kết quả, có body

405

1

/old trả 301

GET danh sách

200

0

/old trả 302

GET danh sách

200

0

/old trả 307

POST endpoint mới, có body

201

1

/old trả 308

POST endpoint mới, có body

201

1

Thứ tự kiểm khi gặp sự cố

Khi export bị trùng hoặc mất, kiểm theo thứ tự: mã server trả sau POST, Location trỏ tới đâu, client gửi method gì ở request thứ hai, server tạo bao nhiêu job. Bốn câu trả lời này đủ để định vị lỗi nằm ở server, ở thư viện, hay ở lệnh curl của người chạy.

Mỗi mã đúng một chỗ

Từ bảng rút ra: 303 dành cho trỏ tới kết quả sau POST, 307 và 308 dành cho endpoint dời chỗ. Dùng nhầm mã nào ở chỗ nào cũng sinh lỗi. Client không sửa được lỗi của server, nhưng có thể dừng lại thay vì làm theo mù quáng.

Đầu ra nghiệm thu của HTTP 303 See Other sau POST: trace POST rồi GET kết quả

Phần này là hàm submit dùng được, kèm tiêu chí và giới hạn.

Hàm submit không phụ thuộc mặc định

Hàm submit export

Hàm submit export

Hàm dưới gửi POST với allow_redirects=False, tự đi theo 303 bằng GET, và dừng lại với 307, 308, 301, 302 kèm thông báo rõ ràng. Trace được trả về cùng kết quả.

def submit_export(session, url, payload):
    r = session.post(url, json=payload, allow_redirects=False, timeout=5)
    trace = [("POST", url, r.status_code, r.headers.get("Location"))]
    if r.status_code == 303:
        loc = requests.compat.urljoin(url, r.headers["Location"])
        g = session.get(loc, allow_redirects=False, timeout=5)
        trace.append(("GET", loc, g.status_code, None))
        return g.json(), trace
    if r.status_code in (307, 308):
        raise RuntimeError(f"{r.status_code}: server muon gui lai POST toi {r.headers.get('Location')}")
    if r.status_code in (301, 302):
        raise RuntimeError(f"{r.status_code} sau POST: khong tu doi sang GET")
    r.raise_for_status()
    return r.json(), trace

Hàm này là mã lab. Bản dùng thật nên ghi Location vào sổ job trước khi gửi GET.

Trace thật của hàm

Chạy trên API lab, hàm trả kết quả export id 17 với 1240 dòng và đúng bộ lọc. Hai ca sai mã dừng lại với thông báo, không gửi request thứ hai.

ket qua: {'id': 17, 'rows': 1240, 'filter': {'shop': 'a', 'from': '2026-10-01'}}
 trace ('POST', '/v1/exports?mode=303', 303, '/v1/exports/17')
 trace ('GET', '/v1/exports/17', 200, None)
dung lai: 307: server muon gui lai POST toi /v1/exports/18
dung lai: 302 sau POST: khong tu doi sang GET, kiem tra lai endpoint

Ca 307 vẫn tạo job 18 ở bước POST. Thông báo dừng giúp người vận hành biết có một job đang chờ, không gửi lại.

Tiêu chí đạt

Client đạt khi mọi dòng dưới đây đúng.

  • Bước POST tắt tự chuyển hướng.
  • 303 được đi theo bằng GET, không body, không Content-Type.
  • Location được ghi vào sổ trước khi gửi GET.
  • 307 và 308 sau POST tạo job không được tự gửi lại.
  • 301 và 302 sau POST không được tự đổi sang GET rồi báo thành công.
  • Số job phía server khớp số lần anh em định tạo.
  • curl dùng -d mà không thêm -X POST khi có -L.

Đếm job là phép kiểm cuối

Mọi lần export phải đối được với một job id phía server. Mã 200 ở client không chứng minh gì.

Trong lab, tổng số job tạo ra là 18 sau mọi ca, khớp số dòng job moi=1 trong trace. Với API thật, đối soát bằng endpoint liệt kê job theo khoảng thời gian.

Sổ job tối giản

Sổ job chỉ cần vài cột: thời điểm gửi, URL POST, mã nhận được, Location, trạng thái GET, kích thước kết quả. Một bảng SQLite là đủ cho script đơn lẻ. Lần chạy sau đọc các dòng có Location mà chưa có kết quả, rồi chỉ gửi GET cho chúng.

Thử lại POST cần khóa riêng

Nếu POST thất bại vì mạng trước khi nhận được mã, không biết job đã tạo hay chưa. Khi API hỗ trợ khóa chống trùng như Idempotency-Key, gửi kèm khóa để POST lại an toàn. Khi API không hỗ trợ, gọi endpoint liệt kê job để kiểm trước khi gửi lại.

Những gì lab chưa chạy

Chưa thử API export thương mại. Chưa thử Location sang host khác và việc bỏ Authorization. Chưa thử HEAD sau 303, HTTP/2, proxy có cache. Chưa thử Idempotency-Key, tham khảo bài 202 polling. Hàm submit và sổ job là ví dụ triển khai, chỉ chạy trên API lab.

Proxy ở đâu trong luồng

Proxy chuyển tiếp nằm giữa client và API, không đổi mã 3xx hay method. Hành vi đổi POST sang GET thuộc về client. Khi API chỉ nhận request từ IP tại Việt Nam, anh em có thể mua proxy tại trang chủ ProxyVN. Nhớ cấu hình proxy giống nhau cho bước POST và bước GET. Vai trò của proxy có ở bài Web Scraping proxy là gì, cách ghi chuỗi cấu hình có ở bài định dạng proxy.

Khi GET kết quả bị chặn

Nếu bước GET đi qua đường khác bước POST, ví dụ Location trỏ tới tên miền tải tệp có chặn theo vùng, GET có thể thất bại dù job đã tạo. Sổ job giúp thử lại GET sau khi sửa đường đi, không cần POST lại.

HTTP 303 See Other sau POST: câu hỏi thường gặp

Các câu dưới đây gom những điểm hay bị hỏi.

requests có đổi POST sang GET khi gặp 303 không?

Có. Lab với requests 2.34.2 thấy GET tới Location, không body, không Content-Type.

httpx xử lý 303 thế nào?

Mặc định không đi theo. Bật follow_redirects thì đổi sang GET, nhưng trong lab vẫn còn Content-Type.

Vì sao curl trả 405 sau 303?

Vì dùng -X POST cùng -L. curl giữ method đã đặt cho mọi request. Bỏ -X POST, chỉ dùng -d.

302 sau POST có nguy hiểm không?

Có. Lab cho thấy mọi client đổi sang GET, báo 200, nhưng không job nào được tạo.

Khi nào server nên dùng 307?

Khi endpoint dời chỗ và POST phải tới đích mới với nguyên body. Không dùng 307 để trỏ tới kết quả.

Có nên dùng 201 thay vì 303?

Đều được, tùy thiết kế API. 201 kèm Location không làm client tự chuyển hướng, nên đơn giản hơn cho script.

HEAD có dùng được sau 303 không?

RFC cho phép GET hoặc HEAD. HEAD hữu ích để kiểm kích thước kết quả trước khi tải. Lab chưa thử HEAD.

HTTP 303 See Other sau POST và những điều cần nhớ

HTTP 303 See Other sau POST nghĩa là POST đã xong, chỉ còn GET kết quả. Tắt tự chuyển hướng ở bước POST, ghi Location rồi mới GET, dừng lại với 301, 302, 307, 308 thay vì tin thư viện, và đếm job phía server. Proxy.vn - Nhà cung cấp dịch vụ proxy chất lượng hàng đầu Việt Nam khuyên anh em kiểm lại trace của HTTP 303 See Other sau POST.

TIN TỨC LIÊN QUAN