Mert Özen Her Satırda Daha İleri
Geliştirici Araçları API Araçları — #06

HTTP Trafiğini Yakalamak: mitmproxy ve Fiddler ile Tarayıcının, HttpClient'ın ve Angular Dev Server'ın İsteklerini Görmek, Değiştirmek ve Tekrar Oynatmak

Mert Özen 15 Kas 2026 15 dk 5 görüntülenme
HTTP Trafiğini Yakalamak: mitmproxy ve Fiddler ile Tarayıcının, HttpClient'ın ve Angular Dev Server'ın İsteklerini Görmek, Değiştirmek ve Tekrar Oynatmak

Bookstore.Api'nin kargo API'sine giden HTTPS isteğini görmek için mitmproxy'yi 127.0.0.1'de çalıştırıp HttpClient'ı HTTPS_PROXY ile ona yönlendiriyor, CA'yı sisteme değil yalnız o client'a veriyoruz. Python addon ile cevabı 3 saniye geciktirip timeout ve retry'ı gerçek çıktıyla izliyor (kullanıcı 503 alırken kargoda üç teklif oluşuyor), filtre, kayıt ve replay'i, Angular dev server ile Chromium'un loopback istisnasını ve Fiddler'ın modlarını karşılaştırıyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; trafik mitmproxy 12.3 ile yakalanmış, resilience ayarları Microsoft.Extensions.Http.Resilience 10.11 ile kurulmuş, dev server örneği Angular 22 ile çalıştırılmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) checkout sayfası kargo ücretini gösterirken Bookstore.Api kargo firmasının API'sine bir POST /v1/quotes isteği atıyor. HttpClient'ın resilience ayarı şöyle: her deneme en fazla 2 saniye sürebilir, sonra en fazla 2 retry. Bir sabah kargo API'si her isteğe 3 saniyede cevap vermeye başlıyor. Cevaplar başarılı, sadece geç.

Kullanıcı "Kargo ücretini hesapla"ya bir kez bastığında kargo firmasının tarafında kaç fiyat teklifi oluşur ve kullanıcı ne görür? Sıfır mı, bir mi, üç mü? Tahminini yap; cevabı yavaş kargoyu proxy'de taklit ettiğimiz bölümde gerçek çıktıyla göreceğiz.

DevTools'un Göremediği Trafik

Tarayıcıdan çıkan istekleri Browser DevTools serisinin "Network Paneli: Waterfall, Timing, Throttling, HAR ve Request Override'ları" yazısında inceledik. Kargo ücretini soran istek ise tarayıcıdan çıkmıyor: tarayıcı Bookstore.Api'ye gidiyor, Bookstore.Api de sunucu tarafında kargo firmasına. Ödeme sağlayıcısına ve e-posta servisine giden çağrılar da böyle. Bu istekleri görmek için arada duran bir HTTP proxy gerekiyor: istemci isteği proxy'ye verir, proxy hedefe iletir ve ikisini de kaydeder.

Bu yazı için Bookstore.Api'ye küçük bir Shipping module'ü ekledim. CarrierClient typed client'ı kargo API'sine fiyat soruyor; adres, API key ve standard resilience handler ShippingModule'de ayarlanıyor:

services.AddHttpClient<CarrierClient>(http =>
    {
        http.BaseAddress = options.BaseAddress
            ?? throw new InvalidOperationException("Shipping:BaseAddress is missing.");
        http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.ApiKey);
    })
    .ConfigurePrimaryHttpMessageHandler(() => CreatePrimaryHandler(options, environment))
    .AddStandardResilienceHandler(resilience =>
    {
        // deneme başına 2 sn, en fazla 2 retry
        resilience.AttemptTimeout.Timeout = options.AttemptTimeout;
        resilience.Retry.MaxRetryAttempts = options.MaxRetryAttempts;
        resilience.Retry.Delay = TimeSpan.FromMilliseconds(200);
        resilience.TotalRequestTimeout.Timeout = options.AttemptTimeout * (options.MaxRetryAttempts + 2);
        resilience.CircuitBreaker.SamplingDuration = options.AttemptTimeout * 10;
    });

Pipeline'ın kendisi External Service Integration ve Resilience serisinin "Microsoft.Extensions.Http.Resilience ile Standard Pipeline: Varsayılanlar, Ayarlar ve Tuzaklar" yazısında anlatılıyor. Gerçek bir kargo firmasına bağlanamayacağımız için tools/fake-carrier/fake_carrier.py adında küçük bir sahte kargo API'si yazdım. 127.0.0.1 üzerinde 52682'de HTTP, 52683'te HTTPS dinliyor ve ilk açılışta kendi self-signed certificate'ını üretiyor.

