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:

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ốiBiến đổiGọiBackend 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 /kkhô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 …
}
Caddy trả 400 Bad Request, và không ghi gì vào log Sau khi cắt, chuỗi còn lại là 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}
Cách viết đúng Địa chỉ trong 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.