Bài 2
reverse_proxy và tiền tố đường dẫn
Trong nginx, một dấu gạch chéo trong proxy_pass quyết định backend nhận đường dẫn nào.
Caddy bỏ hẳn cơ chế đó: reverse_proxy gửi nguyên request URI, và muốn cắt tiền tố thì
phải viết ra bằng một chỉ thị riêng. Bài này đo bảy tổ hợp, trong đó hai tổ hợp không trả về 200.
Caddy không cắt gì cả
Mặc định reverse_proxy chuyển tiếp nguyên request URI. Có hai cách yêu cầu cắt tiền tố,
và chúng cắt khác nhau:
-
handle_path /b/*— cắt tiền tố của chính mẫu đó. Tiền tố bị cắt là mẫu sau khi bỏ*ở cuối rồi bỏ nốt dấu/cuối, nên/b/*cắt/bchứ không phải/b/. -
uri strip_prefix /c— cắt đúng chuỗi bạn viết, không thêm bớt gì. Nó độc lập với mẫu củahandle, nên có thể cắt nhầm hoặc không cắt gì.
Ngoài ra rewrite * <mẫu> thay hẳn đường dẫn, với {uri} là giá trị
ngay trước bước đó — nên đặt rewrite trong một khối handle_path sẽ thấy
phần đã cắt, còn đặt trong handle thì thấy đường dẫn nguyên.
Bảy tổ hợp, đo trên Caddy thật
Bộ đo gồm hai server trong cùng một container caddy:2-alpine. Server ở cổng 8081 đóng
vai backend và in lại đúng {http.request.uri} mà nó nhận.
:8081 {
handle {
respond "{http.request.uri}" 200
}
}
:80 {
handle /a/* {
reverse_proxy localhost:8081
}
handle_path /b/* {
reverse_proxy localhost:8081
}
handle /c/* {
uri strip_prefix /c
reverse_proxy localhost:8081
}
handle /d/* {
rewrite * /goc{uri}
reverse_proxy localhost:8081
}
handle_path /e/* {
rewrite * /goc{uri}
reverse_proxy localhost:8081
}
}
| Khối | Biến đổi | Gọi | Backend nhận |
|---|---|---|---|
handle /a/* | không có | /a/x/y | /a/x/y |
handle_path /b/* | không có | /b/x/y | /x/y |
handle /c/* | uri strip_prefix /c | /c/x/y | /x/y |
handle /d/* | rewrite * /goc{uri} | /d/x/y | /goc/d/x/y |
handle_path /e/* | rewrite * /goc{uri} | /e/x/y | /goc/x/y |
handle_path /j/sub/* | không có | /j/sub/a | /a |
handle_path /k | không có | /k | / |
Hai dòng cuối cho thấy hai chi tiết nhỏ: handle_path cắt được tiền tố nhiều tầng, và
khi cắt hết sạch thì Caddy đặt lại đường dẫn thành / chứ không để rỗng.
Dòng thứ tư và thứ năm là cặp đáng so nhất. Cùng một rewrite * /goc{uri}, đặt trong
handle cho ra /goc/d/x/y, đặt trong handle_path cho ra
/goc/x/y. {uri} đọc giá trị tại thời điểm chạy, và
handle_path đã cắt trước đó.
Mất dấu gạch chéo thì được 400
Hai cách viết dưới đây đều cắt quá tay một ký tự, và cả hai đều cho cùng một kết quả:
handle_path /g* { reverse_proxy … } gọi /gx/y → còn lại "x/y"
handle /h/* { gọi /h/x/y → còn lại "x/y"
uri strip_prefix /h/
reverse_proxy …
}
x/y — không bắt đầu bằng / nên không còn là
một đường dẫn hợp lệ. Đo được: mã 400, thân trả về đúng chuỗi 400 Bad Request, header
Via: 1.1 Caddy, và docker logs không có dòng lỗi nào. Backend không hề
nhận được request.
Đây là chỗ Caddy và nginx phạt cùng một sơ suất theo hai cách. Với nginx, location /d/
cộng proxy_pass http://be:8080/goc cho ra /gocx/y — một đường dẫn hợp lệ
nhưng sai, backend trả 404 và không ai biết vì sao. Với Caddy, request dừng ngay ở proxy với mã 400.
Lỗi của Caddy dễ thấy hơn, nhưng khó lần ra nguồn hơn vì không có log.
Đường dẫn trong địa chỉ upstream thì được 502
Người quen nginx hay viết đường dẫn ngay trong địa chỉ backend. Caddy không hiểu cách viết đó, nhưng cũng không từ chối nó:
handle /f/* {
reverse_proxy localhost:8081/goc
}
$ caddy validate --config /c --adapter caddyfile
Valid configuration
Server khởi động bình thường. Nhìn vào cấu hình JSON mới thấy vấn đề — phần đường dẫn bị ghép thẳng vào chuỗi quay số:
"upstreams": [ { "dial": "localhost:8081/goc:80" } ]
Mọi request qua khối đó trả về 502, và log ghi:
{"level":"error","logger":"http.log.error",
"msg":"dial localhost:8081: unknown network localhost:8081",
"status":502}
reverse_proxy chỉ gồm host và cổng. Muốn thêm tiền tố cho backend thì
dùng rewrite * /goc{uri} trong cùng khối — dòng thứ tư và thứ năm của bảng trên là hai
biến thể của cách làm đó.
Phòng thí nghiệm
Đổi bất kỳ ô nào. Thuật toán chạy trên trình duyệt là bản viết lại quy tắc ở trên, đã đối chiếu với Caddy v2.11.4 trên đúng 13 tổ hợp — khớp 13/13, kể cả hai trường hợp 400 và 502.
Đường dẫn gửi tới backend
Đổi bất kỳ ô nào.
Bảy tổ hợp đã đo:
Từng bước
Tự kiểm tra
Backend phục vụ dưới /api/ và bạn cũng muốn lộ ra ngoài ở /api/. Viết thế nào?
handle /api/* { reverse_proxy be:3000 }. Không cần cắt gì, vì Caddy vốn không cắt. Đây là trường hợp mà người quen nginx hay viết thừa một dòng handle_path.
Backend phục vụ ở gốc / nhưng bạn lộ ra ngoài ở /api/. Viết thế nào?
handle_path /api/* { reverse_proxy be:3000 }. handle_path cắt /api, nên /api/users tới backend thành /users. Thử trong phòng thí nghiệm để thấy từng bước.
Vì sao uri strip_prefix /api/ (có gạch chéo cuối) là một cấu hình đáng ngờ?
Vì nó cắt cả dấu gạch chéo, để lại users thay vì /users, và Caddy trả 400 Bad Request mà không ghi log. Đúng trường hợp đã đo ở mục 400 phía trên.
Bạn cần backend nhận /v1/users khi người dùng gọi /api/users. Viết thế nào?
handle_path /api/* { rewrite * /v1{uri} … }. handle_path cắt /api trước, còn /users; rồi rewrite thấy {uri} là /users và ghép thành /v1/users. Đây chính là dòng thứ năm của bảng, đổi /goc thành /v1.