HttpClient'ı proxy'ye yönlendirmek için kod değiştirmek gerekmiyor. Handler'ın UseProxy değeri varsayılan olarak açık ve kendine proxy verilmemiş her handler HttpClient.DefaultProxy'yi kullanıyor. Microsoft Learn'e göre DefaultProxy önce HTTP_PROXY, HTTPS_PROXY, ALL_PROXY ve NO_PROXY environment variable'larına bakıyor. Bunlar yoksa Windows'ta kullanıcının, macOS'ta sistemin proxy ayarını okuyor, Linux'ta proxy kullanmıyor. Proxy'yi yalnız API process'ine vermek için environment variable yeterli; PowerShell'de $env:HTTPS_PROXY = "http://127.0.0.1:52681".

mitmproxy'yi Yalnız Bu İş İçin Çalıştırmak

mitmproxy açık kaynaklı (MIT lisanslı), Python ile yazılmış bir proxy. Üç arayüzü var: terminalde etkileşimli mitmproxy, tarayıcıda açılan mitmweb ve log gibi akan mitmdump. Bu yazıdaki çıktılar mitmdump'tan. Kendi Python venv'ime pip ile kurdum; resmi sitede Windows installer'ı da var:

$ pip install mitmproxy
$ mitmdump --version
Mitmproxy: 12.3.1
Python:    3.12.14

Proxy'yi repo kökünde şöyle açıyorum:

$ mitmdump --listen-host 127.0.0.1 -p 52681 --set confdir=.mitmproxy \
    --set ssl_verify_upstream_trusted_ca=tools/fake-carrier/.certs/carrier-cert.pem \
    --flow-detail 3

Her seçenek bir sebeple orada. listen_host'un varsayılanı boş, yani mitmproxy bütün network arayüzlerinde dinliyor; aynı ağdaki biri senin proxy'n üzerinden trafik geçirebilir. Varsayılan 8080 portu çoğu makinede dolu olduğu için 52681'i seçtim. confdir, mitmproxy'nin ilk açılışta ürettiği CA'yı (Certificate Authority; certificate imzalayan kök) varsayılan ~/.mitmproxy yerine repo'daki gitignore'lu .mitmproxy/ klasörüne koyuyor. Orada altı dosya oluşuyor; mitmproxy-ca.pem private key'i de içeriyor ve kimseyle paylaşılmaz. Son seçenek proxy'nin sahte kargoya bağlanırken onun certificate'ına güvenmesini sağlıyor. ssl_insecure ile doğrulamayı tamamen kapatmaktan çok daha dar bir izin.

HTTPS'i Görmek: CA'yı Sisteme Değil İstemciye Vermek

06-tls-proxy-tr.png

Proxy bir HTTPS isteğinin içini ancak TLS'i (Transport Layer Security; HTTPS'in şifreli bağlantı katmanı) kendisi sonlandırırsa görebilir. HttpClient proxy'ye CONNECT 127.0.0.1:52683 der. mitmproxy istemciye kargo adına, kendi CA'sının imzaladığı bir certificate sunar ve kargoya ayrı bir TLS bağlantısı açar. Zincirin nasıl doğrulandığı SSL/TLS Certificate'ları ve HTTPS serisinin "Bir TLS Certificate'ın İçinde Ne Var? X.509, SAN, Chain of Trust ve Dosya Formatları" yazısında. İstemci mitmproxy'nin CA'sına güvenmiyorsa ilk bağlantı kurulamaz. API'yi yalnız HTTPS_PROXY ile başlatınca olan bu:

$ curl -s -w '\n%{http_code} %{time_total}s\n' $B/api/shipping/quotes \
    -H "Authorization: Bearer $TOKEN" --json '{"city":"Ankara","books":3}'
{"type":"https://tools.ietf.org/html/rfc9110#section-15.6.4","title":"Service Unavailable","status":503,"detail":"The carrier did not return a quote. Try again later.", ...}
503 0.333116s

# mitmdump
[14:06:40.631][127.0.0.1:54561] client connect
[14:06:40.632][127.0.0.1:54561] server connect 127.0.0.1:52683
[14:06:40.642][127.0.0.1:54561] client disconnect
[14:06:40.863][127.0.0.1:54563] client connect
...
[14:06:40.960][127.0.0.1:54565] client disconnect

