Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using PCL.Core.IO.Net.Http.Cache.Models;

namespace PCL.Core.IO.Net.Http.Cache;

/// <summary>
/// HTTP 缓存处理器
/// </summary>
public class HttpCacheHandler:DelegatingHandler
{
private HttpCacheRepository _repository;
public HttpCacheHandler(HttpMessageHandler invoker, HttpCacheRepository repo)
{
InnerHandler = invoker;
_repository = repo;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if(!_repository.TryGetCacheData(request.RequestUri!.ToString(),out var details))
return await base.SendAsync(request, cancellationToken);
if (details.ExpiredAt is not null &&
details.LastUpdate.AddSeconds((double)details.ExpiredAt) < DateTimeOffset.Now
&& _repository.TryGetCacheResponse(request,out var cacheResponse) && !details.EnsureValidate
)
return cacheResponse;

if(details.Tag is not null) request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(details.Tag));
if(details.LastModify is not null) request.Headers.IfModifiedSince = DateTimeOffset.Parse(details.LastModify);
var response = await base.SendAsync(request, cancellationToken);
if (response.Headers.CacheControl?.NoStore ?? false) return response;
if(response.StatusCode == HttpStatusCode.NotModified && _repository.TryGetCacheResponse(request,out cacheResponse))
return cacheResponse;
var handle = await _repository.TryBeginUpdateAsync(request.RequestUri.ToString());
var newDetails = handle?.Details;
newDetails?.RequestUri = request.RequestUri.ToString();
newDetails?.LastUpdate = DateTimeOffset.Now;
newDetails?.EnsureValidate = response.Headers.CacheControl?.NoCache ?? false;
Comment on lines +35 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Last-Modified 处理使用的是 Date 头而不是 Last-Modified,并且可能解析/存储无效的值。

LastModify 通过 If-Modified-Since 头发送,但在更新时你使用的是 response.Headers.Date 并直接调用 ToString()。这会忽略 Last-Modified 头部、可能为 null,并产生后续可能无法稳定解析的字符串。建议优先使用 response.Content.Headers.LastModified(或根据需求使用 response.Headers.LastModified),并将其存为 DateTimeOffset 或标准化字符串(例如 ToString("o")),在下次构建请求时使用不变区域性进行解析。

建议实现如下:

        if(details.Tag is not null) request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(details.Tag));
        if (details.LastModify is not null &&
            DateTimeOffset.TryParse(
                details.LastModify,
                CultureInfo.InvariantCulture,
                DateTimeStyles.RoundtripKind,
                out var lastModified))
        {
            request.Headers.IfModifiedSince = lastModified;
        }

        var response = await base.SendAsync(request, cancellationToken);
        var handle = await _repository.TryBeginUpdateAsync(request.RequestUri.ToString());
        var newDetails = handle?.Details;
        newDetails?.RequestUri = request.RequestUri.ToString();
        newDetails?.LastUpdate = DateTimeOffset.Now;
        newDetails?.EnsureValidate = response.Headers.CacheControl?.NoCache ?? false;

        if (newDetails is not null)
        {
            var lastModifiedHeader =
                response.Content?.Headers.LastModified
                ?? response.Headers.LastModified
                ?? response.Headers.Date;

            newDetails.LastModify = lastModifiedHeader.HasValue
                ? lastModifiedHeader.Value.ToString("o", CultureInfo.InvariantCulture)
                : null;
        }
  1. 确保在 HttpCacheHandler.cs 顶部添加 using System.Globalization;,以便 CultureInfoDateTimeStyles 能够正确解析。
  2. 上述改动假定 LastModify 是一个可空的 string。如果你更倾向将其存为 DateTimeOffset?,请相应更新 Details 模型,并去掉 .ToString("o", ...) 与解析调用,直接使用 DateTimeOffset? 赋值。
Original comment in English

suggestion (bug_risk): Last-modified handling uses the Date header instead of Last-Modified and may parse/store invalid values.

LastModify is sent as If-Modified-Since, but when updating you use response.Headers.Date and ToString(). That ignores the Last-Modified header, may be null, and produces a value that might not parse reliably later. Prefer response.Content.Headers.LastModified (or response.Headers.LastModified if intended), store it as a DateTimeOffset or a standardized string (e.g. ToString("o")), and parse using invariant culture when creating the next request.

Suggested implementation:

        if(details.Tag is not null) request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(details.Tag));
        if (details.LastModify is not null &&
            DateTimeOffset.TryParse(
                details.LastModify,
                CultureInfo.InvariantCulture,
                DateTimeStyles.RoundtripKind,
                out var lastModified))
        {
            request.Headers.IfModifiedSince = lastModified;
        }

        var response = await base.SendAsync(request, cancellationToken);
        var handle = await _repository.TryBeginUpdateAsync(request.RequestUri.ToString());
        var newDetails = handle?.Details;
        newDetails?.RequestUri = request.RequestUri.ToString();
        newDetails?.LastUpdate = DateTimeOffset.Now;
        newDetails?.EnsureValidate = response.Headers.CacheControl?.NoCache ?? false;

        if (newDetails is not null)
        {
            var lastModifiedHeader =
                response.Content?.Headers.LastModified
                ?? response.Headers.LastModified
                ?? response.Headers.Date;

            newDetails.LastModify = lastModifiedHeader.HasValue
                ? lastModifiedHeader.Value.ToString("o", CultureInfo.InvariantCulture)
                : null;
        }
  1. Ensure using System.Globalization; is present at the top of HttpCacheHandler.cs so CultureInfo and DateTimeStyles resolve correctly.
  2. This change assumes LastModify is a nullable string. If you prefer to store it as DateTimeOffset?, update the Details model accordingly and remove the .ToString("o", ...)/parse calls, assigning the DateTimeOffset? directly instead.

newDetails?.LastModify = response.Content.Headers.LastModified.ToString();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk):LastModified 调用 ToString() 而不进行空检查,可能抛出异常或生成与区域设置相关的值。

LastModified 是一个 DateTimeOffset?;当它为 null 时,.ToString() 会返回空字符串,这在之后调用 DateTimeOffset.Parse 时会导致异常。

建议:

  • 保留 null 而不是空字符串:newDetails.LastModify = response.Content.Headers.LastModified?.ToString("O");
  • 使用不依赖区域设置的往返格式(例如 "O"),以保证解析与区域设置无关。
Original comment in English

issue (bug_risk): Using ToString() on LastModified without null checks can throw or produce culture-dependent values.

LastModified is a DateTimeOffset?; when it's null, .ToString() returns an empty string, which will later cause DateTimeOffset.Parse to throw.

Recommend:

  • Preserve nulls instead of empty strings: newDetails.LastModify = response.Content.Headers.LastModified?.ToString("O");
  • Use an invariant, round-trip format (e.g. "O") so parsing is culture-independent.

newDetails?.Tag = response.Headers.ETag?.Tag;
if (handle is not null)
response.Content = new StreamContent(new CacheStream(handle,
await response.Content.ReadAsStreamAsync(cancellationToken)));
return response;
}
}
Loading
Loading