HTTP Caching: Cache-Control, ETag, 304 Not Modified ve Vary ile Sunucuyu Hiç Yormamak
HTTP cache'i sunucu değil header'lar yönetir: max-age süresince istek hiç gelmez, sonra ETag ile gelen istek gövdesiz 304 alır. Denemede menünün ikinci isteği tarayıcıdan 0 byte ile döndü, 304'ü ise Angular HttpClient 200 olarak gördü. nginx önünde Vary'siz kitap yanıtı İngilizce kullanıcıya Türkçe fiyat verdi, public işaretli sepet ise Bob'a Alice'in sepetini gösterdi.
Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; shared cache olarak nginx 1.29, istemci tarafında Angular 22.2 ve Chrome 156 kullanıldı.
Önce Tahmin Et
Bookstore'un Angular arayüzü kitap detayını API'den (Application Programming Interface) HttpClient ile istiyor. Sunucu yanıta bir ETag koyuyor, tarayıcı ikinci istekte bu değeri If-None-Match ile geri gönderiyor ve sunucu gövdesiz bir 304 Not Modified dönüyor. Birinci soru: Angular kodundaki subscribe hangi status kodunu görür, 304 mü, 200 mü?
İkinci soru: sepet endpoint'i Cache-Control: public, max-age=60 dönüyor ve önünde bir reverse proxy cache var. Alice sepetini Authorization: Bearer alice ile istiyor, birkaç saniye sonra Bob kendi token'ıyla aynı URL'yi (Uniform Resource Locator) istiyor. Bob kimin sepetini görür?
Cache-Control: Kimin, Ne Kadar Saklayacağı
ASP.NET Core Web API serisinin "ASP.NET Core Web API'de Caching: IMemoryCache, Redis ve Response Caching" yazısı [ResponseCache]'i, Cache-Control: public,max-age=60 header'ını ve Age'i gösterdi. Bu yazı oradan devam ediyor ve HTTP (Hypertext Transfer Protocol) cache'inin kurallarını yazan iki belgeye dayanıyor: cache davranışı için RFC (Request for Comments) 9111, conditional request'ler ve ETag için RFC 9110. Önce dil: RFC 9111 iki tür cache tanımlıyor. Private cache tek bir kullanıcıya aittir, tipik örneği tarayıcı. Shared cache birden çok kullanıcıya hizmet eder: reverse proxy, CDN (Content Delivery Network), kurumsal proxy.
| Response directive | Anlamı | Bookstore'da |
|---|---|---|
max-age=60 | Yanıt 60 saniye taze; bu sürede cache sunucuya sormadan kullanır | Kategori menüsü |
s-maxage=300 | Yalnız shared cache için max-age'i ezer; süre dolunca doğrulamadan kullanılamaz | Menü, proxy'de 5 dakika |
no-cache | Saklanabilir ama her kullanımdan önce sunucuyla doğrulanmalı | Sepet |
no-store | Hiçbir cache isteğin ya da yanıtın hiçbir parçasını saklamaz | Ödeme ve kart bilgileri |
private | Shared cache saklamaz, tarayıcı saklayabilir | Kişiye özel her şey |
public | Normalde yasak olsa bile saklanabilir, Authorization'lı isteğe gelen yanıt dahil | Yalnız gerçekten herkese aynı olan yanıt |
stale-while-revalidate=30 | Süre dolduktan sonra 30 saniye eski kopya verilip arka planda yenilenebilir (RFC 5861) | Menü |
İsimler yanıltıcı, en sık karışanı ilk ikisi: no-cache "cache'leme" demiyor, "her seferinde sor" diyor; asıl "saklama" no-store. Bir de hiç header yazmamanın anlamı var. RFC 9111'in 4.2.2 bölümüne göre açık bir süre yoksa cache sezgisel (heuristic) bir tazelik süresi atayabilir; Last-Modified varsa önerilen tipik oran, son değişiklikten bu yana geçen sürenin yüzde 10'u. Yani Cache-Control yazmadığın bir yanıt, sandığın gibi "cache'lenmez" değil.
ETag ve 304: Gövdesiz Cevap
Tazelik süresi dolduğunda cache yanıtı çöpe atmaz, sunucuya "elimdeki hâlâ geçerli mi?" diye sorar. Bu soru bir conditional request: elindeki ETag'i If-None-Match, Last-Modified'ı If-Modified-Since ile gönderir. Değişiklik yoksa sunucu gövdesiz 304 döner. ASP.NET Core bunu senin yerine yapmıyor; kararı veren küçük bir yardımcı yazdık:
public static class Conditional
{
// RFC 9110: If-None-Match varsa If-Modified-Since yok sayılır; ETag'ler weak comparison ile kıyaslanır
public static bool IsNotModified(HttpRequest request, EntityTagHeaderValue etag, DateTimeOffset? lastModified)
{
RequestHeaders headers = request.GetTypedHeaders();
if (headers.IfNoneMatch.Count > 0)
return headers.IfNoneMatch.Any(t => t.Equals(EntityTagHeaderValue.Any) || t.Compare(etag, useStrongComparison: false));
if (lastModified is { } modified && headers.IfModifiedSince is { } since)
return TruncateToSeconds(modified) <= since;
return false;
}
private static DateTimeOffset TruncateToSeconds(DateTimeOffset value) =>
new(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Offset);
}
İki kural doğrudan RFC 9110'dan: If-None-Match geldiyse If-Modified-Since yok sayılır ve If-None-Match kıyaslaması weak comparison ile yapılır. TruncateToSeconds ise HTTP tarih formatının saniyeden küçük kısmı taşımamasından: yuvarlamazsan, milisaniyeli bir UpdatedAt hiçbir zaman header'daki tarihe eşit çıkmaz ve 304 hiç dönmez.
Menü için ETag'i içeriğin kendisinden türetiyoruz: gövdeyi serialize edip SHA (Secure Hash Algorithm) ailesinden SHA-256 hash'inin ilk 16 hex karakterini alıyoruz. Veritabanında sürüm bilgisi olmayan, birkaç kaynaktan derlenen yanıtlar için en pratik yol bu:
app.MapGet("/api/categories/menu", (HttpContext http, CatalogStore store) =>
{
byte[] body = JsonSerializer.SerializeToUtf8Bytes(store.Menu, json);
EntityTagHeaderValue etag = new($"\"{Convert.ToHexString(SHA256.HashData(body))[..16]}\"");
http.Response.Headers.CacheControl = "public, max-age=60, s-maxage=300, stale-while-revalidate=30";
http.Response.Headers.ETag = etag.ToString();
if (Conditional.IsNotModified(http.Request, etag, lastModified: null))
return Results.StatusCode(StatusCodes.Status304NotModified);
return Results.Bytes(body, "application/json");
});
$ curl -i localhost:17525/api/categories/menu
HTTP/1.1 200 OK
Content-Length: 79
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "436F5A205A91F9FB"
[{"id":1,"name":"Fiction"},{"id":2,"name":"Science"},{"id":3,"name":"History"}]
$ curl -i localhost:17525/api/categories/menu -H 'If-None-Match: "436F5A205A91F9FB"'
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "436F5A205A91F9FB"
Header'ları return'den önce yazmamızın sebebi RFC 9110'un 304 için koyduğu kural: sunucu, 200'de göndereceği ETag, Vary, Cache-Control ve Expires header'larını 304'te de göndermek zorunda; cache saklı kopyanın tazelik bilgisini bu header'larla günceller. Hash'li ETag'in bir bedeli var: 304 dönsen bile gövdeyi üretip hash'lemek zorundasın. Kazanç bant genişliği ve istemcinin parse süresi; veritabanı yükü değil. Veritabanını da kurtarmak istiyorsan bir sürüm bilgisine ihtiyacın var.
Sürüm Tabanlı ETag ve Last-Modified
Kitap kaydında bir Version sayacı ve UpdatedAt var; SQL (Structured Query Language) Server'da aynı işi bir rowversion kolonu görür. Fiyat ise isteğin diline göre biçimleniyor, bu yüzden ETag'e kültürü de katıyoruz:
app.MapGet("/api/books/{id:int}", (int id, HttpContext http, CatalogStore store) =>
{
if (!store.Books.TryGetValue(id, out Book? book)) return Results.NotFound();
CultureInfo culture = PickCulture(http.Request);
// aynı URL'nin her dil temsili ayrı bir ETag taşır
EntityTagHeaderValue etag = new($"\"{book.Version}-{culture.Name}\"");
http.Response.Headers.CacheControl = "public, max-age=30";
http.Response.Headers.ETag = etag.ToString();
http.Response.GetTypedHeaders().LastModified = book.UpdatedAt;
http.Response.Headers.Vary = "Accept-Language";
if (Conditional.IsNotModified(http.Request, etag, book.UpdatedAt))
return Results.StatusCode(StatusCodes.Status304NotModified);
return Results.Ok(new { book.Id, book.Title, priceText = book.Price.ToString("N2", culture), book.Version });
});
$ curl -i localhost:17525/api/books/42 -H 'Accept-Language: tr-TR'
HTTP/1.1 200 OK
Cache-Control: public, max-age=30
ETag: "1-tr-TR"
Last-Modified: Sun, 01 Nov 2026 09:30:00 GMT
Vary: Accept-Language
{"id":42,"title":"Distributed Systems Basics","priceText":"1.249,50","version":1}
$ curl -i localhost:17525/api/books/42 -H 'Accept-Language: tr-TR' -H 'If-Modified-Since: Sun, 01 Nov 2026 09:30:00 GMT'
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=30
ETag: "1-tr-TR"
Last-Modified: Sun, 01 Nov 2026 09:30:00 GMT
Vary: Accept-Language
$ curl -s -X PUT localhost:17525/api/books/42/price -H 'Content-Type: application/json' -d '{"price":1199.90}' -w '%{http_code}'
204
$ curl -i localhost:17525/api/books/42 -H 'Accept-Language: tr-TR' -H 'If-None-Match: "1-tr-TR"'
HTTP/1.1 200 OK
Cache-Control: public, max-age=30
ETag: "2-tr-TR"
Last-Modified: Sun, 08 Nov 2026 02:24:04 GMT
Vary: Accept-Language
{"id":42,"title":"Distributed Systems Basics","priceText":"1.199,90","version":2}
$ curl -s localhost:17525/stats
{"/api/books/42 200":2,"/api/books/42 304":2,"/api/books/42/price 204":1,"/api/categories/menu 200":1,"/api/categories/menu 304":1}
Fiyat değişince sürüm 2'ye çıktı ve eski ETag'le gelen istek yeni gövdeyi aldı. Burada ETag veritabanından okunan bir sayıdan geliyor; gerçek bir uygulamada yalnız Version kolonunu okuyup 304'e karar verebilir, kitabın geri kalanını hiç yüklemeyebilirsin. ETag'e kültürü katmasaydık, Türkçe ve İngilizce temsil aynı validator'ı taşırdı ve bir cache birinin 304'üyle ötekinin gövdesini tazeleyebilirdi.
Shared Cache'in Gözünden: Vary
Davranışı gerçek bir shared cache'te görmek için API'nin önüne nginx koyduk; tek eklememiz cache durumunu header'a yazmak:
proxy_cache_path /var/cache/nginx/api keys_zone=api:10m max_size=100m;
server {
listen 80;
location / {
proxy_pass http://host.docker.internal:17525;
proxy_cache api;
add_header X-Cache-Status $upstream_cache_status always;
}
}
Menüyü proxy üzerinden iki kez istedik: ilki MISS, ikincisi HIT ve sunucu sayacı tek bir 200 gösterdi. Sonra kitap endpoint'ini Vary satırı olmadan başlatıp aynı URL'yi iki dille istedik:
$ curl -i localhost:17526/api/books/42 -H 'Accept-Language: tr-TR'
HTTP/1.1 200 OK
Cache-Control: public, max-age=30
ETag: "1-tr-TR"
Last-Modified: Sun, 01 Nov 2026 09:30:00 GMT
X-Cache-Status: MISS
{"id":42,"title":"Distributed Systems Basics","priceText":"1.249,50","version":1}
$ curl -i localhost:17526/api/books/42 -H 'Accept-Language: en-US'
HTTP/1.1 200 OK
Cache-Control: public, max-age=30
ETag: "1-tr-TR"
Last-Modified: Sun, 01 Nov 2026 09:30:00 GMT
X-Cache-Status: HIT
{"id":42,"title":"Distributed Systems Basics","priceText":"1.249,50","version":1}
İngilizce isteyen kullanıcı Türkçe biçimli fiyatı aldı. Proxy'nin key'i yalnız URL; dile bakması gerektiğini ona söyleyen header Vary. Satırı geri koyunca:
$ curl -i localhost:17526/api/books/42 -H 'Accept-Language: en-US'
HTTP/1.1 200 OK
Cache-Control: public, max-age=30
ETag: "1-en-US"
Last-Modified: Sun, 01 Nov 2026 09:30:00 GMT
Vary: Accept-Language
X-Cache-Status: MISS
{"id":42,"title":"Distributed Systems Basics","priceText":"1,249.50","version":1}
$ curl -i localhost:17526/api/books/42 -H 'Accept-Language: tr-TR'
HTTP/1.1 200 OK
Cache-Control: public, max-age=30
ETag: "1-tr-TR"
Last-Modified: Sun, 01 Nov 2026 09:30:00 GMT
Vary: Accept-Language
X-Cache-Status: HIT
{"id":42,"title":"Distributed Systems Basics","priceText":"1.249,50","version":1}
RFC 9111'in 4.1 bölümü bunu cache key'in ikincil parçası olarak tanımlıyor: Vary'de adı geçen her request header'ı, saklı yanıtın kullanılabilmesi için eşleşmek zorunda. Bedelini de hesaba kat: Accept-Language tarayıcıdan tarayıcıya onlarca farklı değer alabilir ve her biri ayrı bir entry demek. Sunucuda dili birkaç desteklenen değere indirgemek yanıt sayısını sınırlar, ama proxy entry'leri gelen header değerine göre ayırır; değerleri normalleştirmek proxy ya da CDN tarafının işi.
public, private ve Authorization
Şimdi ikinci tahmin. Sepet endpoint'ini önce public, max-age=60 ile başlattık:
$ curl -i localhost:17526/api/me/cart -H 'Authorization: Bearer alice'
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
X-Cache-Status: MISS
{"user":"alice","items":["Book 42"]}
$ curl -i localhost:17526/api/me/cart -H 'Authorization: Bearer bob'
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
X-Cache-Status: HIT
{"user":"alice","items":["Book 42"]}
Bob, Alice'in sepetini gördü. Proxy burada kural dışı bir şey yapmadı: RFC 9111'in 3.5 bölümü, shared cache'in Authorization header'lı bir isteğe gelen yanıtı ancak yanıt public, s-maxage ya da must-revalidate taşıyorsa başkasına verebileceğini söylüyor, 5.2.2.9 da public'in tam olarak buna izin verdiğini. Hatayı yapan, "her şeye public yazalım, hızlansın" diyen kod. Doğrusu:
// yalnız demo: "Bearer <kullanıcı>" header'ını kimlik sayar, gerçek authentication değildir
app.MapGet("/api/me/cart", (HttpContext http) =>
{
string? user = http.Request.Headers.Authorization.ToString() is { Length: > 7 } auth ? auth[7..] : null;
if (user is null) return Results.Unauthorized();
http.Response.Headers.CacheControl = "private, no-cache";
return Results.Ok(new { user, items = user == "alice" ? new[] { "Book 42" } : ["Book 7", "Book 9"] });
});
$ curl -i localhost:17526/api/me/cart -H 'Authorization: Bearer bob'
HTTP/1.1 200 OK
Cache-Control: private, no-cache
X-Cache-Status: MISS
{"user":"bob","items":["Book 7","Book 9"]}
Kimliği cookie ile taşıyan bir uygulamada risk daha büyük, çünkü cookie için RFC'de Authorization'daki gibi bir koruma yok. Kişiye özel yanıta her zaman private, para ya da kimlik bilgisi taşıyana no-store yaz.
[ResponseCache] ve Middleware'in Sınırları
[ResponseCache] yalnız Cache-Control ve Vary header'larını yazar; ETag üretmez, 304 kararı vermez. Response caching middleware'in kaynak kodunda bir ContentIsNotModified kontrolü var: cache'teki yanıtın ETag'i ya da Last-Modified'ı varsa ve istekle eşleşiyorsa 304 döner. Ama o ETag'i yine senin endpoint'in üretmeli. Bir istisna static file middleware: UseStaticFiles her dosyaya kendisi ETag ve Last-Modified koyuyor ve 304'ü de kendi dönüyor:
$ curl -i localhost:17525/ng/index.html
HTTP/1.1 200 OK
ETag: "1dd6f286aabc52e"
Last-Modified: Sun, 08 Nov 2026 02:25:27 GMT
$ curl -i localhost:17525/ng/index.html -H 'If-None-Match: "1dd6f286aabc52e"'
HTTP/1.1 304 Not Modified
ETag: "1dd6f286aabc52e"
Dikkat edersen Cache-Control yok, yani tarayıcı yukarıdaki sezgisel tazelik kuralını uygulayabilir. Static dosyalarda StaticFileOptions.OnPrepareResponse ile süreyi açıkça yaz. Sunucu tarafında yanıtın tamamını saklamak ise output cache'in işi ve serinin altıncı yazısının konusu.
Angular Tarafı: Tarayıcı Cache'i, HttpClient ve Interceptor
Angular 22'de HttpClient varsayılan olarak fetch kullanıyor (withXhr ile XHR'a (XMLHttpRequest) döner); ikisi de tarayıcının HTTP cache'inden geçer. Bunu görmek için gerçek bir HttpClient'ı createApplication ile ayağa kaldıran küçük bir sayfa yazdık, esbuild ile JIT (Just-in-Time) modunda paketleyip API'nin kendi origin'inden headless Chrome'da çalıştırdık. Her satır bir isteğin HttpClient'a görünen status'ünü ve Resource Timing'deki transfer boyutunu gösteriyor:
menu #1: status=200 network=1 lastTransfer=379B
menu #2: status=200 network=2 lastTransfer=0B
book #1: status=200 network=1 lastTransfer=381B
book #2 no-cache: status=200 network=2 lastTransfer=300B
cart #1: status=200 network=1 lastTransfer=336B
cart #2: status=200 network=2 lastTransfer=336B
cart #3 interceptor: status=200 network=1 lastTransfer=336B
cart #4 interceptor: status=200 network=1 lastTransfer=336B
server: {"/api/books/42 200":1,"/api/books/42 304":1,"/api/categories/menu 200":1,"/api/me/cart 200":3}
Birinci tahminin cevabı book #2 satırında. İsteği { cache: 'no-cache' } fetch option'ıyla attık; tarayıcı If-None-Match gönderdi, sunucu 304 döndü (sayaçta "/api/books/42 304":1), transfer 300 byte'lık header'dan ibaret. Ama HttpClient 200 ve tam gövde gördü: tarayıcı 304'ü kendi cache'indeki kopyayla birleştirip uygulamaya 200 olarak veriyor. Angular kodunda 304'ü yakalamaya çalışan bir if hiçbir zaman çalışmaz. menu #2 ise ağa hiç çıkmadı: max-age=60 sürerken transfer 0 byte, sunucuda tek bir 200.
Sepet private, no-cache döndüğü ve ETag taşımadığı için her istek sunucuya tam gövdeyle gitti. Sık tekrarlanan böyle bir okuma için istemci tarafında kısa ömürlü bir cache istiyorsan, Angular dokümanındaki HttpContextToken yaklaşımıyla isteğe özel açılan bir interceptor yeterli:
import { HttpContextToken, HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { of, tap } from 'rxjs';
// bir GET yanıtının bellekte ne kadar yaşayacağı; 0 ise istemci cache'i yok
export const CACHE_TTL_MS = new HttpContextToken<number>(() => 0);
interface Entry {
response: HttpResponse<unknown>;
expiresAt: number;
}
const store = new Map<string, Entry>();
export const memoryCacheInterceptor: HttpInterceptorFn = (req, next) => {
const ttl = req.context.get(CACHE_TTL_MS);
if (req.method !== 'GET' || ttl <= 0) {
return next(req);
}
const key = req.urlWithParams;
const hit = store.get(key);
if (hit && hit.expiresAt > Date.now()) {
return of(hit.response.clone());
}
return next(req).pipe(
tap((event) => {
if (event instanceof HttpResponse) {
store.set(key, { response: event, expiresAt: Date.now() + ttl });
}
}),
);
};
// logout'ta çağır ki sonraki kullanıcı öncekinin verisini görmesin
export function clearHttpCache(): void {
store.clear();
}
http.get('/api/books/42', { cache: 'no-cache' });
const ttl = new HttpContext().set(CACHE_TTL_MS, 30_000);
http.get('/api/me/cart?v=2', { headers: { Authorization: 'Bearer alice' }, context: ttl });
cart #3 ve #4 satırları bunu gösteriyor: aynı URL için Resource Timing'de tek bir ağ isteği var, sunucudaki üçüncü 200 o. Ne zaman gereksiz? Sunucu doğru max-age ve ETag veriyorsa tarayıcı bu işi zaten yapıyor, üstelik sayfa yenilense de; interceptor'daki Map ise sayfa yenilendiğinde kaybolur ve sunucunun invalidation kararından habersizdir. Interceptor'ı sunucunun header'larını değiştiremediğin üçüncü parti bir API'de ya da aynı ekranda aynı veriyi isteyen birkaç component'i tek isteğe indirmek için kullan; kendi API'nde önce header'ları düzelt.
Üretimde Dikkat
Ölçüm için sunucuya ulaşan istekleri status koduna göre say, bizim /stats endpoint'imiz gibi. 304 oranı yükseliyorsa ETag çalışıyor demektir; ama 304 de bir istek: sunucunun kimliği doğrulaması, sürümü okuması ve cevap yazması gerekiyor. Gerçek kazanç max-age süresince hiç gelmeyen isteklerden geliyor ve onlar sayaçta görünmüyor; CDN ya da proxy log'larındaki hit oranına bak.
Tarayıcıya verdiğin max-age'i geri alamazsın. Menüye bir saat verip yanlış bir kategori yayınlarsan, o bir saat boyunca bazı kullanıcılar onu görür. Bu yüzden tarayıcı için kısa max-age ve ETag, proxy için daha uzun s-maxage ve purge yeteneği iyi bir ayrım. Değişmeyen asset'lerde ise tersi geçerli: dosya adına hash koyup çok uzun süre ver; ayrıntısı ilerideki Frontend kategorisindeki Browser API'leri ve PWA (Progressive Web App) serisinde.
Load balancer'ın arkasında birden çok instance varsa ETag her instance'ta aynı üretilmeli. İçerik hash'i ve veritabanı sürümü bu şartı sağlar; instance'ın başlangıç zamanı ya da bellekteki bir sayaç sağlamaz ve her instance değişiminde 304 yerine 200 alırsın.
Sık Düşülen Tuzaklar
Kişiye özel yanıta public yazmak. Denememizde Bob, Alice'in sepetini aldı; Authorization header'ı onu korumadı, çünkü public korumayı bilerek kaldırıyor.
Vary'i unutmak. Dil, para birimi, sıkıştırma gibi bir request header'ına göre değişen her yanıt Vary'de o header'ı listelemeli; yoksa ilk gelen kullanıcının temsili herkese gider.
no-cache'i "saklama" sanmak. Saklama no-store'dur; no-cache saklar ama her seferinde sorar.
304'te header'ları unutmak. ETag'i ve Cache-Control'ü yalnız 200 dalında yazarsan cache saklı kopyanın tazelik bilgisini güncelleyemez.
Header yazmamak. Cache-Control olmayan ama Last-Modified taşıyan bir yanıt, tarayıcının sezgisel olarak cache'lemesine açıktır.
Angular'da 304 beklemek. HttpClient tarayıcının birleştirdiği 200'ü görür; 304'ü ancak sunucu log'unda ya da tarayıcının network sekmesinde görürsün.
Küçük Bir Deneme
★☆☆ Menüyü nginx üzerinden iste, sonra API'yi durdur ve 5 dakika dolmadan tekrar iste. Yanıt geliyor mu? s-maxage'i 10 saniyeye indirip aynı denemeyi yap; nginx'in proxy_cache_use_stale ayarı olmadan ne görüyorsun?
★★☆ Kitap endpoint'ine Accept-Encoding'e göre sıkıştırma ekle (AddResponseCompression) ve yanıtın Vary header'ına bak. Tek bir kitap için proxy'de kaç ayrı entry oluşuyor?
★★★ memoryCacheInterceptor'ı, süresi dolan entry için tam istek yerine If-None-Match ile conditional request gönderecek şekilde genişlet. Tarayıcı zaten aynı işi yaparken bunun bir faydası olur mu? Network sekmesinde iki yaklaşımı karşılaştır.
Bu yazıda cache'i sunucunun dışına taşıdık: tarayıcı ve proxy, sunucunun header'larına güvenerek isteği hiç göndermiyor ya da gövdesiz bir 304 ile yetiniyor. Bir sonraki yazıda cache'i yeniden sunucuya alıyoruz ama yanıtın tamamıyla: ASP.NET Core output cache'in policy'leri, vary-by kuralları, tag ile eviction, resource locking ve Redis store.