# Bookstore.Api
System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.Security.Authentication.AuthenticationException: The remote certificate was rejected by the provided RemoteCertificateValidationCallback.

Proxy üç bağlantı görüyor, hiçbirinde istek yok: bir deneme ve iki retry, hepsi TLS'te reddediliyor. mitmproxy dokümanı CA'yı işletim sistemine ya da tarayıcıya kurmayı anlatıyor (mitm.it sayfası, Keychain, Windows certificate store). Bunu yapma. Sistemin güvendiği bir kök eklemek, o makinedeki bütün uygulamaların HTTPS trafiğini CA dosyasını ele geçiren herkese açar ve iş bitince silinmesi unutulur. Bookstore'da CA'yı yalnız kargo client'ına ve yalnız Development'ta veriyorum:

// AddShippingModule içinde: uygulama açılırken kontrol edilir
if (options.ExtraRootCertificates.Count > 0 && !environment.IsDevelopment())
{
    throw new InvalidOperationException("Shipping:ExtraRootCertificates is only allowed in Development.");
}

private static SocketsHttpHandler CreatePrimaryHandler(ShippingOptions options, IHostEnvironment environment)
{
    // UseProxy varsayılan olarak açık: HTTPS_PROXY, HTTP_PROXY ve NO_PROXY burada da geçerli.
    SocketsHttpHandler handler = new() { PooledConnectionLifetime = TimeSpan.FromMinutes(5) };
    if (options.ExtraRootCertificates.Count == 0)
    {
        return handler;
    }

    X509Certificate2Collection roots = [];
    foreach (string path in options.ExtraRootCertificates)
    {
        roots.ImportFromPemFile(Path.GetFullPath(path, environment.ContentRootPath));
    }
    handler.SslOptions.RemoteCertificateValidationCallback =
        (_, certificate, _, errors) => IsTrusted(certificate, errors, roots);
    return handler;
}

private static bool IsTrusted(X509Certificate? certificate, SslPolicyErrors errors, X509Certificate2Collection roots)
{
    if (errors == SslPolicyErrors.None)
    {
        return true;
    }
    // Yanlış host adı ya da eksik certificate asla kabul edilmez; yalnız zincir yeniden kontrol edilir.
    if (certificate is null || errors != SslPolicyErrors.RemoteCertificateChainErrors)
    {
        return false;
    }

    using X509Chain chain = new();
    chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
    chain.ChainPolicy.CustomTrustStore.AddRange(roots);
    chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
    using X509Certificate2 leaf = X509CertificateLoader.LoadCertificate(certificate.GetRawCertData());
    return chain.Build(leaf);
}

Callback isim uyuşmazlığını hiçbir zaman kabul etmiyor; yalnız zincir hatasında zinciri verilen köklerle yeniden kuruyor (CustomRootTrust). Development dışında listeye dosya verilirse uygulama açılırken hata verip duruyor. Listenin ilk elemanı appsettings.Development.json'daki sahte kargo certificate'ı; mitmproxy'nin CA'sını ikinci eleman olarak environment variable ile ekliyorum:

$ export HTTPS_PROXY=http://127.0.0.1:52681
$ export Shipping__ExtraRootCertificates__1=../../.mitmproxy/mitmproxy-ca-cert.pem
$ dotnet run --project src/Bookstore.Api -- --urls http://127.0.0.1:52680

Yol API projesinin klasörüne göre. API'yi 5080 yerine 52680'de açtım; $B bu adres, $TOKEN da Ada'nın login token'ı:

$ curl -s $B/api/shipping/quotes -H "Authorization: Bearer $TOKEN" --json '{"city":"Ankara","books":3}'
{"quoteId":"q-0003","city":"Ankara","books":3,"price":49.9,"currency":"TRY","estimatedDays":2}

# mitmdump
127.0.0.1:54598: POST https://127.0.0.1:52683/v1/quotes HTTP/1.1
    Host: 127.0.0.1:52683
    Authorization: Bearer fake-carrier-key-for-tests
    Transfer-Encoding: chunked
    traceparent: 00-caf1f662dbca40992fc170110e78843a-65af1a08c8a2213b-00
    Content-Type: application/json; charset=utf-8

    {
        "destinationCity": "Ankara",
        "parcels": 1,
        "weightGrams": 1350
    }

 << HTTP/1.0 200 OK 75b
    Server: BaseHTTP/0.6 Python/3.12.14
    Content-Type: application/json
    Content-Length: 75

    {
        "quoteId": "q-0003",
        "price": 49.9,
        "currency": "TRY",
        "estimatedDays": 2
    }

