Clojure clj-http proxy: proxy-host, xác thực, SOCKS và lỗi 407
Clojure clj-http proxy là cách cho thư viện clj-http gửi request qua máy trung gian bằng các khóa trong map request. Khác với cấu hình JVM toàn cục, clj-http cho đặt proxy riêng cho từng request. ProxyVN chạy clj-http 3.13.1 trên Java 8 qua proxy HTTP có mật khẩu và proxy SOCKS5 tự dựng, ghi lại 19 ca thử và nguyên văn từng lỗi.
Clojure clj-http proxy nằm trong map request

Proxy đặt ngay trong map request
clj-http bọc Apache HttpClient
clj-http là thư viện HTTP phổ biến nhất trong hệ sinh thái Clojure, do Lee Hinman duy trì. Bản 3.x bọc Apache HttpClient 4.5. Trong lab, pom của clj-http 3.13.1 khai báo httpclient 4.5.14 và httpcore 4.4.16, User-Agent gửi đi là Apache-HttpClient. Mọi hành vi mạng, kể cả cách xác thực proxy, đi theo Apache HttpClient.
Điểm hay của clj-http là cấu hình nằm ngay trong map tham số của từng lời gọi. Muốn request này qua proxy A, request kia qua proxy B, chỉ cần map khác nhau. Không cần tạo client riêng như nhiều thư viện Java khác.
Proxy theo request là thế mạnh của clj-http, nhưng cũng là nơi dễ quên. Một hàm gọi thiếu khóa :proxy-host sẽ đi thẳng mà không báo gì. Phần sau có cách gom cấu hình để tránh chuyện này.
Ranh giới với bài Java proxy
Bài Java proxy nói về http.proxyHost và các system properties của JVM. clj-http cũng đọc các thuộc tính đó khi không có khóa proxy trong request, và mình đã thử điều này trong lab. Nhưng bài này tập trung vào API của clj-http: :proxy-host, :proxy-port, :proxy-user, :proxy-pass, :proxy-ignore-hosts và connection manager cho SOCKS.
Brief của bài yêu cầu phân biệt rõ cấu hình theo request với JVM properties. Kết quả lab ở phần sau cho thấy hai cơ chế cùng tồn tại, và khóa trong request được ưu tiên.
Môi trường lab
Máy lab chạy Windows 10 với Java 8 bản 1.8.0_401. Mình tải Clojure 1.12.1, clj-http 3.13.1 và các phụ thuộc Apache từ Maven Central và Clojars, chạy bằng java -cp "lib/*" clojure.main. Không dùng Leiningen hay Clojure CLI vì máy không có sẵn.
Proxy thử là một chương trình Python nghe các cổng riêng: website thử, proxy HTTP cần tài khoản user1 với mật khẩu p@ss#2026, proxy SOCKS5. Website thử tên shop.test, chỉ phân giải được bên trong proxy. Request nào đi thẳng sẽ báo UnknownHostException, request nào qua proxy HTTP sẽ có trường via là http-proxy trong JSON trả về.
Khai báo phụ thuộc cho Clojure clj-http proxy
deps.edn
Với Clojure CLI và deps.edn, khai báo clj-http trong tệp deps.edn. Brief yêu cầu tệp deps.edn làm đầu ra, đây là bản tối thiểu mình dùng làm mẫu.
{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.12.1"}
clj-http/clj-http {:mvn/version "3.13.1"}
cheshire/cheshire {:mvn/version "5.13.0"}}}
Cheshire là phụ thuộc tùy chọn, cần khi dùng :as :json để đọc JSON. README của clj-http ghi rõ cheshire dành cho :as :json. Nếu chỉ đọc chuỗi, có thể bỏ.
Với Leiningen, dòng tương ứng trong project.clj lấy từ trang clj-http trên Clojars, bản 3.13.1 tại thời điểm viết. README ghi dòng 3.13.x hỗ trợ Clojure 1.8.0 trở lên. Khóa đúng bản trong tệp phụ thuộc, vì clj-http kéo theo một chuỗi thư viện Apache, nâng một bản có thể đổi hành vi mạng.
Phụ thuộc thật sự cần khi chạy tay
Nếu chạy bằng java -cp như lab, cần đủ các jar: clojure, spec.alpha, core.specs.alpha, clj-http, httpcore, httpclient, httpclient-cache, httpasyncclient, httpcore-nio, httpmime, commons-codec, commons-io, commons-logging, slingshot, potemkin và riddley. Thiếu commons-logging hay riddley sẽ lỗi ngay khi nạp namespace.
Cách chạy tay chỉ hợp cho thử nghiệm. Dự án thật nên dùng Clojure CLI hoặc Leiningen để công cụ tự giải phụ thuộc và khóa phiên bản.
Tách thông tin proxy ra biến môi trường
Không ghi tài khoản proxy trong mã nguồn. Đọc từ biến môi trường rồi dựng map cấu hình một lần. Mật khẩu trong lab có @ và #, nhưng vì clj-http nhận tên và mật khẩu là hai giá trị riêng, không cần mã hóa phần trăm. Đây là điểm khác lớn so với các thư viện nhận proxy dạng URL.
(ns app.net)
(def proxy-opts
{:proxy-host (System/getenv "PROXY_HOST")
:proxy-port (some-> (System/getenv "PROXY_PORT") Integer/parseInt)
:proxy-user (System/getenv "PROXY_USER")
:proxy-pass (System/getenv "PROXY_PASS")})
Nếu nhà cung cấp gửi proxy dạng ip:port:user:pass, tách thành bốn biến. Bài định dạng proxy có bảng chuyển đổi giữa các kiểu viết.
Cấu hình Clojure clj-http proxy theo request
Khóa :proxy-host và :proxy-port
README của clj-http ghi proxy theo request đặt bằng :proxy-host và :proxy-port. Tài khoản đặt thêm bằng :proxy-user và :proxy-pass. Trong lab, mình gọi:
(require '[clj-http.client :as http])
(http/get "http://shop.test:8911/b"
{:proxy-host "127.0.0.1" :proxy-port 8912
:proxy-user "user1" :proxy-pass "p@ss#2026"})
Kết quả trả về 200, thân JSON có path /b và via là http-proxy. Không có khóa proxy, cùng URL báo java.net.UnknownHostException: shop.test, chứng tỏ request đi thẳng.
Apache gửi tài khoản sau khi bị hỏi

Hỏi 407 rồi mới gửi tài khoản
Nhật ký proxy cho thấy một chi tiết đáng chú ý. Mỗi lời gọi có tài khoản tạo ra hai request tới proxy. Request đầu không có tiêu đề Proxy-Authorization, proxy trả 407. Request thứ hai mới có tài khoản và được chấp nhận.
Đây là cơ chế hỏi và đáp mô tả trong chương xác thực của Apache HttpClient 4.5: client chỉ gửi tài khoản khi proxy yêu cầu. Với URL http và https đều như vậy. Ở HTTPS, nhật ký ghi hai lệnh CONNECT tới example.com:443, lệnh đầu không có tài khoản, lệnh sau có.
Hệ quả thực tế là mỗi request mới tốn thêm một vòng tới proxy. Với proxy xa hoặc chậm, độ trễ cộng thêm thấy rõ. Với gói proxy tính theo số request, cần hỏi nhà cung cấp xem request bị 407 có bị tính không.
HTTPS có tài khoản chạy được
Mình gọi https://example.com/ qua proxy có tài khoản, kết quả 200 và thân là trang HTML của example.com. Khác với client tích hợp của Java hay các thư viện dựa trên nó, clj-http 3.x dùng Apache nên không bị thuộc tính jdk.http.auth.tunneling.disabledSchemes chặn xác thực Basic cho đường hầm.
Với mật khẩu sai, HTTPS báo clojure.lang.ExceptionInfo với thông điệp clj-http: status 407. Đây là thông điệp giống hệt trường hợp URL http. Nhờ vậy, mã xử lý lỗi không cần phân biệt hai loại URL.
Đọc lỗi Clojure clj-http proxy
407 là ExceptionInfo có :status
Theo README, clj-http mặc định ném ngoại lệ khi gặp mã trạng thái bất thường. Với proxy từ chối tài khoản, ngoại lệ là ExceptionInfo, thông điệp clj-http: status 407, và ex-data chứa :status 407. Khi không gửi tài khoản nào, kết quả cũng vậy.
Đặt :throw-exceptions false thì clj-http trả map response với :status 407 thay vì ném. Mình đã thử cả hai cách. Cách thứ hai tiện khi muốn xử lý mọi mã trạng thái bằng case thay vì try.
(let [r (http/get url (merge proxy-opts {:throw-exceptions false}))]
(case (:status r)
200 (:body r)
407 (throw (ex-info "Proxy tu choi tai khoan" {:proxy (:proxy-host proxy-opts)}))
429 (throw (ex-info "Website gioi han tan suat" {:url url}))
(throw (ex-info "Loi khac" {:status (:status r)}))))
Một điểm cần nhớ: với clj-http, 407 của proxy và 503 của website đều là ExceptionInfo có :status. Muốn biết lỗi ở proxy hay website, đọc mã: 407 gần như luôn là proxy. Bài sửa lỗi 407 Proxy Authentication Required liệt kê nguyên nhân phía nhà cung cấp.
Lỗi mạng là ngoại lệ Java gốc
Khi proxy không mở cổng, clj-http ném java.net.ConnectException với thông điệp Connection refused: connect. Khi website chậm hơn :socket-timeout, ném java.net.SocketTimeoutException: Read timed out. Khi request đi thẳng mà tên miền không phân giải được, ném UnknownHostException.
Ba ngoại lệ này không phải ExceptionInfo, không có :status. Mã xử lý lỗi cần bắt cả hai nhóm. Mình gom thành bảng tra dưới đây từ chính các lần chạy trong lab.
Hiện tượng trong lab |
Ngoại lệ |
Ý nghĩa |
|---|---|---|
Không khóa proxy, tên chỉ có trong proxy |
UnknownHostException |
Request đi thẳng |
Không gửi tài khoản |
ExceptionInfo, status 407 |
Proxy đòi xác thực |
Sai mật khẩu, http hoặc https |
ExceptionInfo, status 407 |
Proxy từ chối |
Proxy không mở cổng |
ConnectException: Connection refused |
Sai cổng hoặc proxy chết |
Website chậm hơn socket-timeout |
SocketTimeoutException: Read timed out |
Chậm ở website hoặc proxy |
Website trả 503 |
ExceptionInfo, status 503 |
Website lỗi, proxy đã làm xong |
SOCKS với tên chỉ có trong proxy |
UnknownHostException |
Tên miền phân giải tại máy |
Dòng cuối của bảng liên quan tới SOCKS và rò rỉ DNS, phần sau giải thích kỹ. Các dòng còn lại cho thấy hầu hết lỗi clj-http proxy được nhận ra ngay từ tên ngoại lệ.
Timeout theo đơn vị mili giây
README ghi :socket-timeout và :connection-timeout tính bằng mili giây. Trong lab, đường dẫn /slow chờ 5 giây, với :socket-timeout 2000 thì clj-http ném SocketTimeoutException sau khoảng 2 giây. :connection-timeout áp cho việc kết nối tới proxy khi có proxy, không phải tới website.
Mình đặt :connection-timeout 5000 và :socket-timeout 15000 cho tuyến proxy. Không đặt thì một proxy treo có thể giữ luồng rất lâu. Với tác vụ crawl chạy nhiều luồng, vài luồng treo đủ làm cả tác vụ chậm lại.
Clojure clj-http proxy theo request và JVM properties
clj-http đọc http.proxyHost khi request không có khóa proxy
README ghi proxy hệ thống cấu hình qua thuộc tính Java dạng scheme.proxyHost và scheme.proxyPort, cùng http.nonProxyHosts cho danh sách bỏ qua. Mình thử đặt http.proxyHost và http.proxyPort bằng System/setProperty rồi gọi một URL http không có khóa proxy nào.
(System/setProperty "http.proxyHost" "127.0.0.1")
(System/setProperty "http.proxyPort" "8932")
(http/get "http://shop.test:8931/l")
Kết quả 200, via là http-proxy, nhật ký của proxy ở cổng 8932 ghi GET http://shop.test:8931/l. Nghĩa là clj-http đã theo thuộc tính JVM. Điều này hữu ích khi muốn đổi proxy cho cả ứng dụng mà không sửa mã, nhưng cũng là rủi ro nếu ai đó đặt thuộc tính này trên máy chủ mà nhóm không biết.
Khóa trong request được ưu tiên
README ghi proxy theo request ghi đè http.nonProxyHosts. Trong mã, khi có :proxy-host, clj-http dùng proxy đó thay vì đọc thuộc tính hệ thống. Vì vậy cách an toàn cho tuyến nào cần proxy là luôn truyền khóa proxy rõ ràng, không dựa vào JVM.
Với tuyến nào bắt buộc đi thẳng, chuyện phức tạp hơn. Không truyền khóa proxy chưa đủ, vì clj-http có thể đọc thuộc tính JVM. Kiểm trên máy chủ thật xem các thuộc tính http.proxyHost, https.proxyHost có đang được đặt không, và thêm host nội bộ vào danh sách bỏ qua.
Danh sách bỏ qua với :proxy-ignore-hosts
README có khóa :proxy-ignore-hosts nhận danh sách host bỏ qua proxy, mặc định gồm localhost và 127.0.0.1. Mình thử đặt :proxy-ignore-hosts #{"shop.test"} cùng với khóa proxy. Request tới shop.test đi thẳng và báo UnknownHostException, đúng như mong đợi.
(def base-opts
(merge proxy-opts
{:proxy-ignore-hosts #{"localhost" "127.0.0.1" "inventory.internal"}
:connection-timeout 5000
:socket-timeout 15000}))
Khi tự đặt :proxy-ignore-hosts, nhớ giữ lại localhost và 127.0.0.1 nếu ứng dụng gọi dịch vụ cục bộ, vì giá trị mới thay thế giá trị mặc định. Bài no_proxy là gì giải thích thêm quy ước bỏ qua proxy ở các công cụ khác.
Clojure clj-http proxy với SOCKS
make-socks-proxied-conn-manager
README hướng dẫn dùng SOCKS bằng cách tạo connection manager qua clj-http.conn-mgr/make-socks-proxied-conn-manager với host và cổng, rồi truyền vào khóa :connection-manager của request. Hàm này còn nhận keystore và truststore nếu cần.
(require '[clj-http.conn-mgr :as conn-mgr])
(def socks-cm (conn-mgr/make-socks-proxied-conn-manager "127.0.0.1" 1080))
(http/get "http://127.0.0.1:8941/k2" {:connection-manager socks-cm})
Trong lab, gọi tới một địa chỉ IP qua proxy SOCKS5 mở thì chạy được, nhật ký SOCKS ghi CONNECT với kiểu địa chỉ ipv4. Hàm chỉ nhận host và cổng, mình không thấy tham số tài khoản SOCKS trong chữ ký mô tả ở README.
Tên miền được phân giải tại máy

SOCKS phân giải tên miền tại máy
Khi gọi http://shop.test qua cùng connection manager SOCKS, clj-http ném UnknownHostException: shop.test. Proxy SOCKS không ghi nhận kết nối nào. Nghĩa là tên miền được phân giải trên máy chạy clj-http trước khi gửi IP cho proxy.
Với crawler, đây là rò rỉ DNS: nhà mạng hoặc máy chủ DNS cục bộ thấy toàn bộ tên miền anh em truy cập, dù dữ liệu đi qua proxy. Nó cũng làm hỏng mọi tên miền chỉ phân giải được ở phía proxy. Bài DNS leak khi dùng proxy có cách kiểm bằng công cụ bên ngoài.
Khuyến nghị
Nếu gói proxy có cổng HTTP, dùng :proxy-host với cổng HTTP. Cách này có xác thực rõ ràng, chạy với cả HTTPS, và tên miền được gửi cho proxy trong request hoặc lệnh CONNECT. SOCKS qua connection manager hợp với proxy mở trong mạng nội bộ, không hợp với proxy thương mại có tài khoản.
Bài proxy HTTP và SOCKS5 so sánh hai loại kỹ hơn. Với clj-http, kết quả lab nghiêng hẳn về HTTP.
Chạy đối chứng Clojure clj-http proxy
Bộ request thử trên endpoint lab có xác thực
Brief yêu cầu một bộ request thử trên endpoint lab có xác thực. Đây là tệp mình chạy, rút gọn phần in kết quả. Hàm tryit in tên ca, mã trạng thái hoặc tên ngoại lệ.
(require '[clj-http.client :as http])
(defn tryit [n f]
(try (let [r (f)] (println n "OK" (:status r)))
(catch Exception e (println n "ERR" (.getName (class e)) (.getMessage e)))))
(def P {:proxy-host "127.0.0.1" :proxy-port 8912 :proxy-user "user1" :proxy-pass "p@ss#2026"})
(tryit "direct" #(http/get "http://shop.test:8911/a" {:socket-timeout 3000}))
(tryit "proxy" #(http/get "http://shop.test:8911/b" P))
(tryit "no-auth" #(http/get "http://shop.test:8911/c" {:proxy-host "127.0.0.1" :proxy-port 8912}))
(tryit "bad-pass" #(http/get "http://shop.test:8911/d" (assoc P :proxy-pass "sai")))
(tryit "post" #(http/post "http://shop.test:8911/e" (merge P {:body "{\"sku\":\"A1\"}" :content-type :json})))
(tryit "503" #(http/get "http://shop.test:8911/status/503" P))
(tryit "timeout" #(http/get "http://shop.test:8911/slow" (merge P {:socket-timeout 2000})))
(tryit "redirect" #(http/get "http://shop.test:8911/redir" P))
(tryit "ignore" #(http/get "http://shop.test:8911/i" (merge P {:proxy-ignore-hosts #{"shop.test"}})))
(tryit "dead" #(http/get "http://shop.test:8911/j" {:proxy-host "127.0.0.1" :proxy-port 8999}))
(tryit "https" #(http/get "https://example.com/" P))
Kết quả: direct báo UnknownHostException, proxy trả 200, no-auth và bad-pass báo status 407. Ca post trả 200 với thân {"sku":"A1"}, ca 503 báo status 503, ca timeout báo Read timed out. Ca redirect trả 200, ignore đi thẳng, dead báo Connection refused, còn https trả 200.
Redirect đi qua proxy
Đường dẫn /redir trả 302 sang /after-redirect. clj-http theo redirect, response cuối 200, khóa :trace-redirects chứa http://shop.test:8911/after-redirect. Nhật ký proxy ghi cả hai request, request sau cũng có tài khoản.
Điều này nghĩa là redirect giữ nguyên cấu hình proxy của request ban đầu. Với redirect sang host khác, cùng logic áp dụng, nhưng nếu host mới nằm trong :proxy-ignore-hosts thì sẽ đi thẳng.
POST và thân request
POST với :body chuỗi JSON và :content-type :json đi qua proxy, website nhận đúng thân. Nhật ký proxy ghi hai POST: lần đầu không tài khoản bị 407, lần sau có tài khoản. Apache gửi lại thân request ở lần thứ hai vì thân là chuỗi có thể đọc lại.
Với thân là luồng chỉ đọc được một lần, như InputStream lớn, cơ chế hỏi và đáp có thể gặp khó vì thân đã bị đọc ở lần đầu. Mình chưa thử trường hợp này. Nếu cần tải lên tệp lớn qua proxy có tài khoản, thử trước với đúng loại thân sẽ dùng.
Với clj-http, mỗi request có tài khoản tốn hai vòng tới proxy: lần đầu nhận 407, lần sau mới gửi tài khoản. Đó là hành vi bình thường của Apache HttpClient, không phải lỗi cấu hình.
Clojure clj-http proxy khi lấy dữ liệu từ website
Tiêu đề và User-Agent
Proxy chỉ đổi IP nguồn. Trong lab, mọi request clj-http đều gửi User-Agent dạng Apache-HttpClient kèm số phiên bản. Website nhận ra chuỗi này rất dễ, có thể chặn hoặc trả nội dung khác người dùng thật, dù IP proxy sạch.
Đặt tiêu đề bằng khóa :headers trong map request, ví dụ User-Agent và Accept-Language phù hợp với thị trường đang lấy dữ liệu. Gom các tiêu đề này vào external-opts để mọi lời gọi đều có. Thay đổi tiêu đề không làm proxy hoạt động khác, nó chỉ ảnh hưởng cách website nhìn request.
Cookie và phiên
clj-http có cookie store để giữ cookie giữa các request, dùng qua khóa :cookie-store với một store tạo từ namespace clj-http.cookies. Với website cần đăng nhập rồi mới lấy dữ liệu, dùng một cookie store cho mỗi phiên và một proxy cố định cho phiên đó.
Khi dùng proxy xoay đổi IP giữa chừng, cookie phiên gắn với IP cũ có thể làm website nghi ngờ hoặc buộc đăng nhập lại. Với luồng có đăng nhập, chọn proxy tĩnh hoặc phiên proxy xoay có thời gian giữ IP đủ dài. Với trang công khai không cần đăng nhập, cookie ít quan trọng hơn.
Giới hạn tốc độ
clj-http không tự giới hạn tốc độ. Khi dùng pmap hay future để gửi song song, số request đồng thời có thể vượt hạn mức luồng của gói proxy. Hạn chế số luồng bằng một bể luồng cố định, hoặc chia URL thành lô nhỏ, chờ lô trước xong mới gửi lô sau.
Theo dõi tỷ lệ 403 và 429 theo thời gian. Khi tỷ lệ tăng, giảm tốc trước, đổi IP sau. Hai mã này nghĩa là request đã qua proxy tới website, website đang từ chối, không phải proxy hỏng.
Đưa Clojure clj-http proxy vào dự án thật
Gom cấu hình vào một chỗ
Vì proxy nằm trong map request, cách an toàn là viết hàm bọc thay vì gọi http/get trực tiếp khắp nơi. Hàm bọc merge cấu hình mạng chung với map của từng lời gọi. Mã nghiệp vụ chỉ gọi hàm bọc.
(ns app.http
(:require [clj-http.client :as http]
[app.net :refer [proxy-opts]]))
(def external-opts
(merge proxy-opts
{:proxy-ignore-hosts #{"localhost" "127.0.0.1"}
:connection-timeout 5000
:socket-timeout 15000
:throw-exceptions false}))
(defn ext-get [url & [opts]]
(http/get url (merge external-opts opts)))
(defn int-get [url & [opts]]
(http/get url (merge {:connection-timeout 2000 :socket-timeout 5000} opts)))
ext-get luôn qua proxy, int-get dành cho dịch vụ nội bộ. Với int-get, nhớ kiểm thuộc tính JVM trên máy chủ như đã nói ở trên, vì không truyền khóa proxy chưa bảo đảm đi thẳng.
Connection manager và hiệu năng
Mỗi lời gọi không có :connection-manager sẽ dùng cơ chế mặc định của clj-http. Khi gọi nhiều request tới cùng host, dùng pooled connection manager giúp giữ kết nối và giảm số lần bắt tay. README có hướng dẫn with-connection-pool và cảnh báo rằng ở bản hiện tại, request bất đồng bộ dùng pool không chỉ định được connection manager.
Khi dùng pool qua proxy có tài khoản, vòng hỏi và đáp 407 vẫn có thể xảy ra ở mỗi kết nối mới. Kết nối giữ lại trong pool không cần xác thực lại cho request sau trên cùng kết nối, tùy cách proxy xử lý. Mình chưa đo con số cụ thể.
Thử lại có kiểm soát
README có khóa :retry-handler, một hàm nhận ngoại lệ, số lần thử và ngữ cảnh HTTP, trả true để thử lại. Đây là chỗ phù hợp để thử lại lỗi mạng tạm thời như ConnectException hay SocketTimeoutException qua proxy.
(defn retry-net [ex try-count _ctx]
(and (< try-count 3)
(or (instance? java.net.SocketTimeoutException ex)
(instance? java.net.ConnectException ex))))
(ext-get "https://api.example.com/items" {:retry-handler retry-net})
Chỉ thử lại thao tác an toàn khi gửi lại như GET. Với POST tạo đơn hàng, thử lại qua proxy chập chờn có thể tạo bản ghi trùng. Khi website trả 429, đừng thử lại ngay, đọc Retry-After và giãn nhịp. Bài lỗi 429 khi dùng proxy có cách xử lý.
Ghi log mà không lộ tài khoản
Map cấu hình của clj-http chứa :proxy-pass, nên in nguyên map ra log là lộ mật khẩu ngay. Lỗi này hay xảy ra khi ai đó thêm dòng in options để gỡ lỗi rồi quên xóa. Cách an toàn là viết hàm làm sạch, bỏ :proxy-pass và che :proxy-user trước khi ghi.
(defn safe-opts [opts]
(-> opts
(dissoc :proxy-pass)
(update :proxy-user #(when % (str (subs % 0 (min 2 (count %))) "***")))))
(defn log-call [url opts resp]
(println {:url url :proxy (:proxy-host opts) :status (:status resp) :opts (safe-opts opts)}))
Trong log, ghi tên máy proxy, host đích, mã trạng thái và thời gian là đủ để chẩn đoán. Khi một proxy bắt đầu trả 407 hàng loạt, log cho thấy ngay proxy nào và từ lúc nào. Khi một website bắt đầu trả 403, log cho thấy website nào và qua proxy nào.
Với ngoại lệ, ex-data của ExceptionInfo do clj-http ném có thể chứa cả request ban đầu. Đừng in nguyên ex-data. Lấy :status và các trường cần thiết, bỏ phần còn lại. Thư viện ghi log có cơ chế lọc khóa, nhưng hàm làm sạch tự viết dễ kiểm hơn.
Kiểm IP ra trước khi chạy tác vụ
Trước mỗi tác vụ lấy dữ liệu, gọi một dịch vụ trả IP công khai qua ext-get và so với IP máy chủ. Nếu trùng, dừng tác vụ ngay. Ca này bắt được gần hết lỗi quên khóa proxy, sai hàm bọc hay ai đó đổi cấu hình mà không báo.
Với proxy tĩnh, so IP ra với IP mà nhà cung cấp cam kết. Với proxy xoay, chỉ cần IP ra khác IP máy chủ. Ghi IP ra vào log khởi động để đối chiếu về sau khi website phản ánh truy cập bất thường.
Mình cũng khuyên chạy ca này trong pipeline CI với proxy cục bộ có nhật ký. Khi nâng clj-http hay Apache HttpClient, ca này kết hợp với bộ request thử ở trên cho biết ngay hành vi mạng có đổi hay không. Đó là cách rẻ nhất để không phải dò lỗi trên máy chủ thật, nơi mỗi lần thử sai đều tốn thời gian và có thể để lộ IP thật của máy chủ cho website đích.
Chọn loại proxy
clj-http hay dùng trong dịch vụ Clojure gọi API đối tác và tác vụ lấy dữ liệu. Với API yêu cầu IP cố định, proxy tĩnh phù hợp. Với lấy dữ liệu nhiều trang công khai, proxy xoay giúp chia tải. Proxy xác thực theo IP giúp bỏ vòng 407 ở đầu mỗi kết nối.
ProxyVN có proxy dân cư tĩnh, datacenter và proxy xoay, hỗ trợ HTTP và SOCKS5, cấp tài khoản user và mật khẩu. Nếu cần proxy HTTP để chạy hàm ext-get ở trên, anh em có thể mua proxy Việt Nam tại trang chủ ProxyVN rồi chạy lại bộ request thử.
Câu hỏi thường gặp về Clojure clj-http proxy
Mật khẩu có @ hay # có cần mã hóa không. Không. clj-http nhận :proxy-user và :proxy-pass là hai chuỗi riêng. Trong lab, mật khẩu p@ss#2026 gửi nguyên văn vẫn được proxy chấp nhận.
Vì sao nhật ký proxy có hai dòng cho một request. Apache HttpClient gửi request đầu không có tài khoản, nhận 407, rồi mới gửi tài khoản. Đây là hành vi bình thường.
HTTPS qua proxy có tài khoản có cần cờ JVM nào không. Với clj-http 3.x trong lab thì không. Thuộc tính jdk.http.auth.tunneling.disabledSchemes áp cho client của JDK, còn clj-http dùng Apache.
clj-http có đọc http.proxyHost không. Có, khi request không có :proxy-host. Trong lab, đặt thuộc tính bằng System/setProperty làm request đi qua proxy đó.
SOCKS có tài khoản dùng thế nào. Hàm make-socks-proxied-conn-manager trong README chỉ nhận host, cổng và tùy chọn keystore. Mình không thấy tham số tài khoản. Nên dùng cổng HTTP nếu gói proxy có.
Có nên tắt :insecure? khi lỗi chứng chỉ qua proxy không. Không nên. Proxy HTTP kiểu CONNECT không chạm vào chứng chỉ của website. Lỗi chứng chỉ chỉ khi qua proxy thường do mạng công ty giải mã HTTPS, cần nạp chứng chỉ gốc vào truststore.
Có thể dùng proxy khác nhau cho từng request không. Có, đó là thế mạnh của clj-http. Mỗi map request có thể mang :proxy-host và tài khoản riêng. Với nhiều proxy, tạo một vector các map cấu hình rồi chọn theo vòng hoặc theo tài khoản đang xử lý.
Proxy xoay đổi IP theo tên người dùng dùng thế nào. Thay :proxy-user theo mã phiên mà nhà cung cấp quy định. Vì clj-http đặt tài khoản theo request, đổi phiên chỉ cần đổi giá trị trong map, không cần tạo client mới.
Checklist Clojure clj-http proxy

Checklist Clojure clj-http proxy
Trước khi triển khai
- Hàm bọc gom cấu hình proxy, không gọi http/get trực tiếp với map rời rạc.
- Truyền :proxy-host rõ ràng cho tuyến cần proxy.
- Kiểm thuộc tính JVM trên máy chủ cho tuyến cần đi thẳng.
- Giữ localhost và 127.0.0.1 khi tự đặt :proxy-ignore-hosts.
- Đặt :connection-timeout và :socket-timeout bằng mili giây.
- Bắt cả ExceptionInfo có :status và ngoại lệ Java gốc.
- Không dùng SOCKS cho tên miền cần phân giải ở proxy.
- Thử lại chỉ cho GET và lỗi mạng tạm thời.
Giới hạn của lab
Lab chạy trên Java 8 với clj-http 3.13.1, proxy tự dựng. Chưa thử proxy kết nối bằng TLS, chưa thử NTLM, chưa thử request bất đồng bộ, chưa thử thân request dạng luồng lớn và chưa thử gateway thương mại. Với bản clj-http khác hoặc Apache HttpClient khác, chạy lại bộ request thử trước khi tin kết quả.
Clojure clj-http proxy và những điều cần nhớ
Clojure clj-http proxy đặt bằng :proxy-host, :proxy-port, :proxy-user, :proxy-pass ngay trong map request. Apache hỏi 407 rồi mới gửi tài khoản, HTTPS có tài khoản chạy được, SOCKS phân giải tên miền tại máy. Gom cấu hình vào hàm bọc và kiểm thuộc tính JVM. Cần proxy ổn định cho Clojure clj-http proxy, liên hệ Proxy.vn - Nhà cung cấp dịch vụ proxy chất lượng hàng đầu Việt Nam.