aspnetcore/test/Microsoft.Net.Http.Server.F.../ResponseCachingTests.cs

1084 lines
54 KiB
C#

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
namespace Microsoft.Net.Http.Server
{
public class ResponseCachingTests
{
private readonly string _absoluteFilePath;
private readonly long _fileLength;
public ResponseCachingTests()
{
_absoluteFilePath = Directory.GetFiles(Directory.GetCurrentDirectory()).First();
_fileLength = new FileInfo(_absoluteFilePath).Length;
}
[Fact]
public async Task Caching_SetTtlWithoutContentType_NotCached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
// Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address);
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "2";
// Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_SetTtlWithContentType_Cached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
// Http.Sys does not set the optional Age header for cached content.
// http://tools.ietf.org/html/rfc7234#section-5.1
public async Task Caching_CheckAge_NotSentWithCachedContent()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
Assert.False(response.Headers.Age.HasValue);
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
Assert.False(response.Headers.Age.HasValue);
}
}
[Fact]
// Http.Sys does not update the optional Age header for cached content.
// http://tools.ietf.org/html/rfc7234#section-5.1
public async Task Caching_SetAge_AgeHeaderCachedAndNotUpdated()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.Headers["age"] = "12345";
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
Assert.True(response.Headers.Age.HasValue);
Assert.Equal(TimeSpan.FromSeconds(12345), response.Headers.Age.Value);
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
Assert.True(response.Headers.Age.HasValue);
Assert.Equal(TimeSpan.FromSeconds(12345), response.Headers.Age.Value);
}
}
[Fact]
public async Task Caching_SetTtlZeroSeconds_NotCached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(0);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address);
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "2";
// Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_SetTtlMiliseconds_NotCached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromMilliseconds(900);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address);
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "2";
// Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_SetTtlNegative_NotCached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(-10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address);
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "2";
// Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_SetTtlHuge_Cached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.MaxValue;
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_SetTtlAndWriteBody_Cached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Response.ShouldBuffer = true;
context.Response.Body.Write(new byte[10], 0, 10);
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_Flush_NotCached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Response.Body.Flush();
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address);
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "2";
context.Dispose();
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_WriteFlush_NotCached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Response.Body.Write(new byte[10], 0, 10);
context.Response.Body.Flush();
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address);
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "2";
context.Dispose();
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_WriteFullContentLength_Cached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.ContentLength = 10;
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Response.Body.Write(new byte[10], 0, 10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(10, response.Content.Headers.ContentLength);
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(10, response.Content.Headers.ContentLength);
}
}
[Fact]
public async Task Caching_SendFileNoContentLength_NotCached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
await context.Response.SendFileAsync(_absoluteFilePath, 0, null, CancellationToken.None);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(_fileLength, response.Content.Headers.ContentLength);
responseTask = SendRequestAsync(address);
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "2";
context.Dispose();
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_SendFileWithFullContentLength_Cached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.ContentLength =_fileLength;
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
await context.Response.SendFileAsync(_absoluteFilePath, 0, null, CancellationToken.None);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(_fileLength, response.Content.Headers.ContentLength);
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(_fileLength, response.Content.Headers.ContentLength);
}
}
[Fact]
public async Task Caching_SetTtlAndStatusCode_Cached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
// Http.Sys will cache any status code.
for (int status = 200; status < 600; status++)
{
// 407 (Proxy Authentication Required) makes CoreCLR's HttpClient throw
if (status == 407)
{
continue;
}
var responseTask = SendRequestAsync(address + status);
var context = await server.GetContextAsync();
context.Response.StatusCode = status;
context.Response.Headers["x-request-count"] = status.ToString();
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(status, (int)response.StatusCode);
Assert.Equal(status.ToString(), response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await SendRequestAsync(address + status);
Assert.Equal(status, (int)response.StatusCode);
Assert.Equal(status.ToString(), response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
}
// Only GET requests can have cached responses.
[Theory]
// See HTTP_VERB for known verbs
[InlineData("HEAD")]
[InlineData("UNKNOWN")]
[InlineData("INVALID")]
[InlineData("OPTIONS")]
[InlineData("DELETE")]
[InlineData("TRACE")]
[InlineData("TRACK")]
[InlineData("MOVE")]
[InlineData("COPY")]
[InlineData("PROPFIND")]
[InlineData("PROPPATCH")]
[InlineData("MKCOL")]
[InlineData("LOCK")]
[InlineData("UNLOCK")]
[InlineData("SEARCH")]
[InlineData("CUSTOMVERB")]
[InlineData("PATCH")]
[InlineData("POST")]
[InlineData("PUT")]
// [InlineData("CONNECT", null)] 400 bad request if it's not a WebSocket handshake.
public async Task Caching_VariousUnsupportedRequestMethods_NotCached(string method)
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address, method);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = context.Request.Method + "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(method + "1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address, method);
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = context.Request.Method + "2";
// Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(method + "2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// RFC violation. http://tools.ietf.org/html/rfc7234#section-4.4
// "A cache MUST invalidate the effective Request URI ... when a non-error status code
// is received in response to an unsafe request method."
[Theory]
// See HTTP_VERB for known verbs
[InlineData("HEAD")]
[InlineData("UNKNOWN")]
[InlineData("INVALID")]
[InlineData("OPTIONS")]
[InlineData("DELETE")]
[InlineData("TRACE")]
[InlineData("TRACK")]
[InlineData("MOVE")]
[InlineData("COPY")]
[InlineData("PROPFIND")]
[InlineData("PROPPATCH")]
[InlineData("MKCOL")]
[InlineData("LOCK")]
[InlineData("UNLOCK")]
[InlineData("SEARCH")]
[InlineData("CUSTOMVERB")]
[InlineData("PATCH")]
[InlineData("POST")]
[InlineData("PUT")]
// [InlineData("CONNECT", null)] 400 bad request if it's not a WebSocket handshake.
public async Task Caching_UnsupportedRequestMethods_BypassCacheAndLeaveItIntact(string method)
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
// Cache the first response
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = context.Request.Method + "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("GET1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
// Try to clear the cache with a second request
responseTask = SendRequestAsync(address, method);
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = context.Request.Method + "2";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Dispose();
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(method + "2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
// Send a third request to check the cache.
responseTask = SendRequestAsync(address);
// The cache wasn't cleared when it should have been
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("GET1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// RFC violation / implementation limiation, Vary is not respected.
// http://tools.ietf.org/html/rfc7234#section-4.1
[Fact]
public async Task Caching_SetVary_NotRespected()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address, "GET", "x-vary", "vary1");
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.Headers["vary"] = "x-vary";
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal("x-vary", response.Headers.GetValues("vary").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await SendRequestAsync(address, "GET", "x-vary", "vary2");
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal("x-vary", response.Headers.GetValues("vary").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// http://tools.ietf.org/html/rfc7234#section-3.2
[Fact]
public async Task Caching_RequestAuthorization_NotCached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address, "GET", "Authorization", "Basic abc123");
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address, "GET", "Authorization", "Basic abc123");
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "2";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Dispose();
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task Caching_RequestAuthorization_NotServedFromCache()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address, "GET", "Authorization", "Basic abc123");
context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "2";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Dispose();
// Send a second request and make sure we get the same response (without listening for one on the server).
response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// Responses can be cached for requests with Pragma: no-cache.
// http://tools.ietf.org/html/rfc7234#section-5.2.1.4
[Fact]
public async Task Caching_RequestPragmaNoCache_Cached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address, "GET", "Pragma", "no-cache");
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// RFC violation, Requests with Pragma: no-cache should not be served from cache.
// http://tools.ietf.org/html/rfc7234#section-5.4
// http://tools.ietf.org/html/rfc7234#section-5.2.1.4
[Fact]
public async Task Caching_RequestPragmaNoCache_NotRespectedAndServedFromCache()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
response = await SendRequestAsync(address, "GET", "Pragma", "no-cache");
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// Responses can be cached for requests with cache-control: no-cache.
// http://tools.ietf.org/html/rfc7234#section-5.2.1.4
[Fact]
public async Task Caching_RequestCacheControlNoCache_Cached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address, "GET", "Cache-Control", "no-cache");
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// RFC violation, Requests with Cache-Control: no-cache should not be served from cache.
// http://tools.ietf.org/html/rfc7234#section-5.2.1.4
[Fact]
public async Task Caching_RequestCacheControlNoCache_NotRespectedAndServedFromCache()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
response = await SendRequestAsync(address, "GET", "Cache-Control", "no-cache");
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// RFC violation
// http://tools.ietf.org/html/rfc7234#section-5.2.1.1
[Fact]
public async Task Caching_RequestCacheControlMaxAgeZero_NotRespectedAndServedFromCache()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
response = await SendRequestAsync(address, "GET", "Cache-Control", "min-fresh=0");
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// RFC violation
// http://tools.ietf.org/html/rfc7234#section-5.2.1.3
[Fact]
public async Task Caching_RequestCacheControlMinFreshOutOfRange_NotRespectedAndServedFromCache()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
response = await SendRequestAsync(address, "GET", "Cache-Control", "min-fresh=20");
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
}
}
// Http.Sys limitation, partial responses are not cached.
[Fact]
public async Task Caching_CacheRange_NotCached()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address, "GET", "Range", "bytes=0-10");
var context = await server.GetContextAsync();
context.Response.StatusCode = 206;
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.Headers["content-range"] = "bytes 0-10/100";
context.Response.ContentLength = 11;
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Response.Body.Write(new byte[100], 0, 11);
context.Dispose();
var response = await responseTask;
Assert.Equal(206, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[11], await response.Content.ReadAsByteArrayAsync());
responseTask = SendRequestAsync(address, "GET", "Range", "bytes=0-10");
context = await server.GetContextAsync();
context.Response.StatusCode = 206;
context.Response.Headers["x-request-count"] = "2";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.Headers["content-range"] = "bytes 0-10/100";
context.Response.ContentLength = 11;
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Response.Body.Write(new byte[100], 0, 11);
context.Dispose();
response = await responseTask;
Assert.Equal(206, (int)response.StatusCode);
Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal("bytes 0-10/100", response.Content.Headers.GetValues("content-range").FirstOrDefault());
Assert.Equal(new byte[11], await response.Content.ReadAsByteArrayAsync());
}
}
// http://tools.ietf.org/html/rfc7233#section-4.1
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.CoreCLR, SkipReason = "Cached response contains duplicate Content-Length headers (#167).")]
public async Task Caching_RequestRangeFromCache_RangeServedFromCache()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.ContentLength = 100;
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Response.Body.Write(new byte[100], 0, 100);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[100], await response.Content.ReadAsByteArrayAsync());
response = await SendRequestAsync(address, "GET", "Range", "bytes=0-10", HttpCompletionOption.ResponseHeadersRead);
Assert.Equal(206, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal("bytes 0-10/100", response.Content.Headers.GetValues("content-range").FirstOrDefault());
Assert.Equal(11, response.Content.Headers.ContentLength);
}
}
// http://tools.ietf.org/html/rfc7233#section-4.1
[Fact]
public async Task Caching_RequestMultipleRangesFromCache_RangesServedFromCache()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.ContentLength = 100;
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Response.Body.Write(new byte[100], 0, 100);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(new byte[100], await response.Content.ReadAsByteArrayAsync());
response = await SendRequestAsync(address, "GET", "Range", "bytes=0-10,15-20");
Assert.Equal(206, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.True(response.Content.Headers.GetValues("content-type").First().StartsWith("multipart/byteranges;"));
}
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.CoreCLR, SkipReason = "Cached response contains duplicate Content-Length headers (#167).")]
public async Task Caching_RequestRangeFromCachedFile_ServedFromCache()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseLength = _fileLength / 2; // Make sure it handles partial files.
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.ContentLength = responseLength;
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
await context.Response.SendFileAsync(_absoluteFilePath, 0, responseLength, CancellationToken.None);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(responseLength, response.Content.Headers.ContentLength);
// Send a second request and make sure we get the same response (without listening for one on the server).
var rangeLength = responseLength / 2;
response = await SendRequestAsync(address, "GET", "Range", "bytes=0-" + (rangeLength - 1), HttpCompletionOption.ResponseHeadersRead);
Assert.Equal(206, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(rangeLength, response.Content.Headers.ContentLength);
Assert.Equal("bytes 0-" + (rangeLength - 1) + "/" + responseLength, response.Content.Headers.GetValues("content-range").FirstOrDefault());
}
}
[Fact]
public async Task Caching_RequestMultipleRangesFromCachedFile_ServedFromCache()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
var responseLength = _fileLength / 2; // Make sure it handles partial files.
var responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.ContentLength = responseLength;
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
await context.Response.SendFileAsync(_absoluteFilePath, 0, responseLength, CancellationToken.None);
context.Dispose();
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.Equal(responseLength, response.Content.Headers.ContentLength);
// Send a second request and make sure we get the same response (without listening for one on the server).
var rangeLength = responseLength / 4;
response = await SendRequestAsync(address, "GET", "Range", "bytes=0-" + (rangeLength - 1) + "," + rangeLength + "-" + (rangeLength + rangeLength - 1), HttpCompletionOption.ResponseHeadersRead);
Assert.Equal(206, (int)response.StatusCode);
Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
Assert.True(response.Content.Headers.GetValues("content-type").First().StartsWith("multipart/byteranges;"));
}
}
private async Task<HttpResponseMessage> SendRequestAsync(string uri, string method = "GET", string extraHeader = null, string extraHeaderValue = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
using (var handler = new HttpClientHandler() { AllowAutoRedirect = false })
{
using (var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) })
{
var request = new HttpRequestMessage(new HttpMethod(method), uri);
if (!string.IsNullOrEmpty(extraHeader))
{
request.Headers.Add(extraHeader, extraHeaderValue);
}
return await client.SendAsync(request, httpCompletionOption);
}
}
}
}
}