Artık her şey düz metin: kargonun API key'i, body'deki ağırlık, cevaptaki fiyat. Body chunked gidiyor, çünkü PostAsJsonAsync JSON'u önceden ölçmeden yazıyor; sahte kargonun ilk sürümü bunu okuyamamıştı, proxy'de görünce anlaşıldı. traceparent header'ını HttpClient kendisi ekliyor (W3C trace context; bir isteği servisler arasında izlemeye yarayan kimlik). Kargo firmasıyla bir sorunu konuşurken bu ID işe yarar.

Bu yöntemin iki sınırı var. Certificate pinning yapan bir istemci (beklediği certificate'ı ya da public key'i kodda sabitleyen uygulama, çoğunlukla mobil) CA'ya güvense bile proxy'nin certificate'ını reddeder; mitmproxy dokümanı böyle trafiği ignore_hosts ile çözmeden geçirmeyi öneriyor. HSTS (HTTP Strict Transport Security; tarayıcıya bir siteye yalnız HTTPS ile gitmesini söyleyen header) ise tarayıcıda certificate uyarısını geçme seçeneğini kaldırır. Tarayıcı CA'ya güvenmiyorsa HSTS'li bir sitede "yine de devam et" seçeneği çıkmaz. Ayrıntısı SSL/TLS serisinin "HTTPS'i Zorunlu Kılmak: HTTP→HTTPS Redirect, HSTS'i Kademeli Açmak, Preload ve Mixed Content" yazısında.

Yavaş Kargo: Addon ile Cevabı Geciktirmek

mitmproxy'yi asıl kullanışlı yapan addon'lar: request, response gibi event'lerde çalışan Python sınıfları. Kargonun yavaşladığı sabahı taklit eden addon tools/mitm/slow_carrier.py:

"""mitmproxy addon: holds the carrier's quote responses to simulate a slow carrier.

    mitmdump -s tools/mitm/slow_carrier.py --set carrier_delay_ms=3000 --set carrier_slow_count=1
"""
import asyncio
import logging

from mitmproxy import ctx, http


class SlowCarrier:
    def __init__(self) -> None:
        self.slowed = 0

    def load(self, loader) -> None:
        loader.add_option("carrier_delay_ms", int, 0, "Milliseconds to hold each /v1/quotes response.")
        loader.add_option("carrier_slow_count", int, 0, "How many responses to hold (0 = all).")

    async def response(self, flow: http.HTTPFlow) -> None:
        if ctx.options.carrier_delay_ms <= 0 or not flow.request.path.startswith("/v1/quotes"):
            return
        if ctx.options.carrier_slow_count and self.slowed >= ctx.options.carrier_slow_count:
            return
        self.slowed += 1
        logging.info("slow_carrier: holding response #%d for %d ms", self.slowed, ctx.options.carrier_delay_ms)
        # async bekleme: proxy'deki diğer bağlantılar durmaz
        await asyncio.sleep(ctx.options.carrier_delay_ms / 1000)


addons = [SlowCarrier()]

İlk denemede gecikmeyi saniye olarak float tanımlamıştım. mitmproxy 12.3 açılışta "Failed to parse option carrier_delay: unsupported option type: <class 'float'>" yazıp çıktı; option tipleri arasında float yok. Bu yüzden milisaniyeyi int olarak alıyorum. Bütün cevapları 3 saniye tutunca:

$ mitmdump --listen-host 127.0.0.1 -p 52681 --set confdir=.mitmproxy \
    --set ssl_verify_upstream_trusted_ca=tools/fake-carrier/.certs/carrier-cert.pem \
    -s tools/mitm/slow_carrier.py --set carrier_delay_ms=3000
[14:07:32.792][127.0.0.1:54619] client connect
[14:07:32.794][127.0.0.1:54619] server connect 127.0.0.1:52683
[14:07:32.814] slow_carrier: holding response #1 for 3000 ms
[14:07:34.794][127.0.0.1:54619] client disconnect
[14:07:35.043][127.0.0.1:54622] client connect
[14:07:35.053] slow_carrier: holding response #2 for 3000 ms
127.0.0.1:54619: POST https://127.0.0.1:52683/v1/quotes HTTP/1.1
     << HTTP/1.0 200 OK 75b
