在我的 ASP.NET Core 3.1 api 中,我将最大请求大小限制为 10 Mb:
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(k =>
{
k.Limits.MaxRequestBodySize = 1024 * 1024 * 10;
});
webBuilder.UseStartup<Startup>();
});
当请求大于 10 Mb 时,kestrel 会简单地关闭连接而不返回任何响应。
当请求大小超过限制时,如何返回有意义的响应?
当请求大于 10 Mb 时,kestrel 会简单地关闭连接而不返回任何响应。
Kestrel 确实返回 413 响应,但客户端并不总是能够读取它。当 Kestrel 对 HTTP/1.1 请求强制执行
MaxRequestBodySize
时,它会在响应 413 后立即关闭底层 TCP 连接,因为在不读取整个请求正文的情况下取消 HTTP/1.1 请求的唯一方法是关闭套接字。
即使 Kestrel 发送 413 响应,客户端通常也无法观察到它们,因为在尝试读取响应之前,它们会尝试通过其对等方关闭的套接字上传过大的正文,从而遇到连接重置错误。
可以尽早编写自定义中间件,在管道早期使用 413 响应来短路过大的请求。然后,如果请求大小超过中间件强制限制但低于 Kestrel 强制限制,套接字将保持打开状态,允许客户端读取 413 响应。
尽管 Kestrel 被迫读取整个请求正文才能可靠地工作,但只要 Kestrel 强制执行的限制足够低,以至于您不关心带宽/出口成本,这也是可以接受的。当应用程序不使用请求正文时,Kestrel 会有效地耗尽请求正文。限制请求正文大小的主要原因是为了防止应用程序级代码被其阻塞。
但是,有几个问题需要注意。一个常见的错误是只使用
if (context.Request.ContentLength > 10_000_000)
之类的东西来检查请求正文是否太大。
HttpContext.Request.ContentLength
可以是 null
,因为它源自 Content-Length 请求标头,即使对于具有非空正文的请求,该标头也并不总是设置。当它是 null
时,context.Request.ContentLength > 10_000_000
返回 false。为了支持限制可能不包含此标头的分块请求和 HTTP/2 请求的大小,我们必须包装 HttpContext.Request.Body
并计算每次读取的大小以确保它不超过限制。
const long RequestSizeLimit = 10_000_000;
static Task Write413Response(HttpContext context)
{
if (context.Response.HasStarted)
{
// The status code has already been sent and cannot be changed.
// However, rethrowing will prevent a successful chunk terminator.
return Task.CompletedTask;
}
context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
return context.Response.WriteAsync("Payload Too Large");
}
app.Use(async (context, next) =>
{
if (context.Request.ContentLength > RequestSizeLimit)
{
// The client sent a Content-Length header over the limit.
await Write413Response(context);
}
else if (context.Request.ContentLength is null)
{
// There was no Content-Length header. The request body could be any size.
var orignalRequestBody = context.Request.Body;
try
{
context.Request.Body = new SizeLimitedStream(context.Request.Body, RequestSizeLimit);
await next(context);
}
catch (PayloadTooLargeException)
{
await Write413Response(context);
throw;
}
finally
{
context.Request.Body = orignalRequestBody;
}
}
else
{
// The client sent a Content-Length header under the limit.
// Kestrel enforces the accuracy of the Content-Length header.
await next(context);
}
});
我也不建议通过将 Kestrel 的
MaxRequestBodySize
设置为 null
来完全禁用它。它应该大于中间件强制执行的 RequestSizeLimit
,因此 Kestrel 在中间件响应 413 状态代码后不会立即关闭套接字,但 Kestrel 可能不应该允许客户端发送无限量的数据作为一部分请求正文的内容,即使它刚刚被耗尽。
相反,我只需将
MaxRequestBodySize
增加到中间件强制下限的某个倍数。无论大小限制如何,在中间件退出后,Kestrel 只会在关闭套接字之前耗尽最多 5 秒,因此您可能会认为,使用正确编写的中间件,Kestrel 的服务器强制请求大小限制是不必要的,但这也减轻了任何可能的错误中间件。
builder.WebHost.ConfigureKestrel(kestrelOptions =>
{
// Kestrel's default limit is 30 MiB. This increases it to 100 MiB.
options.Limits.MaxRequestBodySize = RequestSizeLimit * 10;
});
这里是中间件使用的
SizeLimitedStream
的实现,它复制自ASP.NET Core的新请求解压中间件,但修改为抛出自定义异常类型,该异常类型不能与next()
中间件抛出的其他异常混合在一起:
// Copied from https://github.com/dotnet/aspnetcore/blob/597413644dec9fb34bcce580cea9629a96747600/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs
// Added PayloadTooLargeException
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
internal sealed class PayloadTooLargeException : IOException
{
public PayloadTooLargeException() : base("Payload Too Large") { }
}
internal sealed class SizeLimitedStream : Stream
{
private readonly Stream _innerStream;
private readonly long? _sizeLimit;
private long _totalBytesRead;
public SizeLimitedStream(Stream innerStream, long? sizeLimit)
{
if (innerStream is null)
{
throw new ArgumentNullException(nameof(innerStream));
}
_innerStream = innerStream;
_sizeLimit = sizeLimit;
}
public override bool CanRead => _innerStream.CanRead;
public override bool CanSeek => _innerStream.CanSeek;
public override bool CanWrite => _innerStream.CanWrite;
public override long Length => _innerStream.Length;
public override long Position
{
get
{
return _innerStream.Position;
}
set
{
_innerStream.Position = value;
}
}
public override void Flush()
{
_innerStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
var bytesRead = _innerStream.Read(buffer, offset, count);
_totalBytesRead += bytesRead;
if (_totalBytesRead > _sizeLimit)
{
throw new PayloadTooLargeException();
}
return bytesRead;
}
public override long Seek(long offset, SeekOrigin origin)
{
return _innerStream.Seek(offset, origin);
}
public override void SetLength(long value)
{
_innerStream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
_innerStream.Write(buffer, offset, count);
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
var bytesRead = await _innerStream.ReadAsync(buffer, cancellationToken);
_totalBytesRead += bytesRead;
if (_totalBytesRead > _sizeLimit)
{
throw new PayloadTooLargeException();
}
return bytesRead;
}
}
对于 ASP.NET Core 6.0:
禁用
MaxRequestBodySize
webBuilder.ConfigureKestrel((ctx, options) =>
{
options.Limits.MaxRequestBodySize = null;
});
使用自定义中间件:
app.Use(async (context, next) =>
{
if (context.Request.ContentLength > 10_000_000)
{
context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
await context.Response.WriteAsync("Payload Too Large");
return;
}
await next.Invoke();
});