Go ile yazdığınız servis, isteklerini bir proxy üzerinden göndermek zorunda: başka bir ülkeden çıkış, iş ortağınızın whitelist'e aldığı sabit bir IP ya da tek adresin bütün istekleri taşımaması için bir havuz. Go bunun için ek paket istemez; net/http proxy desteğini SOCKS5 ve standart ortam değişkenleri dahil kendi içinde taşır. Sürprizler varsayılanlarda gizlidir: http.Get zaten HTTPS_PROXY değişkenini okur, localhost hiçbir zaman proxy'den geçmez, değişkenler süreç başına bir kez okunur ve bir HTTP/2 bağlantısı rotasyon için yazdığınız proxy fonksiyonunu sessizce atlayabilir.
Bu yazıda http.ProxyURL, ProxyFromEnvironment ve NO_PROXY kurallarını, Proxy-Authorization başlığını, Go'nun iki SOCKS5 yolunu, istek başına farklı proxy seçimini, zaman aşımlarını, yeniden denemeyi, hata metinlerinin birebir hâlini ve eksiksiz bir indirme programını anlatıyoruz. Her örnek Go 1.26.6 (golang.org/x/net v0.62.0) ile, user:pass isteyen yerel bir HTTP proxy ve yerel bir SOCKS5 sunucusu üzerinde çalıştırıldı; çıktılar ve kayıt satırları gerçektir, yalnız proxy adresi pr.proxynet.io:8000 yer tutucusuyla değiştirildi. Aynı işin Node.js tarafı Node.js'te Proxy Kullanımı yazısında.
net/http hangi proxy'yi kullanacağına nasıl karar verir?
Her istek bir http.Transport üzerinden geçer ve Transport'un Proxy alanı, isteği alıp proxy adresini (ya da doğrudan bağlantı için nil) döndüren bir fonksiyondur. Proxy türünü adresin şeması belirler: net/http Transport belgesine göre http, https, socks5 ve socks5h desteklenir, boş şema http sayılır ve adresteki kimlik bilgileri Proxy-Authorization başlığına dönüşür. http.Get'in arkasındaki http.DefaultTransport bu alana http.ProxyFromEnvironment koyar; kendi kurduğunuz Transport ise siz ayarlayana kadar proxy kullanmaz.
Sonrası hedefe göre değişir:
- Transport hedefe değil proxy'ye bağlanır; port yazılmamışsa
httpiçin 80,httpsiçin 443, SOCKS için 1080 kullanılır. - Düz
http://hedef. İstek, istek satırında tam URL ile proxy'ye gider ve her isteğeProxy-Authorizationeklenir. https://hedef. Go,Proxy-Authorizationbaşlığıyla birlikte bir kezCONNECT host:443gönderir; proxy200dediğinde TLS el sıkışması tünelin içinde yapılır, proxy yalnız host ve portu görür. CONNECT yöntemi RFC 9110'da tanımlıdır.socks5://proxy. Go SOCKS el sıkışmasını yapar, adreste kimlik bilgisi varsa oturum açar ve hedefin alan adını gönderir; DNS'i proxy çözer.- Bağlantı havuzda tutulur. Aynı hedefe aynı proxy üzerinden giden sonraki istek, HTTP/1.1 keep-alive ya da HTTP/2 ile aynı bağlantıyı kullanır ve yeniden kullanılan bağlantı
Proxyfonksiyonunu bir daha çağırmaz.
http.ProxyURL ile proxy nasıl ayarlanır?
http.ProxyURL, sabit bir adresi doğru biçimdeki fonksiyona sarar. Adresi bir kez ayrıştırın, Transport'a koyun ve Transport'u zaman aşımı tanımlı bir Client'a verin:
package main
import (
"cmp"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
// Bu satırı paneldeki endpoint oluşturucu verir. Git'e değil, ortam değişkenine yazın.
raw := cmp.Or(os.Getenv("PROXY_URL"), "http://user:pass@pr.proxynet.io:8000")
proxyURL, err := url.Parse(raw)
if err != nil {
panic(err)
}
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 20 * time.Second,
}
for _, target := range []string{"https://httpbin.org/ip", "http://httpbin.org/ip"} {
resp, err := client.Get(target)
if err != nil {
fmt.Println("error:", err)
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%s -> %d %s", target, resp.StatusCode, body)
}
}https://httpbin.org/ip -> 200 {
"origin": "203.0.113.42"
}
http://httpbin.org/ip -> 200 {
"origin": "203.0.113.42"
}Proxy tarafında iki istek farklı görünür: HTTPS hedef için bir CONNECT, HTTP hedef için tam URL ile iletilen bir GET, ikisi de kimlik bilgisiyle:
13:25:51.052 [http-proxy] CONNECT httpbin.org:443 proxy-authorization=Basic user -> 200 (tunnel #1)
13:25:51.563 [http-proxy] GET http://httpbin.org/ip proxy-authorization=Basic user user-agent=Go-http-client/1.1 -> forwardİki alışkanlık edinin: her yanıttan sonra resp.Body.Close(), yoksa bağlantı havuza dönemez; Client'ta Timeout, yoksa yanıt vermeyi kesen bir çıkış goroutine'i sonsuza kadar bekletir. Varsayılan User-Agent değeri Go-http-client/1.1'dir; hedefin programınızı tanıyabilmesi için kendi değerinizi yazın.
Go, HTTP_PROXY, HTTPS_PROXY ve NO_PROXY değişkenlerini nasıl okur?
http.ProxyFromEnvironment üç değişkeni ya da küçük harfli hâllerini her istekte uygular; kurallar, net/http'nin de kullandığı httpproxy paketinin belgesinde yazılıdır:
HTTP_PROXYyalnızhttp://hedeflere,HTTPS_PROXYyalnızhttps://hedeflere uygulanır. Karar hedefin şemasına göre verilir, proxy'ninkine göre değil;HTTPS_PROXYiçindehttp://ile başlayan bir proxy adresi normaldir.- Değer tam bir URL ya da
httpsayılan çıplak birhost[:port]olabilir; başka bir biçim isteği hatayla düşürür. - Büyük ve küçük harfli değişken birlikte tanımlıysa küçük harfli olan kazanır.
NO_PROXY, virgülle ayrılmış IP adresleri,10.0.0.0/8gibi CIDR aralıkları ve alan adlarını listeler; her birine port eklenebilir. Alan adı kendisiyle ve bütün alt alan adlarıyla eşleşir; nokta ile başlayan ad yalnız alt alan adlarıyla eşleşir; tek bir*proxy'yi tamamen kapatır.localhostve loopback adresleri hiçbir zaman proxy'den geçmez.
Aşağıdaki program hiçbir şey göndermeden ProxyFromEnvironment'a beş hedef için ne yapacağını sorar:
package main
import (
"fmt"
"net/http"
)
func main() {
targets := []string{
"https://httpbin.org/ip",
"http://httpbin.org/ip",
"https://api.internal.example.com/v1",
"http://10.0.0.7:8080/health",
"http://localhost:9000/",
}
for _, target := range targets {
req, _ := http.NewRequest(http.MethodGet, target, nil)
proxyURL, err := http.ProxyFromEnvironment(req)
switch {
case err != nil:
fmt.Printf("%-36s error: %v\n", target, err)
case proxyURL == nil:
fmt.Printf("%-36s direct\n", target)
default:
fmt.Printf("%-36s via %s\n", target, proxyURL.Redacted())
}
}
}HTTPS_PROXY ve HTTP_PROXY birlikte tanımlı, NO_PROXY=.example.com,10.0.0.0/8 iken:
https://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
http://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
https://api.internal.example.com/v1 direct
http://10.0.0.7:8080/health direct
http://localhost:9000/ directYalnız HTTP_PROXY tanımlıyken HTTPS hedefler doğrudan gider; bir Go programının "proxy'yi görmezden gelmesinin" en sık nedeni budur:
https://httpbin.org/ip direct
http://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
https://api.internal.example.com/v1 direct
http://10.0.0.7:8080/health via http://user:xxxxx@pr.proxynet.io:8000
http://localhost:9000/ directRedacted() parolayı xxxxx ile maskeler; kayıtlarda bunu kullanın.
Değişken kurallarında yazmayan bir kural daha var: net/http ortamı ilk istekte bir kez okur ve sürecin ömrü boyunca önbellekte tutar; sonradan os.Setenv ile yapılan değişiklik işe yaramaz:
before Setenv: <nil>
after Setenv, net/http: <nil>
after Setenv, httpproxy: http://user:xxxxx@pr.proxynet.io:8000Çalışma sırasında değişen ortamı görmek için, değişkenleri her çağrıda yeniden okuyan httpproxy.FromEnvironment().ProxyFunc() fonksiyonunu Transport.Proxy alanına koyun.
Go, proxy kullanıcı adı ve parolasını nasıl gönderir?
Proxy adresinde kullanıcı adı ve parola varsa Go Proxy-Authorization: Basic … başlığını kendisi gönderir. Düz http:// hedefte başlık her istekle gider, keep-alive bağlantıda bile: yeniden deneme testimizde aynı soket üzerinden giden dört GET'in her biri başlığı taşıdı. https:// hedefte ise yalnız CONNECT isteğinde bir kez gönderilir ve tünel, bağlantı yaşadığı sürece yeniden kullanılır.
Paroladaki @, : ya da / gibi karakterler yüzde kodlaması yapılmazsa adresi bozar; adresi parçalardan kurun:
proxyURL := &url.URL{
Scheme: "http",
Host: "pr.proxynet.io:8000",
User: url.UserPassword("user", "p@ss:word"), // ağa p%40ss%3Aword olarak kodlanmış gider
}Go'nun proxy'ye gönderdiği tek şema Basic'tir; NTLM ya da Kerberos için üçüncü taraf paket gerekir. Sağlayıcı gate'i Basic'i ya da IP whitelist'i kabul eder; whitelist'te adreste parola hiç bulunmaz, bu da sabit adresli sunucuya daha uygundur. İki yöntemi Proxy Kimlik Doğrulama: User:Pass ve IP Whitelist yazısında karşılaştırdık.
Go'da SOCKS5 proxy nasıl kullanılır?
İki yol var. Transport.Proxy bir socks5:// adresi kabul eder ve el sıkışmayı kendisi yapar; Go socks5'i tam olarak socks5h gibi ele alır, yani hedefin alan adını proxy çözer. golang.org/x/net/proxy paketi ise SOCKS5 dialer'ını size bir değer olarak verir; bunu Transport.DialContext alanına bağlarsınız ve HTTP olmayan bağlantılarda da kullanabilirsiniz.
package main
import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"time"
"golang.org/x/net/proxy"
)
func main() {
// SOCKS_URL=socks5://user:pass@pr.proxynet.io:<endpoint oluşturucunun gösterdiği SOCKS5 portu>
socksURL, err := url.Parse(os.Getenv("SOCKS_URL"))
if err != nil || socksURL.Host == "" {
log.Fatal("set SOCKS_URL to socks5://user:pass@host:port")
}
// 1. net/http socks5:// adresini kendisi işler. Alan adını proxy çözer.
viaTransport := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(socksURL)},
Timeout: 20 * time.Second,
}
show("Transport.Proxy", viaTransport)
// 2. golang.org/x/net/proxy SOCKS5 dialer'ını verir; her TCP bağlantısında kullanılabilir.
password, _ := socksURL.User.Password()
auth := &proxy.Auth{User: socksURL.User.Username(), Password: password}
dialer, err := proxy.SOCKS5("tcp", socksURL.Host, auth, &net.Dialer{Timeout: 10 * time.Second})
if err != nil {
log.Fatal(err)
}
viaDialer := &http.Client{
Transport: &http.Transport{DialContext: dialer.(proxy.ContextDialer).DialContext},
Timeout: 20 * time.Second,
}
show("x/net/proxy dialer", viaDialer)
}
func show(label string, client *http.Client) {
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
fmt.Printf("%-19s error: %v\n", label, err)
return
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%-19s %d %s", label, resp.StatusCode, body)
}Transport.Proxy 200 {
"origin": "203.0.113.42"
}
x/net/proxy dialer 200 {
"origin": "203.0.113.42"
}SOCKS5 sunucumuz iki istemci için de aynı üç adımı kaydetti: Go "kimlik doğrulama yok" ve "kullanıcı adı/parola" yöntemlerini önerdi, sunucu RFC 1929'da tanımlı kullanıcı adı/parola yöntemini seçti ve bağlantı isteği IP değil alan adı taşıdı:
13:26:07.490 [socks5] greeting methods=[0,2]
13:26:07.490 [socks5] auth user=user -> OK
13:26:07.490 [socks5] CONNECT domain httpbin.org:443Dialer yolunda Transport.Proxy alanını boş bırakın; dialer zaten proxy'den geçer. SOCKS5 TCP ve UDP taşır ama net/http yalnız TCP bağlantısı açar. Proxynet'in SOCKS5 Proxy ürünü HTTP portunun yanında kendi portunda, aynı kullanıcı adı ve parolayla çalışır.
Hangi yöntem hangi duruma uyar?
| Yöntem | Nereye yazılır | Kapsam | Not |
|---|---|---|---|
http.ProxyURL(u) | Transport.Proxy | o Transport | tek sabit proxy |
http.ProxyFromEnvironment | Transport.Proxy (DefaultTransport varsayılanı) | süreç | bir kez okunur; localhost atlanır |
kendi func(*http.Request) fonksiyonunuz | Transport.Proxy | istek başına | rotasyon, host'a göre kural |
socks5:// adresi | yukarıdaki üçünden biri | o Transport | DNS proxy'de çözülür |
x/net/proxy dialer'ı | Transport.DialContext | o Transport | ham TCP de; Proxy boş kalır |
Her istekte farklı proxy nasıl kullanılır?
Transport.Proxy sıradan bir fonksiyondur, her çağrıda farklı bir adres döndürebilir. Aşağıdaki örnek listeden sırayla seçer ve seçimini yazdırır:
var next atomic.Uint64
transport := &http.Transport{
// Liste üzerinde sırayla döner. Transport bir istek için bağlantı seçerken çağrılır.
Proxy: func(req *http.Request) (*url.URL, error) {
u := proxies[(next.Add(1)-1)%uint64(len(proxies))]
fmt.Printf("Proxy func: %s %s via %s\n", req.Method, req.URL, u.Redacted())
return u, nil
},
DisableKeepAlives: true, // her istekte yeni bağlantı, dolayısıyla yeni proxy seçimi
}Önemli olan son satır. HTTP/2 konuşan https://httpbin.org/ip adresine, listede iki proxy varken dört GET gönderdik; fonksiyonun kaç kez çağrıldığını ve proxy'lerin kaç tünel açtığını saydık:
| Transport ayarı | Proxy fonksiyonu çağrısı | Açılan tünel |
|---|---|---|
| Varsayılan (HTTP/2 anlaşıldı) | 1 | 1, yalnız ilk proxy |
Yalnız HTTP/1.1 (boş TLSNextProto haritası) | 4 | 2, proxy başına bir, yeniden kullanıldı |
DisableKeepAlives: true | 4 | 4, sırayla |
Varsayılan ayarlarla Go ilk proxy üzerinden tek bir HTTP/2 bağlantısı açtı ve dört isteği de onun içinden geçirdi; fonksiyona bir daha sorulmadı. HTTP/1.1 keep-alive ile her seferinde soruldu, ama üçüncü ve dördüncü istek kendi proxy'sine açık duran boş bağlantıyı yeniden kullandı. İstek başına bir tüneli yalnız DisableKeepAlives ya da req.Close = true verir. Rotating gate de aynı kurala uyar, çünkü çıkış IP'si tünel açılırken seçilir: Residential Proxy gate'i tek adres üzerinden her yeni tünelde çıkışı değiştirir, giriş yapılan akışlarda ise kullanıcı adındaki oturum parametresi aynı çıkışı 1-60 dakika tutar.
Zaman aşımı, bağlantı sınırları ve TLS
http.Client.Timeout sıfırda bırakılmaması gereken tek ayardır: bağlantıdan son bayta kadar isteğin tamamını kapsar. İnce ayarlar Transport'tadır: net.Dialer{Timeout: …} ile verilen DialContext proxy'ye bağlanmayı, TLSHandshakeTimeout tünel içindeki el sıkışmayı, ResponseHeaderTimeout başlıkların beklenmesini sınırlar; tek bir istek için http.NewRequestWithContext ile verilen süre sınırı aynı işi görür.
Kaç bağlantı açacağınızı üç alan belirler. MaxConnsPerHost host başına bağlantı sayısını sınırlar ve fazlasını bekletir; hedef başına ucuz bir hız sınırlayıcıdır. MaxIdleConnsPerHost varsayılan olarak 2'dir; daha çok işçiniz varsa biten bağlantılar havuza dönmek yerine kapanır, bu yüzden işçi sayısına eşitleyin. IdleConnTimeout boşta kalan bağlantıları kapatır.
TLS'te InsecureSkipVerify seçeneğine hiç uzanmayın. Proxy arkasında görülen x509: certificate signed by unknown authority hatası, kurumsal bir proxy'nin TLS'i kendi sertifika otoritesiyle incelediğini söyler; o CA'yı tls.Config.RootCAs alanına ekleyin. Sağlayıcı gate'i tünelin içindeki TLS'e hiç dokunmaz, orada hata hedefin sertifikasını işaret eder.
Eksiksiz örnek: yeniden deneme, bekleme ve eşzamanlılık sınırı
Aşağıdaki program parçaları birleştirir: yukarıdaki zaman aşımları ve havuz sınırları, ağ hatalarında, 429 ve 5xx yanıtlarında üstel bekleme ve rastgele paylı yeniden deneme, Retry-After başlığına uyma, 407'de yeniden denememe ve aynı anda en fazla dört istek:
// fetch.go - bir URL listesini proxy üzerinden zaman aşımı, yeniden deneme, bekleme ve eşzamanlılık sınırıyla indirir.
package main
import (
"cmp"
"context"
"errors"
"fmt"
"io"
"log"
"math/rand/v2"
"net"
"net/http"
"net/url"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
)
const (
maxAttempts = 4 // ilk deneme + 3 yeniden deneme
maxInFlight = 4 // aynı anda çalışan istek sayısı
)
var (
retryStatus = map[int]bool{429: true, 500: true, 502: true, 503: true, 504: true}
errProxyAuth = errors.New("proxy rejected the credentials (407): check user:pass or the IP whitelist")
)
func newClient(proxyURL *url.URL) *http.Client {
return &http.Client{
Timeout: 30 * time.Second, // isteğin tamamı: bağlantı, başlıklar ve gövde
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
MaxConnsPerHost: maxInFlight,
MaxIdleConnsPerHost: maxInFlight,
IdleConnTimeout: 60 * time.Second,
// CONNECT tünelindeki 407, "Proxy Authentication Required" metni yerine tipli bir hataya dönüşür.
OnProxyConnectResponse: func(_ context.Context, _ *url.URL, _ *http.Request, res *http.Response) error {
if res.StatusCode == http.StatusProxyAuthRequired {
return errProxyAuth
}
return nil
},
},
}
}
// backoff: 1, 2, 4 sn ... en çok 30 sn, artı 1 sn'ye kadar rastgele pay; işçiler aynı anda yeniden denemesin.
func backoff(attempt int) time.Duration {
d := min(time.Duration(1<<(attempt-1))*time.Second, 30*time.Second)
return d + time.Duration(rand.IntN(1000))*time.Millisecond
}
func sleep(ctx context.Context, d time.Duration) {
select {
case <-ctx.Done():
case <-time.After(d):
}
}
// fetch tek bir URL'yi GET ile alır. Ağ hataları, 429 ve 5xx yeniden denenir; 407 denenmez,
// çünkü kimlik bilgisi yanlıştır ve ikinci deneme bunu düzeltemez.
func fetch(ctx context.Context, client *http.Client, target string) ([]byte, int, error) {
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err != nil {
return nil, 0, err
}
req.Header.Set("User-Agent", "price-monitor/1.0 (+mailto:you@example.com)")
resp, err := client.Do(req)
if err != nil {
if errors.Is(err, errProxyAuth) || ctx.Err() != nil {
return nil, 0, err
}
lastErr = err
log.Printf("attempt %d/%d %s: %v", attempt, maxAttempts, target, err)
sleep(ctx, backoff(attempt))
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusProxyAuthRequired { // düz http hedefte 407 bir yanıt olarak gelir
return body, resp.StatusCode, errProxyAuth
}
if !retryStatus[resp.StatusCode] {
return body, resp.StatusCode, err // 200, 404, 301 ... kararı çağıran verir
}
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
if attempt == maxAttempts {
break
}
// Site yavaşlamamızı istedi. Kendi Retry-After değeri bizim takvimimizden önce gelir.
wait := backoff(attempt)
if secs, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
wait = time.Duration(secs) * time.Second
}
log.Printf("attempt %d/%d %s: HTTP %d, sleeping %s", attempt, maxAttempts, target, resp.StatusCode, wait.Round(100*time.Millisecond))
sleep(ctx, wait)
}
return nil, 0, lastErr
}
func main() {
proxyURL, err := url.Parse(cmp.Or(os.Getenv("PROXY_URL"), "http://user:pass@pr.proxynet.io:8000"))
if err != nil {
log.Fatal(err)
}
targets := os.Args[1:]
if len(targets) == 0 {
for i := 1; i <= 4; i++ {
targets = append(targets, fmt.Sprintf("https://httpbin.org/ip?n=%d", i))
}
targets = append(targets, "https://httpbin.org/status/503")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := newClient(proxyURL)
sem := make(chan struct{}, maxInFlight)
var wg sync.WaitGroup
var ok, failed atomic.Int32
for _, target := range targets {
wg.Add(1)
sem <- struct{}{} // maxInFlight istek çalışırken bekletir
go func() {
defer wg.Done()
defer func() { <-sem }()
started := time.Now()
body, status, err := fetch(ctx, client, target)
if err != nil {
failed.Add(1)
log.Printf("fail %s -> %v", target, err)
return
}
ok.Add(1)
log.Printf("ok %s -> %d, %d bytes in %s", target, status, len(body), time.Since(started).Round(100*time.Millisecond))
}()
}
wg.Wait()
log.Printf("done: %d ok, %d failed", ok.Load(), failed.Load())
}Beş URL, dört işçi, yeniden deneme yolunu çalıştırmak için hep 503 dönen bir adres:
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=1 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=2 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=4 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 attempt 1/4 https://httpbin.org/status/503: HTTP 503, sleeping 1.3s
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=3 -> 200, 32 bytes in 700ms
2026/10/28 16:30:58 attempt 2/4 https://httpbin.org/status/503: HTTP 503, sleeping 2.8s
2026/10/28 16:31:00 attempt 3/4 https://httpbin.org/status/503: HTTP 503, sleeping 4.8s
2026/10/28 16:31:05 fail https://httpbin.org/status/503 -> HTTP 503
2026/10/28 16:31:05 done: 4 ok, 1 failedÜç tasarım kararı var. OnProxyConnectResponse, proxy CONNECT'e yanıt verdiğinde, Go 200 dışındaki durumu düz bir hata metnine çevirmeden önce çalışır; orada döndürülen tipli hata errors.Is ile yeniden deneme döngüsünü durdurur. Yanlış parolayla aynı program hem HTTPS hem düz HTTP hedef için tek bir yeniden deneme yapmadan fail … -> proxy rejected the credentials (407) yazdı. Yeniden deneme kümesi bilerek küçük tutuldu: 404 ya da 403 olduğu gibi döner, çünkü başka bir çıkış sayfanın ne olduğunu değiştirmez. Retry-After: 2 gönderen yerel bir sunucuya karşı döngü kendi takvimi yerine denemeler arasında tam 2 saniye bekledi.
İsteğin gerçekten proxy'den geçtiği nasıl doğrulanır?
Gördüğü adresi geri söyleyen bir servise sorun; bir kez Proxy: nil ile, bir kez proxy ile. nil bilerek yazılır: Proxy değeri olmayan Transport ortamı hiç okumaz, bu yüzden HTTPS_PROXY tanımlı olsa bile ilk satır kendi adresinizdir.
direct := &http.Client{Transport: &http.Transport{Proxy: nil}, Timeout: 20 * time.Second}
proxied := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, Timeout: 20 * time.Second}
fmt.Println("without proxy:", exitIP(direct)) // exitIP, https://api.ipify.org?format=json adresini çekip "ip" alanını döndürür
fmt.Println("with proxy: ", exitIP(proxied))Gerçek bir gate ile ikinci satır çıkışın adresini gösterir. Test proxy'mizle iki satır aynı adresi yazdı, çünkü proxy aynı makinede çalışıyor; kanıt, proxy kaydındaki CONNECT api.ipify.org:443 satırıydı. Gerçek proxy ile iki satır aynıysa genellikle neden HTTPS_PROXY olmadan HTTP_PROXY, NO_PROXY içindeki bir hedef ya da Proxy değeri olmadan kurulmuş bir Transport'tur.
Colly'de durum nedir?
Go'nun en yaygın web tarama çatısı Colly, http.Transport üzerine kurulur; yukarıdakilerin hepsi orada da geçerlidir. proxy paketindeki RoundRobinProxySwitcher, http://, https:// ya da socks5:// adresleri alıp Transport.Proxy ile aynı imzaya sahip bir colly.ProxyFunc döndürür; Colly belgelerindeki proxy switcher örneğinde olduğu gibi c.SetProxyFunc(rp) ile bağlanır. Fonksiyon her istekte çağrılır, ama bir isteğin yeni tünel açıp açmayacağına yine rotasyon bölümündeki bağlantı yeniden kullanım kuralı karar verir.
Sık görülen hatalar ve anlamları
Mesajlar Go 1.26.6'nın testlerimizde yazdırdığı hâliyle, yer tutucu adresle:
Get "https://httpbin.org/ip": Proxy Authentication RequiredHTTPS hedefte, düz HTTP hedefte ise normal bir yanıt olarak407durum kodu: yanlışuser:pass, kodlanmamış özel karakter ya da whitelist'te olmayan bir IP. Nedenleri HTTP 407 Proxy Authentication Required yazısında topladık.proxyconnect tcp: dial tcp pr.proxynet.io:8000: …ve devamında işletim sisteminin bağlantı reddi metni (Linux ve macOS'taconnect: connection refused, Windows'taconnectex:ve yerelleştirilmiş bir cümle): o adreste dinleyen yok.proxyconnect, hatanın hedefe değil proxy'ye giderken oluştuğunu söyler.socks connect tcp pr.proxynet.io:8000->httpbin.org:443: unexpected protocol version 72: HTTP portunasocks5://adresi verilmiş; 72,HTTP/1.1içindekiHharfinin bayt değeridir.socks connect tcp …: username/password authentication failed: SOCKS5 sunucusu kimlik bilgisini reddetti.unexpected EOF: SOCKS5 portunahttp://adresi verilmiş, sunucuCONNECTisteğinde bağlantıyı kapatmış;socks4://gibi bilinmeyen bir şemahttpsayılır ve aynı şekilde biter.proxyconnect tcp: tls: first record does not look like a TLS handshake: düz HTTP portunahttps://proxy şeması verilmiş; sağlayıcı portun TLS konuştuğunu söylemiyorsahttp://kullanın.- Proxy hiç kullanılmıyor:
HTTPS_PROXYolmadanHTTP_PROXY,localhostolan ya daNO_PROXY'ye giren hedef, ilk istekten sonra tanımlanan değişkenler ya daProxydeğeri olmadan kurulmuş Transport.
Bu kurulum nerede kullanılır?
- Birden çok ülkeden fiyat ve stok kontrolü: proxy kullanıcı adında ülke parametresi, bağımsız ürün sayfaları için istek başına rotasyon.
- Whitelist'teki sabit adresten çıkan API istemcileri: iş ortağı tek bir statik IP'yi whitelist'e almıştır ve servis tam o adresten çıkmak zorundadır.
- Kendi ağınızın dışından izleme: sağlık kontrolü siteyi başka bir bölgedeki ziyaretçinin gördüğü gibi görür.
- Kurumsal ağdaki işçiler: internete yalnız şirket proxy'si üzerinden çıkan programlar.
Sık yapılan hatalar
- Yalnız
HTTP_PROXYtanımlamak. HTTPS hedefler doğrudan gider;HTTPS_PROXYde tanımlayın. - İlk istekten sonra ortamı değiştirmek. net/http önbellekler; süreci yeniden başlatın ya da
httpproxykullanın. - Keep-alive açıkken rotasyon beklemek. Yeniden kullanılan bağlantı
Proxyfonksiyonunu bir daha çağırmaz. resp.Body.Close()unutmak. Bağlantılar havuza hiç dönmez.Client.Timeouttanımlamamak. Sessiz bir çıkış goroutine'i sonsuza kadar bekletir.@ya da:içeren parolayı adres dizesine yapıştırmak. Adresiurl.UserPasswordile kurun.- Hem
Transport.Proxyhem SOCKS5DialContextayarlamak. Birini seçin. 407'yi yeniden denemek. İkinci deneme aynı yanlış bilgiyi gönderir.
Hangi yaklaşımı seçmeli?
| Durumunuz | Öneri |
|---|---|
| Tek proxy, kod değişmeden | HTTPS_PROXY ve HTTP_PROXY, iç host'lar için NO_PROXY |
| Tek proxy, kodda | Transport{Proxy: http.ProxyURL(u)} ve Client.Timeout |
| Her istekte yeni çıkış | Kendi Proxy fonksiyonunuz ya da rotating gate, DisableKeepAlives: true |
| Giriş akışında aynı çıkış | Kullanıcı adında oturum parametresi, keep-alive açık |
| DNS'i proxy'de çözen SOCKS5 | Transport.Proxy içinde socks5://user:pass@host:port |
| HTTP olmayan bağlantı için SOCKS5 | golang.org/x/net/proxy dialer'ı |
| Tek siteye çok işçi | MaxConnsPerHost ve MaxIdleConnsPerHost = işçi sayısı |
| Kendi CA'sı olan kurumsal proxy | CA tls.Config.RootCAs içinde, asla InsecureSkipVerify |
Sık sorulan sorular
Go'da http.Get proxy'yi otomatik kullanır mı?
Ortam öyle diyorsa evet: http.Get, Transport'unda Proxy alanı http.ProxyFromEnvironment olan http.DefaultClient'ı kullanır. Kendi kurduğunuz Transport ise proxy'siz başlar.
Bu GOPROXY ile aynı şey mi?
Hayır. GOPROXY, go komutuna modülleri nereden indireceğini söyler; programınızın çalışırken gönderdiği isteklerle ilgisi yoktur.
Yalnız tek bir istek için proxy nasıl ayarlanır?
O isteğe kendi Client ve Transport'unu verin ya da doğrudan gitmesi gereken istekler için nil döndüren bir Proxy fonksiyonu yazın. Aynı host'a havuzda açık bir bağlantı olabilir; req.Close = true yeni bağlantıyı zorlar.
Go socks5h destekliyor mu?
Evet, socks5 de aynı şekilde davranır: Go alan adını her zaman proxy'ye gönderir, çözümlemeyi proxy yapar; net/http'de yerel çözümlemeli SOCKS5 modu yoktur.
localhost neden hiç proxy'den geçmiyor?
ProxyFromEnvironment, değişkenler ne derse desin localhost ve loopback adresleri için proxy döndürmez; bu istekleri proxy'den geçirmek için ortam yerine http.ProxyURL kullanın.
Bir isteğin hangi proxy'yi kullandığını nasıl görürüm?
Proxy fonksiyonunu sarıp u.Redacted() değerini kaydedin ya da proxy adresiyle CONNECT yanıtını alan Transport.OnProxyConnectResponse alanını ayarlayın. Keep-alive açıkken seçim yalnız yeni bağlantı açıldığında kaydedilir.
Özet
Go'da proxy, http.Transport üzerindeki bir fonksiyondur: sabit adres için http.ProxyURL, standart değişkenler için http.ProxyFromEnvironment, istek başına seçim için kendi fonksiyonunuz. Adrese user:pass yazın, Proxy-Authorization başlığını Go kendisi gönderir; SOCKS5 için socks5:// kullanın; Client.Timeout ve havuz sınırlarını ayarlayın; 429 ve 5xx'i beklemeyle yeniden deneyin ama 407'yi asla; her istekte yeni çıkış gerekiyorsa keep-alive'ı kapatın. Bu ayarlarla çalışan residential, statik ISP, datacenter ve SOCKS5 uç noktalarını Proxynet proxy sayfasında bulabilirsiniz.