[14:07:37.046][127.0.0.1:54622] client disconnect
[14:07:37.344][127.0.0.1:54624] client connect
[14:07:37.360] slow_carrier: holding response #3 for 3000 ms
127.0.0.1:54622: POST https://127.0.0.1:52683/v1/quotes HTTP/1.1
     << HTTP/1.0 200 OK 75b
[14:07:39.346][127.0.0.1:54624] client disconnect
127.0.0.1:54624: POST https://127.0.0.1:52683/v1/quotes HTTP/1.1
     << HTTP/1.0 200 OK 75b

$ curl -s -w '\n%{http_code} %{time_total}s\n' $B/api/shipping/quotes -H "Authorization: Bearer $TOKEN" --json '{"city":"Ankara","books":3}'
{"type":"https://tools.ietf.org/html/rfc9110#section-15.6.4","title":"Service Unavailable","status":503, ...}
503 6.561202s

# Bookstore.Api
fail: Polly[0]
      Resilience event occurred. EventName: 'OnTimeout', Source: 'CarrierClient-standard//Standard-AttemptTimeout', Operation Key: '', Result: ''
warn: Polly[3]
      Execution attempt. Source: 'CarrierClient-standard//Standard-Retry', Operation Key: '', Result: 'The operation didn't complete within the allowed timeout of '00:00:02'.', Handled: 'True', Attempt: '0', Execution Time: 2012.434ms
...
fail: Polly[3]
      Execution attempt. Source: 'CarrierClient-standard//Standard-Retry', Operation Key: '', Result: 'The operation didn't complete within the allowed timeout of '00:00:02'.', Handled: 'True', Attempt: '2', Execution Time: 2005.1285ms
warn: Bookstore.Shipping.CarrierClient[0]
      Carrier quote failed: TimeoutRejectedException

Tahmin sorusunun cevabı: kullanıcı 6,5 saniye bekleyip 503 görüyor, kargo tarafında ise üç fiyat teklifi oluşuyor. API'nin logu yalnız üç timeout gösteriyor. Proxy logu ise hikâyenin öbür yarısını gösteriyor: her bağlantıda önce "client disconnect" geliyor (HttpClient 2. saniyede vazgeçti), bir saniye sonra kargonun "200 OK" cevabı. Kargo işini yaptı ama cevabı bekleyen kalmadı. Fiyat sormak zararsız, ancak aynı pipeline gönderi ya da ödeme oluşturan bir POST'u da üç kez çağırır: standard handler'ın retry'ı varsayılan olarak POST'u da tekrarlıyor. DisableForUnsafeHttpMethods() bunu kapatıyor. Yan etkisi olan çağrıları güvenle tekrarlamanın yolu Idempotency-Key; External Service Integration ve Resilience serisinin "Idempotent Dış Çağrılar: Idempotency-Key Göndermek, Bilinmeyen Sonuç ve Reconciliation" yazısında.

Yalnız ilk cevabı geciktirince (--set carrier_slow_count=1) retry işe yarıyor:

$ curl -s -w '\n%{http_code} %{time_total}s\n' $B/api/shipping/quotes -H "Authorization: Bearer $TOKEN" --json '{"city":"Ankara","books":3}'
{"quoteId":"q-0008","city":"Ankara","books":3,"price":49.9,"currency":"TRY","estimatedDays":2}
200 2.222351s

# Bookstore.Api
warn: Polly[3]
      Execution attempt. Source: 'CarrierClient-standard//Standard-Retry', Operation Key: '', Result: 'The operation didn't complete within the allowed timeout of '00:00:02'.', Handled: 'True', Attempt: '0', Execution Time: 2005.6902ms
      Received HTTP response headers after 20.5252ms - 200
info: Polly[3]
      Execution attempt. Source: 'CarrierClient-standard//Standard-Retry', Operation Key: '', Result: '200', Handled: 'False', Attempt: '1', Execution Time: 20.915ms

Kullanıcı fiyatı 2,2 saniyede alıyor. Kargo tarafında yine iki teklif var: q-0007 ilk denemede oluşup kayboldu, kullanıcıya q-0008 döndü.

Filtre, Kayıt ve Replay

