Mert Özen Her Satırda Daha İleri
Backend API Gateway ve YARP — #08

YARP'ın Sürüm Geçmişi: 1.0'dan 2.3'e Değişenler ve Eski Konfigürasyonu Okumak

Mert Özen 14 Kas 2026 11 dk 15 görüntülenme
YARP'ın Sürüm Geçmişi: 1.0'dan 2.3'e Değişenler ve Eski Konfigürasyonu Okumak

YARP 2.3 (Şubat 2025) Kasım 2026'da hâlâ son sürüm; 1.0 öncesi config'teki array route'lar ve eski policy adları uygulamayı başlatmaz, ama eski HttpRequest.Timeout gibi alanlar sessizce yok sayılır. 1.0'dan 2.3'e değişen varsayılanları release notlarından derliyor, eski bir config'i 2.3 ve JSON schema ile okuyor, Ocelot tabanlı bir gateway'i YARP'a taşıyıp iki tarafı aynı isteklerle karşılaştırıyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Yarp.ReverseProxy 2.3 ve Ocelot 25.0 ile derlenip denenmiştir; sürüm bilgileri projenin GitHub release notlarından alınmıştır.

Önce Tahmin Et

Elinde YARP (Yet Another Reverse Proxy; .NET için reverse proxy library'si) 1.0 öncesinden kalma bir konfigürasyon var. Route'lar bir array, her birinin RouteId alanı var; cluster'da "LoadBalancingPolicy": "First" ve "HttpRequest": { "Timeout": "00:00:05" } yazıyor. Bu dosyayı YARP 2.3 ile çalıştırınca üç eski ayardan hangileri uygulamayı başlatmaz, hangisi sessizce yok sayılır?

1.0'a Giden Yol: Preview'lar ve İsim Değişiklikleri

YARP'ın ilk preview'ı Haziran 2020'de Microsoft.ReverseProxy adıyla çıktı. 1.0'a kadar on iki preview ve bir RC (Release Candidate; son sürümden önceki aday sürüm) yayımlandı ve bu sürede konfigürasyon modeli birkaç kez değişti. İnternette ya da eski bir repo'da bulduğun bir YARP örneğini okurken en çok bu dönemin isimleri kafa karıştırıyor. GitHub release notlarından derlediğim önemli değişiklikler:

EskiYeniSürüm
BackendsClusters1.0 preview 2 (2020-06)
Route match'te HostHosts (liste)1.0 preview 3 (2020-07)
Microsoft.ReverseProxy paketiYarp.ReverseProxy1.0 preview 10 (2021-03)
Routes bir array, RouteId alanıRoutes bir dictionary, route ID'si anahtar1.0 preview 11 (2021-04)
ProxyRoute, Cluster, DestinationRouteConfig, ClusterConfig, DestinationConfig1.0 preview 12 (2021-06)
IHttpProxyIHttpForwarder1.0 preview 12
First load balancing policyFirstAlphabetical1.0 preview 12
HttpRequest.TimeoutHttpRequest.ActivityTimeout1.0 RC 1 (2021-10)

ActivityTimeout değişikliği yalnız bir isim değişikliği değil. Eski Timeout isteğin toplam süresiydi; ActivityTimeout ise her veri aktarımında yeniden başlıyor. 03'te gördüğümüz uzun bağlantı davranışı ve 04'teki WebSocket keep-alive notu bu değişiklikten geliyor. 1.0, 9 Kasım 2021'de .NET Core 3.1, .NET 5 ve .NET 6 desteğiyle çıktı. Nisan 2022'de bir güvenlik açığı (CVE-2022-26924; CVE, güvenlik açıklarına verilen standart kimlik numarası) nedeniyle 1.0.1 yayımlandı; 1.0.0'da kalmış bir kurulum hâlâ varsa ilk iş onu yükseltmek.

1.1 ve 2.0: Bugün Kullandığımız Parçalar

Serinin önceki yazılarında kullandığımız API'lerin bir kısmı 1.1 ile geldi. 1.1 RC'sinde IProxyStateLookup ve ReassignProxyRequest eklendi; 06'da canary'yi bunlarla kurduk. Aynı RC'de direct forwarding için SocketsHttpHandler'a ActivityHeadersPropagator vermek gerekli oldu; 05'teki InstitutionForwarding bu yüzden o satırı içeriyor. 1.1'in kendisi Mayıs 2022'de çıktı ve HTTP/3 desteğini getirdi.

2.0 (Şubat 2023) .NET Core 3.1 ve .NET 5 desteğini bıraktı ve varsayılanlarda iki değişiklik yaptı. Session affinity'nin varsayılan policy'si Cookie'den HashCookie'ye geçti. Release notuna göre HashCookie cookie değerini korumada daha zayıf bir garanti veriyor ama birden çok YARP instance'ının Data Protection ayarı olmadan aynı affinity cookie'sini paylaşmasına izin veriyor; 03'te kullandığımız buydu. YARP ayrıca SocketsHttpHandler.ConnectTimeout'u varsayılan olarak set etmeye başladı; hedef sunucu cevap vermediğinde loglarda farklı exception'lar görebilirsin. Yeni özellik tarafında HTTP/2 üzerinden WebSocket, route config'inde RateLimiterPolicy ve MaxRequestBodySize, hazır InMemoryConfigProvider (02'de kullandığımız LoadFromMemory) ve isteği proxy'lemeden cevaplayabilen request transform'ları var. Haziran 2023'te başka bir güvenlik açığı (CVE-2023-33141) için 2.0.1 yayımlandı.

2.1, 2.2 ve 2.3

2.1 (Kasım 2023) .NET 8 ile birlikte geldi. Kırıcı değişikliği, cluster'ın kullanılabilir destination policy'sinin varsayılanının HealthyAndUnknown'dan HealthyOrPanic'e geçmesi: bütün destination'lar sağlıksız olsa bile YARP istekleri yine dağıtıyor. 03'te HealthyOrPanic'i açıkça yazdık ama 2.1'den beri varsayılan bu. Aynı sürümde TimeProvider desteği, service discovery'nin temeli olan IDestinationResolver, .NET 8 request timeouts entegrasyonu (04'teki TimeoutPolicy), dağıtık trace için YARP'ın kendi activity'si (07'de gördüğümüz proxy.forwarder span'i) ve MapForwarder gibi direct forwarding kısayolları eklendi.

2.2 (Eylül 2024) route'a OutputCachePolicy ekledi. Breaking change'lerinden biri 04'te bize sorun çıkaran şeyle ilgili: istemcinin bağlantıyı kopardığı durumlardaki hatalar (RequestCanceled ve benzerleri) artık Debug seviyesinde loglanıyor. Request timeout'un 400 dönmesini logda göremememizin sebebi bu. Ayrıca header match'te NotExists modu boş değerli header'ları da eşleştirmeye başladı ve active health check yeni eklenen destination'ları ilk probe'lar tamamlanana kadar Healthy değil Unknown sayıyor.

2.3 (27 Şubat 2025) bugün hâlâ son sürüm. Release notuna göre .NET 8 ve sonrasını destekliyor. Configuration için bir JSON schema geldi, repo microsoft organizasyonundan dotnet organizasyonuna taşındı, dokümantasyon Microsoft Learn'e geçti ve 07'de incelediğimiz minimal container image'ı yayımlanmaya başladı. Breaking change'lerden en önemlisi HttpVersionPolicy'nin artık her zaman uygulanması: daha önce WebSocket gibi protokol yükseltme istekleri, policy izin vermese bile HTTP/1.1 bağlantısı açabiliyordu. Bir request transform'unda HttpRequestMessage.Content'i değiştirmek artık exception fırlatıyor; 02'de "request body'ye dokunma" dememizin bir sebebi de bu.

Kasım 2026 itibarıyla NuGet'te 2.3.0'dan yeni bir sürüm yok. GitHub'daki main dalının sürümü 3.0.0-preview.1 olarak işaretli ve 3 Kasım 2026'da .NET 10'a geçirildi, ama yayımlanmış bir 3.0 paketi yok. Container image'ın etiketleri de hâlâ 2.3 preview'ları. 3.0 yayımlandığında bu yazıyı onun release notlarıyla güncelleyeceğiz; o güne kadar 2.3 üzerine kurulmuş her şey bu serideki gibi çalışıyor.

Eski Bir YARP Konfigürasyonunu Okumak

"Önce Tahmin Et"teki dosyayı bu yazının örneğiyle (samples/Bookstore.Gateway.FromOcelot) çalıştırdım. Örnek, hangi dosyayı okuyacağını ProxyConfig ayarından alıyor ve YARP'ın config'ten ne anladığını gösteren küçük bir endpoint'i var:

// YARP'ın konfigürasyondan gerçekte ne anladığı: eski bir config dosyasını okurken işe yarar
app.MapGet("/debug/routes", (IProxyStateLookup proxyState) => proxyState.GetRoutes().Select(route => new
{
    route.Config.RouteId,
    route.Config.Match.Path,
    route.Config.ClusterId,
    route.Config.Order,
    ActivityTimeout = route.Cluster?.Model.Config.HttpRequest?.ActivityTimeout?.ToString("c", CultureInfo.InvariantCulture),
    route.Cluster?.Model.Config.LoadBalancingPolicy,
}));
{
  "ReverseProxy": {
    "Routes": [
      {
        "RouteId": "books",
        "ClusterId": "catalog",
        "Match": {
          "Path": "/api/v1/books/{**everything}"
        },
        "Transforms": [
          { "PathPattern": "/books/{**everything}" }
        ]
      }
    ],
    "Clusters": {
      "catalog": {
        "LoadBalancingPolicy": "First",
        "HttpRequest": {
          "Timeout": "00:00:05"
        },
        "Destinations": {
          "catalog-1": { "Address": "http://localhost:5301/" }
        }
      }
    }
  }
}

Üç adımda, her seferinde bir sorunu düzelterek:

dotnet run --project samples/Bookstore.Gateway.FromOcelot -- --ProxyConfig=legacy-yarp.json

# 1) dosya olduğu gibi
fail: Yarp.ReverseProxy.Configuration.ConfigProvider.ConfigurationConfigProvider[47]
      Configuration data conversion failed.
      System.Exception: The route config format has changed, routes are now objects instead of an array. The route id must be set as the object name, not with the 'RouteId' field.
Unhandled exception. System.InvalidOperationException: Unable to load or apply the proxy configuration.

# 2) Routes dictionary'ye çevrildi
Unhandled exception. System.InvalidOperationException: Unable to load or apply the proxy configuration.
 ---> System.ArgumentException: No matching ILoadBalancingPolicy found for the load balancing policy 'First' set on the cluster 'catalog'.

# 3) First -> FirstAlphabetical
curl http://localhost:5306/debug/routes
[{"routeId":"books","path":"/api/v1/books/{**everything}","clusterId":"catalog","order":null,"activityTimeout":null,"loadBalancingPolicy":"FirstAlphabetical"}]
books 200

"Önce Tahmin Et"in cevabı: ilk ikisi uygulamayı başlatmadı, Timeout ise sessizce yok sayıldı. YARP eski array biçimini tanıyıp açıklayıcı bir hata veriyor; bilinmeyen policy adını da config doğrulamasında yakalıyor. Ama tanımadığı bir alanı hiç okumuyor: activityTimeout boş, yani istek 5 saniyede değil varsayılan 100 saniyede kesilecek. Eski konfigürasyonu taşırken en tehlikeli hatalar böyle hata vermeyenler. Burada 2.3'ün JSON schema'sı işe yarıyor. Schema'yı bir JSON Schema doğrulayıcısıyla (Python jsonschema) iki dosyaya karşı çalıştırdım:

legacy-yarp.json -> 2 error(s)
   ReverseProxy/Clusters/catalog/HttpRequest : Additional properties are not allowed ('Timeout' was unexpected)
   ReverseProxy/Routes : [{'RouteId': 'books', 'ClusterId': 'catalog', 'Match': {'Path': '/api/v1/books/{**everything}'}, 'Transforms': [{'PathPa
yarp.json -> 0 error(s)

Release notuna göre 2.3 paketini referans aldığında editör, appsettings.json'daki ReverseProxy bölümünü bu schema ile kendisi doğruluyor; config başka bir dosyadaysa ya da CI'da kontrol etmek istiyorsan aynı schema'yı bir doğrulayıcıyla çalıştırabilirsin. LoadBalancingPolicy alanında ise schema bilinen adların yanında her string'e izin veriyor, çünkü kendi policy'ni yazabilirsin; First gibi bir hatayı ancak uygulama başlarken görürsün.

Ocelot'tan YARP'a: Kavramların Karşılıkları

Ocelot, YARP'tan önce .NET'te en yaygın kullanılan açık kaynak API gateway projesi ve bugün de geliştiriliyor. Bookstore'un eski gateway'i Ocelot ile yazılmış olsun; repo'da samples/Bookstore.Gateway.Ocelot bu "önceki" hâli, Ocelot 25.0 ile çalışıyor. İki route'u var: anonim kitap okuma ve token isteyen, rate limit'li sipariş route'u:

{
  "Routes": [
    {
      "UpstreamPathTemplate": "/api/v1/books/{everything}",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/books/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        { "Host": "localhost", "Port": 5301 },
        { "Host": "localhost", "Port": 5311 }
      ],
      "LoadBalancerOptions": { "Type": "RoundRobin" }
    },
    {
      "UpstreamPathTemplate": "/api/v1/orders/{everything}",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/orders/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        { "Host": "localhost", "Port": 5302 }
      ],
      "AuthenticationOptions": { "AuthenticationProviderKeys": [ "Bearer" ] },
      "RateLimitOptions": {
        "ClientIdHeader": "X-Client-Id",
        "EnableRateLimiting": true,
        "Period": "10s",
        "Limit": 5
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5308"
  }
}

Kavramların çoğunun birebir karşılığı var, ama birkaçı başka bir katmana taşınıyor:

OcelotYARP
UpstreamPathTemplate, UpstreamHttpMethodRoute'ta Match.Path, Match.Methods
DownstreamPathTemplatePathPattern ya da PathRemovePrefix transform'u
DownstreamHostAndPorts, DownstreamSchemeCluster'da Destinations ve Address
LoadBalancerOptions.TypeLoadBalancingPolicy (RoundRobin, LeastRequests…); sticky session için session affinity
Priority (büyük değer önce)Order (küçük değer önce)
AuthenticationOptions, RouteClaimsRequirementASP.NET Core authentication ve AuthorizationPolicy
RateLimitOptionsASP.NET Core rate limiter ve RateLimiterPolicy
Route cache ayarlarıASP.NET Core output cache ve OutputCachePolicy
AggregatesKendi endpoint'in (05'teki aggregation)
Service discovery provider'larıIDestinationResolver (03)

Genel çizgi şu: Ocelot authentication, rate limit ve cache gibi işleri kendi konfigürasyon şemasıyla yönetiyor; YARP bunları ASP.NET Core'un kendi middleware'lerine bırakıp route'ta yalnız policy adını tutuyor (04). Taşıma, Ocelot'un JSON'unu YARP'ın JSON'una çevirmekten çok, Ocelot'un içindeki davranışları ASP.NET Core policy'leri olarak yeniden yazmak demek. YARP karşılığı (samples/Bookstore.Gateway.FromOcelot/yarp.json):

"Routes": {
  "books": {
    "ClusterId": "catalog",
    "Match": {
      "Path": "/api/v1/books/{**everything}",
      "Methods": [ "GET" ]
    },
    "Transforms": [
      { "PathPattern": "/books/{**everything}" }
    ]
  },
  "orders": {
    "ClusterId": "ordering",
    "AuthorizationPolicy": "Default",
    "RateLimiterPolicy": "per-user",
    "Match": {
      "Path": "/api/v1/orders/{**everything}",
      "Methods": [ "GET" ]
    },
    "Transforms": [
      { "PathPattern": "/orders/{**everything}" }
    ]
  }
},
"Clusters": {
  "catalog": {
    "LoadBalancingPolicy": "RoundRobin",
    "Destinations": {
      "catalog-1": { "Address": "http://localhost:5301/" },
      "catalog-2": { "Address": "http://localhost:5311/" }
    }
  },
  "ordering": {
    "Destinations": {
      "ordering": { "Address": "http://localhost:5302/" }
    }
  }
}
// Ocelot'un RateLimitOptions'ı X-Client-Id header'ına göre sayıyordu; burada partition doğrulanmış kullanıcı
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.AddPolicy("per-user", context => RateLimitPartition.GetFixedWindowLimiter(
        context.User.FindFirstValue("sub") ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
        _ => new FixedWindowRateLimiterOptions { PermitLimit = 5, Window = TimeSpan.FromSeconds(10), QueueLimit = 0 }));
});

Taşımanın doğru olduğunu görmek için aynı istek setini iki gateway'e de attım ve sonuçları yan yana koydum; soldaki Ocelot (5308), sağdaki YARP (5307):

                                        Ocelot :5308                 YARP :5307
books/9780441172719 x4                  catalog-1 2 1 2              catalog-2 1 2 1
BOOKS/9780441172719                     404                          200
orders/1001 no token                    503                          401
orders/1001 Ada                         503                          200
orders/1001 Ada, X-Client-Id: web x7    200 200 200 200 200 429 429  200 200 200 429 429 429 429

Farkların her biri taşımada bir karar. Büyük harfli adres Ocelot 25.0'da eşleşmedi, YARP'ta eşleşti; Ocelot'ta bunu route bazında belirleyen RouteIsCaseSensitive ayarı var. İki gateway'i aynı adres listesiyle test etmeden geçiş yaparsan, bugün 404 alan bir istemci yarın 200 alır ya da tersi. En büyük fark rate limit'te. Ocelot istemciyi X-Client-Id header'ından tanıyor ve header yoksa isteği 503 ile reddediyor; üstelik bu kontrol authentication'dan önce çalışıyor, o yüzden token'sız istek bile 401 değil 503 aldı:

Rate limiting client could not be identified for the route '/api/v1/orders/{everything}' due to a missing or unknown client ID header required by rule '5/10s/w10s'!

YARP tarafında partition doğrulanmış kullanıcı, header'a ihtiyaç yok. Son satırdaki fark da buradan: YARP Ada'nın önceki iki isteğini de aynı pencerede saydığı için üçüncü istekten sonra 429 döndü. Hangisinin doğru olduğu ürün kararı; ama istemcinin gönderdiği bir header'a göre limit koymak, istemcinin header'ı değiştirerek limitten kaçabilmesi demek. Bu farkı 04'te tartıştık.

Üretimde Dikkat

YARP'ı yükseltmeden önce aradaki her sürümün "Breaking changes" bölümünü oku; bu yazıdaki tablolar o bölümlerden çıktı. Yükseltmeyi bir test ortamında yap ve /debug/routes gibi bir endpoint'le yükseltmeden önceki ve sonraki route tablosunu karşılaştır. Sessiz değişiklikler (yok sayılan bir alan, değişen bir varsayılan) ancak bu karşılaştırmada görünür.

Runtime desteğini YARP desteğinden ayrı takip et. 2.3 .NET 8 ve sonrası için yayımlandı, ama .NET 8'in desteği 10 Kasım 2026'da bitti; desteklenen bir kombinasyonda kalmak için bu seride olduğu gibi .NET 10 kullan. YARP paketi net8.0 hedefini taşıdığı için .NET 10 uygulamasında sorunsuz çalışıyor.

Ocelot'tan geçişi tek seferde yapma. 06'daki strangler yaklaşımı gateway'in kendisi için de geçerli: YARP'ı Ocelot'un önüne ya da yanına koy, route'ları teker teker taşı ve her adımda yukarıdaki gibi aynı istek setiyle iki tarafı karşılaştır. Rate limit ve authentication gibi davranışların farklı olduğu route'ları en sona bırak ve istemcilere önceden duyur.

Sık Düşülen Tuzaklar

Birincisi, internetteki eski bir örneği kopyalamak. IHttpProxy, ProxyRoute ya da array biçiminde Routes görüyorsan örnek 1.0 öncesinden; 2.3'te derlenmez ya da çalışmaz.

İkincisi, hata vermeyen config'e güvenmek. Bilinmeyen alanlar yok sayılıyor; schema doğrulamasını CI'a ekle.

Üçüncüsü, varsayılanların değiştiğini unutmak. 2.1 öncesi bir kurulum HealthyAndUnknown ile çalışıyordu; yükselttiğinde bütün destination'lar sağlıksızken trafik kesilmek yerine dağıtılmaya başlar.

Dördüncüsü, Ocelot'un Priority'sini YARP'ın Order'ına aynen kopyalamak. Ocelot dokümantasyonuna göre Ocelot'ta büyük değer önce eşleşiyor; YARP'ta küçük değer önce. Sayıları ters çevirmeden taşırsan 02'deki catch-all tuzağına düşersin.

Beşincisi, Ocelot'un rate limit ve authentication davranışını birebir aynı sanmak. Yukarıdaki tablo, aynı görünen iki config'in aynı istekte 503 ile 200 arasında fark yaratabildiğini gösteriyor.

Küçük Bir Deneme

★☆☆ legacy-yarp.json'daki Timeout'u ActivityTimeout yap ve /debug/routes çıktısında activityTimeout'un 00:00:05 olduğunu doğrula. Schema doğrulamasını tekrar çalıştır; kaç hata kaldı?

★★☆ ocelot.json'a Priority değeri 1 olan /api/v1/books/summary route'unu ve Priority değeri 0 olan bir catch-all ekle. Aynı davranışı YARP'ta Order ile kur ve iki gateway'e aynı istekleri atarak sonucun eşleştiğini göster.

★★★ Ocelot'un X-Client-Id tabanlı limitini korumak zorundasın, çünkü bazı partner'lar token kullanmıyor. YARP'ta, X-Client-Id'yi yalnız gateway'in bildiği bir API key listesine karşı doğrulayan ve partition'ı doğrulanmış client'a göre yapan bir rate limiter policy'si yaz; bilinmeyen client'lar 401 alsın. 04'teki iç header kuralıyla nasıl birleştiğini de anlat.

Bu yazıyla API Gateway ve YARP serisi bitiyor. Bookstore.Gateway artık route ve cluster modeliyle başlayıp health check, kimlik, rate limit, Angular için bir backend for frontend (BFF), strangler fig ve production işletimine kadar uzanan bir gateway. Gateway'i Bookstore'un microservice'lerinin önüne koymayı ilerideki .NET ile Mikroservis Uygulaması serisinde, YARP'ı Aspire içinde kodla tanımlamayı da ilerideki Aspire ile Dağıtık Uygulamalar serisinde göreceğiz.