Gerçek bir uygulamanın trafiğinde kargo isteği onlarca isteğin arasında kaybolur. mitmdump'a en sona bir filtre ifadesi verirsen yalnız eşleşen flow'ları işler: ~u URL'de, ~m method'da, ~h header'da arar; &, | ve ! ile birleşir. -w flow'ları dosyaya yazar:

$ mitmdump --listen-host 127.0.0.1 -p 52681 --set confdir=.mitmproxy \
    --set ssl_verify_upstream_trusted_ca=tools/fake-carrier/.certs/carrier-cert.pem \
    -s tools/mitm/slow_carrier.py --set carrier_delay_ms=3000 --set carrier_slow_count=1 \
    -w carrier.mitm "~u /v1/quotes"
[14:07:52.068] Only processing flows that match "~u /v1/quotes"

Kaydedilen istekleri -C (client replay) ile yeniden gönderebilirsin; -n proxy'yi açmadan yalnız replay yapıp çıkar:

$ mitmdump -n -C carrier.mitm --set confdir=.mitmproxy \
    --set ssl_verify_upstream_trusted_ca=tools/fake-carrier/.certs/carrier-cert.pem --flow-detail 2
[14:08:09.458] [replay] server connect 127.0.0.1:52683
[replay]: POST https://127.0.0.1:52683/v1/quotes HTTP/1.1
    Host: 127.0.0.1:52683
    Authorization: Bearer fake-carrier-key-for-tests
    Transfer-Encoding: chunked
    traceparent: 00-bc6f6b9c8f6fe1212346e7019a2e7565-587c8259387abb92-00
    Content-Type: application/json; charset=utf-8
 << HTTP/1.0 200 OK 75b
...
$ grep -a -o "Bearer fake-carrier-key-for-tests" carrier.mitm | sort | uniq -c
   2 Bearer fake-carrier-key-for-tests

Replay, düzeltmeden sonra aynı isteği yeniden göndermek ya da kargo firmasına "bu istek bu cevabı aldı" diye göstermek için kullanışlı. Son komut dosyanın ne taşıdığını hatırlatıyor: API key iki kez, düz metin olarak. Gerçek bir ortamda bu satır gerçek bir key olurdu.

Arayüz isteyenler için mitmweb aynı seçeneklerle çalışıp flow'ları tarayıcıda listeliyor. 12.x'te web arayüzü açılışta bir token üretiyor; token'sız istek 403 alıyor:

$ mitmweb --listen-host 127.0.0.1 -p 52681 --web-host 127.0.0.1 --web-port 52685 --no-web-open-browser --set confdir=.mitmproxy
[14:08:28.392] HTTP(S) proxy listening at 127.0.0.1:52681.
[14:08:28.394] Web server listening at http://127.0.0.1:52685/?token=<token>
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:52685/
403

Angular Dev Server ve Tarayıcı

bookstore-web geliştirilirken ng serve /api isteklerini proxy.conf.json ile Bookstore.Api'ye iletiyor. Tarayıcı dev server'a ne gönderdiğini görür, ama dev server'ın backend'e ne gönderdiğini görmez. Arayı görmek için mitmproxy'yi reverse proxy modunda API'nin önüne koyup dev server'ı ona yönlendirdim. Ekibin proxy.conf.json'u değişmesin diye ayrı bir dosya kullandım:

// web/bookstore-web/proxy.mitm.conf.json
{
  "/api": {
    "target": "http://127.0.0.1:52686",
    "secure": false
  }
}
$ mitmdump --listen-host 127.0.0.1 --mode reverse:http://127.0.0.1:52680@52686 --set confdir=.mitmproxy --flow-detail 1
[14:08:44.322] reverse proxy to http://127.0.0.1:52680 listening at 127.0.0.1:52686.
$ npx ng serve --host 127.0.0.1 --port 52687 --proxy-config proxy.mitm.conf.json
  ➜  Local:   http://127.0.0.1:52687/
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:52687/api/auth/login \
    --json '{"email":"ada@example.com","password":"Bookstore-Demo-1"}'
200

# mitmdump
127.0.0.1:54682: POST http://127.0.0.1:52680/api/auth/login
              << 200 OK 390b

Tarayıcının kendisini proxy'ye vermek için Chromium'u --proxy-server=http://127.0.0.1:52681 ve geçici bir profil klasörüyle açmak yeterli; sistem proxy ayarına dokunmak gerekmiyor. Yalnız Chromium localhost ve 127.0.0.1 isteklerini varsayılan olarak proxy'ye göndermiyor. Playwright'ın indirdiği Chromium ile API'nin /openapi/v1.json adresini iki kez açtım. İlkinde sayfa geldi ama proxy hiçbir şey görmedi. İkincisinde --proxy-bypass-list=<-loopback> ekledim; bu, loopback istisnasını kaldırıyor:

# mitmdump, ikinci açılış
127.0.0.1:54706: GET http://127.0.0.1:52680/openapi/v1.json
              << 200 OK 22.9k

.NET'te durum farklıydı: HTTPS_PROXY verilen API, 127.0.0.1'deki kargoya giden isteği proxy'ye gönderdi. HTTPS bir sayfayı bu geçici tarayıcıda açarken --ignore-certificate-errors (Playwright'ta ignoreHTTPSErrors) certificate hatasını yalnız o tarayıcı için yok sayar.

Fiddler: Sistem Proxy'si ve Rules

Bu bölümde resmî belgelerden yararlanılmıştır.

Windows'ta HTTP trafiği denince çoğu ekibin aklına Fiddler gelir ve iki ürün var. Fiddler Classic yalnız Windows'ta çalışıyor; Telerik'in sayfasına göre ücretsiz ama yalnız ticari olmayan kullanım için, bakım modunda ve aktif olarak geliştirilmiyor. Fiddler Everywhere Windows, macOS ve Linux'ta çalışan, hesapla giriş yapılan abonelikli ürün. Fiyat sayfasında ücretsiz plan yok; 15 Kasım 2026 itibarıyla en ucuz plan Lite, yıllık ödemede kullanıcı başına aylık 7 dolar.

Fiddler Everywhere'in yakalama modları dokümanda ayrı ayrı anlatılıyor. System capturing Fiddler'ı işletim sisteminin proxy'si yapıyor; en geniş ve en müdahaleci mod bu. .NET'in DefaultProxy'si Windows'ta kullanıcının proxy ayarını okuduğu için HttpClient trafiği de buraya düşüyor. Explicit capturing sistem ayarına dokunmuyor ve yalnız Fiddler'ın proxy'sine (varsayılan 127.0.0.1:8866) yönlendirdiğin uygulamaları yakalıyor; Bookstore.Api için HTTPS_PROXY=http://127.0.0.1:8866 vermek yukarıdaki kurguyla aynı şey. Terminal capturing proxy'si ayarlı bir terminal, independent browser capturing ayrı bir Chromium açıyor. HTTPS'i çözmek için Fiddler'ın kök certificate'ına güvenilmesi gerekiyor; uygulama bunu sisteme kurmayı öneriyor ve yukarıdaki riskler aynen geçerli.

mitmproxy addon'unun Fiddler'daki karşılığı Rules sekmesi. Fiddler Classic'teki AutoResponder'ın yerini alan Rules ile bir URL'e uyan isteklere elle yazılmış cevap (Manual Response) dönebilir ya da Delay Request aksiyonuyla isteği milisaniye cinsinden geciktirebilirsin. Fiddler'ın .NET dokümanı localhost isteklerinin proxy'yi atlayabileceğini söyleyip localhost. (sonda noktayla) gibi çözümler veriyor.

İhtiyaçmitmproxyFiddler EverywhereBrowser DevTools
Backend'in giden çağrılarıProxy ileProxy ile (system ya da explicit)Göremez
HTTPS içeriğiCA istemciye verilirKök certificate'a güvenilirTarayıcı zaten çözer
Cevabı değiştirmek, geciktirmekPython addonRulesLocal overrides, throttling
ArayüzTerminal ve webDesktop uygulamasıTarayıcının içinde
LisansAçık kaynak (MIT)Ücretli abonelikTarayıcıyla gelir

Bookstore ekibi için seçimim mitmproxy: script'lenebiliyor, addon'lar repo'da duruyor ve sistem ayarına dokunmadan tek process'e bağlanabiliyor. Windows'ta arayüzle çalışmayı seven ve lisansı olan biri için Fiddler Everywhere'in explicit capturing modu aynı işi görür.

Üretimde Dikkat

Bu araçlar geliştirme makinesi içindir. Production'da araya bir proxy koymak bütün kullanıcıların token'larını ve kişisel verilerini bir dosyaya yazmak demek. Production'daki dış çağrıları görmenin doğru yolu HttpClient'ın kendi logları ve trace'leri: yukarıdaki System.Net.Http.HttpClient.CarrierClient log kategorileri ve Polly event'leri. İçerik loglamak gerekiyorsa alanları seçerek ve hassas değerleri maskeleyerek yap.

ExtraRootCertificates gibi bir güven genişletmesini mutlaka ortama bağla. Bookstore'da liste Development dışında doluysa uygulama açılmıyor. Bir config hatasının production'a TLS doğrulamasını sessizce gevşetmiş olarak çıkmasındansa açılmayan bir uygulama daha iyi. Aynı sebeple DangerousAcceptAnyServerCertificateValidator ya da her zaman true dönen bir callback kodda hiç olmamalı. HttpClient'ın certificate doğrulaması SSL/TLS serisinin ".NET'te ve Kestrel'de Certificate: dev-certs, PEM/PFX Yükleme, SNI, Yenilemede Reload ve HttpClient Doğrulaması" yazısında.

CA dosyalarının ömrünü yönet. .mitmproxy/ gitignore'da; iş bitince klasörü sil. mitmproxy her yeni confdir'de yeni bir CA üretiyor, yani eski CA'ya güvenen bir istemci bırakmamak yeterli.

Sık Düşülen Tuzaklar

Proxy'yi kapatıp environment variable'ı açık unutmak. Addon hatası yüzünden mitmdump açılmadan kapandığında API HTTPS_PROXY ile çalışmaya devam ediyordu:

info: System.Net.Http.HttpClient.CarrierClient.ClientHandler[100]
      Sending HTTP request POST https://127.0.0.1:52683/v1/quotes
info: System.Net.Http.HttpClient.CarrierClient.ClientHandler[104]
      HTTP request failed after 8.819ms
      System.Net.Http.HttpRequestException: Connection refused (127.0.0.1:52681)

İstek kargoya gidiyor ama hata mesajında proxy'nin adresi var; ilk bakışta "kargo çöktü" sanılıyor. Sistem proxy'sini değiştiren bir araçta aynı hata bütün uygulamalarda görünür. Aracı kapattıktan sonra sistem ayarının geri döndüğünü kontrol et.

Yakalanan trafiği paylaşmak. .mitm dosyası da HAR (tarayıcının kaydettiği istek arşivi) gibi header'ları ve body'leri olduğu gibi taşıyor. Bir hatayı paylaşırken yalnız ilgili flow'u, token ve kişisel veriler silinmiş halde paylaş; dosyayı repo'ya, issue'ya ya da sohbet kanalına koyma. Bookstore'da *.mitm gitignore'da.

CA'yı sisteme kurup unutmak. En sık yapılan ve en pahalı hata bu; CA'yı yalnız ihtiyacı olan istemciye ver.

--listen-host vermemek. Varsayılan bütün arayüzler; kafede ya da ofis ağında proxy'n başkalarına açık kalır.

Retry'ı yalnız sayıyla değerlendirmek. Logda "Attempt: 2" görüp geçmek kolay. Proxy'de kargonun üç cevabının da 200 olduğunu görmek, sorunun kargoda değil timeout ayarında olduğunu gösteriyor: kargo 3 saniyede cevap veriyorsa 2 saniyelik deneme süresi her isteği boşa harcıyor.

Küçük Bir Deneme

★☆☆ Sahte kargoyu, mitmdump'ı ve API'yi açıp kargo isteğini --flow-detail 3 ile yakala. Sonra Shipping__ExtraRootCertificates__1'i kaldırıp API'yi yeniden başlat ve proxy logunda neyin değiştiğine bak.

★★☆ slow_carrier.py'ye bir option ekle: belirli bir şehir için cevabın status code'unu 503 yapsın (flow.response.status_code). Standard handler'ın 503'ü retry edip etmediğini ve kaç deneme yaptığını logdan say.

★★★ Kargo client'ında POST için retry'ı kapat (DisableForUnsafeHttpMethods()), yavaş kargo senaryosunu yeniden koş ve kargo tarafında oluşan teklif sayısını karşılaştır. Ardından isteğe bir Idempotency-Key header'ı ekle ve addon'da aynı key ile gelen ikinci isteği yakala.

Bu yazıda dışarı giden trafiği gördük. Serinin son yazısında yön tersine dönüyor: ödeme sağlayıcısının webhook'unu tünelle localhost'taki Bookstore.Api'ye getireceğiz, henüz hazır olmayan servisleri de Prism ve WireMock.Net ile taklit edeceğiz.