This commit is contained in:
2026-06-15 18:18:16 +08:00
parent 97c9fba14e
commit 2b9f134e5f
4164 changed files with 386922 additions and 79 deletions

View File

@@ -0,0 +1,132 @@
using System.Collections.Generic;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Response;
using Best.HTTP.Shared.Streams;
namespace Best.HTTP.Request.Settings
{
/// <summary>
/// Delegate for handling the event when headers are received in a response.
/// </summary>
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
/// <param name="resp">The <see cref="HTTPResponse"/> object.</param>
/// <param name="headers">The headers received from the server.</param>
public delegate void OnHeadersReceivedDelegate(HTTPRequest req, HTTPResponse resp, Dictionary<string, List<string>> headers);
/// <summary>
/// Delegate for handling progress during the download.
/// </summary>
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
/// <param name="progress">The number of bytes downloaded so far.</param>
/// <param name="length">The total length of the content being downloaded, or -1 if the length cannot be determined.</param>
public delegate void OnProgressDelegate(HTTPRequest req, long progress, long length);
/// <summary>
/// Delegate for handling the event when the download of content starts.
/// </summary>
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
/// <param name="resp">The <see cref="HTTPResponse"/> object.</param>
/// <param name="stream">The <see cref="DownloadContentStream"/> used for receiving downloaded content.</param>
public delegate void OnDownloadStartedDelegate(HTTPRequest req, HTTPResponse resp, DownloadContentStream stream);
/// <summary>
/// Delegate for creating a new <see cref="DownloadContentStream"/> object.
/// </summary>
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
/// <param name="resp">The <see cref="HTTPResponse"/> object.</param>
/// <param name="bufferAvailableHandler">An interface for notifying connections that the buffer has free space for downloading data.</param>
/// <returns>The newly created <see cref="DownloadContentStream"/>.</returns>
public delegate DownloadContentStream OnCreateDownloadStreamDelegate(HTTPRequest req, HTTPResponse resp, IDownloadContentBufferAvailable bufferAvailableHandler);
#if !UNITY_WEBGL || UNITY_EDITOR
/// <summary>
/// Delegate for handling the event when a response is upgraded.
/// </summary>
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
/// <param name="resp">The <see cref="HTTPResponse"/> object.</param>
/// <param name="contentProvider">A stream that provides content for the upgraded response.</param>
/// <returns><c>true</c> to keep the underlying connection open; otherwise, <c>false</c>.</returns>
public delegate bool OnUpgradedDelegate(HTTPRequest req, HTTPResponse resp, PeekableContentProviderStream contentProvider);
#endif
/// <summary>
/// Represents settings for configuring an HTTP request's download behavior.
/// </summary>
public class DownloadSettings
{
/// <summary>
/// Gets or sets the maximum number of bytes the <see cref="DownloadContentStream"/> will buffer before pausing the download until its buffer has free space again.
/// </summary>
/// <remarks>
/// When the download content stream buffers data up to this specified limit, it will temporarily pause downloading until it has free space in its buffer.
/// Increasing this value may help reduce the frequency of pauses during downloads, but it also increases memory usage.
/// </remarks>
public long ContentStreamMaxBuffered = 1024 * 1024;
/// <summary>
/// Gets or sets a value indicating whether caching should be enabled for this request.
/// </summary>
public bool DisableCache { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the response's <see cref="DownloadContentStream"/> should be populated with downloaded data or if the content should be written only to the local cache when available.
/// </summary>
/// <remarks>
/// If set to <c>true</c> and the content isn't cacheable (e.g., it doesn't have any cache-related headers), the content will be downloaded but will be lost.
/// </remarks>
/// <summary>
/// Gets or sets a value indicating whether the response's <see cref="HTTPResponse.DownStream"/> should be populated with downloaded data or if the content should be written only to the local cache when available.
/// </summary>
/// <remarks>
/// If set to <c>true</c> and the content isn't cacheable (e.g., it doesn't have any cache-related headers), the content will be downloaded but will be lost.
/// This is because the downloaded data would be written exclusively to the local cache and will not be stored in memory or the response's <see cref="HTTPResponse.DownStream"/> for further use.
/// </remarks>
public bool CacheOnly { get; set; }
/// <summary>
/// This event is called when the plugin received and parsed all headers.
/// </summary>
public OnHeadersReceivedDelegate OnHeadersReceived;
/// <summary>
/// Represents a function that creates a new <see cref="DownloadContentStream"/> object when needed for downloading content.
/// </summary>
public OnCreateDownloadStreamDelegate DownloadStreamFactory = (req, resp, bufferAvailableHandler)
=> new DownloadContentStream(resp, req.DownloadSettings.ContentStreamMaxBuffered, bufferAvailableHandler);
/// <summary>
/// Event for handling the start of the download process for 2xx status code responses.
/// </summary>
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
/// <param name="resp">The <see cref="HTTPResponse"/> object representing the response.</param>
/// <param name="stream">
/// The <see cref="DownloadContentStream"/> containing the downloaded data. It might already be populated with some content.
/// </param>
/// <remarks>
/// This event is called when the plugin expects the server to send content. When called, the <see cref="DownloadContentStream"/>
/// might already be populated with some content. It is specifically meant for responses with 2xx status codes.
/// </remarks>
public OnDownloadStartedDelegate OnDownloadStarted;
/// <summary>
/// Gets or sets the event that is called when new data is downloaded from the server.
/// </summary>
/// <remarks>
/// The first parameter is the original <see cref="HTTPRequest"/> object itself, the second parameter is the downloaded bytes, and the third parameter is the content length.
/// There are download modes where we can't figure out the exact length of the final content. In these cases, we guarantee that the third parameter will be at least the size of the second one.
/// </remarks>
public OnProgressDelegate OnDownloadProgress;
#if !UNITY_WEBGL || UNITY_EDITOR
#pragma warning disable 0649
/// <summary>
/// Called when a response with status code 101 (upgrade), "<c>connection: upgrade</c>" header and value or an "<c>upgrade</c>" header received.
/// </summary>
/// <remarks>This callback might be called on a thread other than the main one!</remarks>
/// <remarks>Isn't available under WebGL!</remarks>
public OnUpgradedDelegate OnUpgraded;
#pragma warning restore 0649
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e413d08106f68d4d84ad8500cf3a765
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,98 @@
using System;
using System.Text;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.HostSetting;
using Best.HTTP.Request.Authentication;
using Best.HTTP.Shared;
namespace Best.HTTP.Request.Settings
{
/// <summary>
/// Represents settings related to using a proxy server for HTTP requests.
/// </summary>
public class ProxySettings
{
/// <summary>
/// Checks if there is a proxy configured for the given URI.
/// </summary>
/// <param name="uri">The URI to check for proxy usage.</param>
/// <returns><c>true</c> if a proxy is configured and should be used for the URI; otherwise, <c>false</c>.</returns>
public bool HasProxyFor(Uri uri) => Proxy != null && Proxy.UseProxyForAddress(uri);
/// <summary>
/// Gets or sets the proxy object used for the request.
/// </summary>
public Proxies.Proxy Proxy { get; set; } = HTTPManager.Proxy;
/// <summary>
/// Sets up the HTTP request for passing through a proxy server.
/// </summary>
/// <param name="request">The HTTP request to set up.</param>
public void SetupRequest(HTTPRequest request)
{
var currentUri = request.CurrentUri;
bool tryToKeepAlive = HTTPManager.PerHostSettings.Get(currentUri.Host)
.HTTP1ConnectionSettings
.TryToReuseConnections;
if (!HTTPProtocolFactory.IsSecureProtocol(currentUri) && this.HasProxyFor(currentUri) && !request.HasHeader("Proxy-Connection"))
request.AddHeader("Proxy-Connection", tryToKeepAlive ? "Keep-Alive" : "Close");
// Proxy Authentication
if (!HTTPProtocolFactory.IsSecureProtocol(currentUri) && HasProxyFor(currentUri) && this.Proxy.Credentials != null)
{
switch (Proxy.Credentials.Type)
{
case AuthenticationTypes.Basic:
// With Basic authentication we don't want to wait for a challenge, we will send the hash with the first request
var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(this.Proxy.Credentials.UserName + ":" + this.Proxy.Credentials.Password));
request.SetHeader("Proxy-Authorization", $"Basic {token}");
break;
case AuthenticationTypes.Unknown:
case AuthenticationTypes.Digest:
var digest = DigestStore.Get(this.Proxy.Address);
if (digest != null)
{
string authentication = digest.GenerateResponseHeader(this.Proxy.Credentials, false, request.MethodType, currentUri);
if (!string.IsNullOrEmpty(authentication))
request.SetHeader("Proxy-Authorization", authentication);
}
break;
}
}
}
/// <summary>
/// Handles the proxy's response with status code <c>407</c>.
/// </summary>
/// <param name="request">The HTTP request that received a <c>407</c> response.</param>
/// <returns><c>true</c> to resend the request through the proxy; otherwise, <c>false</c>.</returns>
public bool Handle407(HTTPRequest request)
{
if (this.Proxy == null)
return false;
return this.Proxy.SetupRequest(request);
}
/// <summary>
/// Adds the proxy address to a hash for the given request URI.
/// </summary>
/// <param name="requestUri">The request URI for which the proxy address is added to the hash.</param>
/// <param name="hash">The hash to which the proxy address is added.</param>
public void AddToHash(Uri requestUri, ref UnityEngine.Hash128 hash)
{
if (HasProxyFor(requestUri))
HostKey.Append(this.Proxy.Address, ref hash);
}
public override string ToString()
{
return this.Proxy?.Address?.ToString();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 187900d0bbfe66243ad32654d11316f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,77 @@
using System;
namespace Best.HTTP.Request.Settings
{
public delegate bool OnBeforeRedirectionDelegate(HTTPRequest req, HTTPResponse resp, Uri redirectUri);
/// <summary>
/// Represents settings related to handling HTTP request redirection.
/// </summary>
public class RedirectSettings
{
/// <summary>
/// Indicates whether the request has been redirected.
/// A request's IsRedirected might be true while <see cref="RedirectCount"/> is zero if the redirection is made to the local cache.
/// </summary>
public bool IsRedirected { get; internal set; }
/// <summary>
/// The Uri that the request is redirected to.
/// </summary>
public Uri RedirectUri { get; internal set; }
/// <summary>
/// How many redirection is supported for this request. The default is 10. Zero or a negative value means no redirections are supported.
/// </summary>
/// <summary>
/// Gets or sets the maximum number of redirections supported for this request. The default is <c>10</c>.
/// A value of zero or a negative value means no redirections are supported.
/// </summary>
public int MaxRedirects { get; set; }
/// <summary>
/// Gets the number of times the request has been redirected.
/// </summary>
public int RedirectCount { get; internal set; }
/// <summary>
/// Occurs before the plugin makes a new request to the new URI during redirection.
/// The return value of this event handler controls whether the redirection is aborted (<c>false</c>) or allowed (<c>true</c>).
/// This event is called on a thread other than the main Unity thread.
/// </summary>
public event OnBeforeRedirectionDelegate OnBeforeRedirection
{
add { onBeforeRedirection += value; }
remove { onBeforeRedirection -= value; }
}
private OnBeforeRedirectionDelegate onBeforeRedirection;
/// <summary>
/// Initializes a new instance of the RedirectSettings class with the specified maximum redirections.
/// </summary>
/// <param name="maxRedirects">The maximum number of redirections allowed.</param>
public RedirectSettings(int maxRedirects)
{
this.MaxRedirects = maxRedirects;
this.RedirectCount = 0;
}
/// <summary>
/// Resets <see cref="IsRedirected"/> and <see cref="RedirectCount"/> to their default values.
/// </summary>
public void Reset()
{
this.IsRedirected = false;
this.RedirectCount = 0;
}
internal bool CallOnBeforeRedirection(HTTPRequest req, HTTPResponse resp, Uri redirectUri)
{
if (onBeforeRedirection != null)
return onBeforeRedirection(req, resp, redirectUri);
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ab5aa9485b2a13e4196c40a39756ad17
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
namespace Best.HTTP.Request.Settings
{
/// <summary>
/// Represents settings related to request retry behavior.
/// </summary>
public class RetrySettings
{
/// <summary>
/// Gets the number of times that the plugin has retried the request.
/// </summary>
public int Retries { get; set; }
/// <summary>
/// Gets or sets the maximum number of retry attempts allowed. To disable retries, set this value to <c>0</c>.
/// The default value is <c>1</c> for GET requests, otherwise <c>0</c>.
/// </summary>
public int MaxRetries { get; set; }
/// <summary>
/// Initializes a new instance of the RetrySettings class with the specified maximum retry attempts.
/// </summary>
/// <param name="maxRetries">The maximum number of retry attempts allowed.</param>
public RetrySettings(int maxRetries)
=> this.MaxRetries = maxRetries;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6dc0d7446a891b94082c956de0282ef9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,76 @@
using System;
using Best.HTTP.Shared;
namespace Best.HTTP.Request.Settings
{
/// <summary>
/// Represents settings related to connection-timeouts and processing duration.
/// </summary>
public class TimeoutSettings
{
/// <summary>
/// Gets the timestamp when the request was queued for processing.
/// </summary>
public DateTime QueuedAt { get; internal set; }
/// <summary>
/// Gets the timestamp when the processing of the request started by a connection.
/// </summary>
public DateTime ProcessingStarted { get; internal set; }
/// <summary>
/// Gets or sets the maximum time to wait for establishing the connection to the target server.
/// If set to <c>TimeSpan.Zero</c> or lower, no connect timeout logic is executed. Default value is 20 seconds.
/// </summary>
public TimeSpan ConnectTimeout
{
get => this._connectTimeout ?? HTTPManager.PerHostSettings.Get(this._request.CurrentHostKey.Host).RequestSettings.ConnectTimeout;
set => this._connectTimeout = value;
}
private TimeSpan? _connectTimeout;
/// <summary>
/// Gets or sets the maximum time to wait for the request to finish after the connection is established.
/// </summary>
public TimeSpan Timeout
{
get => this._timeout ?? HTTPManager.PerHostSettings.Get(this._request.CurrentHostKey.Host).RequestSettings.RequestTimeout;
set => this._timeout = value;
}
private TimeSpan? _timeout;
/// <summary>
/// Returns <c>true</c> if the request has been stuck in the connection phase for too long.
/// </summary>
/// <param name="now">The current timestamp.</param>
/// <returns><c>true</c> if the connection has timed out; otherwise, <c>false</c>.</returns>
public bool IsConnectTimedOut(DateTime now) => this.QueuedAt != DateTime.MinValue && now - this.QueuedAt > this.ConnectTimeout;
/// <summary>
/// Returns <c>true</c> if the time has passed the specified Timeout setting since processing started or if the connection has timed out.
/// </summary>
/// <param name="now">The current timestamp.</param>
/// <returns><c>true</c> if the request has timed out; otherwise, <c>false</c>.</returns>
public bool IsTimedOut(DateTime now)
{
bool result = (this.ProcessingStarted != DateTime.MinValue && now - this.ProcessingStarted > this.Timeout) || this.IsConnectTimedOut(now); ;
return result;
}
private HTTPRequest _request;
/// <summary>
/// Initializes a new instance of the TimeoutSettings class for a specific <see cref="HTTPRequest"/>.
/// </summary>
/// <param name="request">The <see cref="HTTPRequest"/> associated with these timeout settings.</param>
public TimeoutSettings(HTTPRequest request)
=> this._request = request;
public void SetProcessing(DateTime now)
{
this.QueuedAt = DateTime.MinValue;
this.ProcessingStarted = now;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39256be26c7ee1b47a5beca18520959d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,157 @@
using System;
using System.IO;
using Best.HTTP.Request.Upload;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
namespace Best.HTTP.Request.Settings
{
public delegate void OnHeadersSentDelegate(HTTPRequest req);
/// <summary>
/// Options for sending the request headers and content, including upload progress monitoring.
/// </summary>
/// <remarks><see cref="SetupRequest"/> might be called when redirected or retried!</remarks>
public class UploadSettings : IDisposable
{
/// <summary>
/// Size of the internal buffer, and upload progress will be fired when this size of data sent to the wire. Its default value is 4 KiB.
/// </summary>
public int UploadChunkSize = 4 * 1024;
/// <summary>
/// The stream that the plugin will use to send data to the server.
/// </summary>
/// <remarks>
/// The stream can be any regular <see cref="System.IO.Stream"/> implementation or a specialized one inheriting from <see cref="UploadStreamBase"/>:
/// <list type="bullet">
/// <item><term><see cref="DynamicUploadStream"/></term><description>A specialized <see cref="UploadStreamBase"/> for data generated on-the-fly or periodically. The request remains active until the <see cref="DynamicUploadStream.Complete"/> method is invoked, ensuring continuous data feed even during temporary empty states.</description></item>
/// <item><term><see cref="JSonDataStream{T}"/></term><description>An <see cref="UploadStreamBase"/> implementation to convert and upload the object as JSON data. It sets the <c>"Content-Type"</c> header to <c>"application/json; charset=utf-8"</c>.</description></item>
/// <item><term><see cref="Upload.Forms.UrlEncodedStream"/></term><description>An <see cref="UploadStreamBase"/> implementation representing a stream that prepares and sends data as URL-encoded form data in an HTTP request.</description></item>
/// <item><term><see cref="Upload.Forms.MultipartFormDataStream"/></term><description>An <see cref="UploadStreamBase"/> based implementation of the <c>multipart/form-data</c> Content-Type. It's very memory-effective, streams are read into memory in chunks.</description></item>
/// </list>
/// </remarks>
public Stream UploadStream;
/// <summary>
/// Set to <c>false</c> if the plugin MUST NOT dispose <see cref="UploadStream"/> after the request is finished.
/// </summary>
public bool DisposeStream = true;
/// <summary>
/// Called periodically when data sent to the server.
/// </summary>
public OnProgressDelegate OnUploadProgress;
/// <summary>
/// This event is fired after the headers are sent to the server.
/// </summary>
public event OnHeadersSentDelegate OnHeadersSent
{
add { _onHeadersSent += value; }
remove { _onHeadersSent -= value; }
}
private OnHeadersSentDelegate _onHeadersSent;
// <summary>
// Whether to send an "<c>Expect: 100-continue</c>" header and value when there's content to send (<see cref="UploadSettings.UploadStream"/> != <c>null</c>).
// By using "<c>Expect: 100-continue</c>" the server is able to respond with an error (like <c>401-unauthorized</c>, <c>405-method not allowed</c>, etc.) or redirect before the client sends the whole payload.
// </summary>
// <remarks>
// More details can be found here:
// <list type="bullet">
// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9110#name-expect">RFC-9110 - Expect header</see></description></item>
// <item><description><see href="https://daniel.haxx.se/blog/2020/02/27/expect-tweaks-in-curl/">EXPECT: TWEAKS IN CURL (by Daniel Stenberg)</see></description></item>
// </list>
// </remarks>
//public bool SendExpect100Continue = true;
// False by default, set to true only when "Expect: 100-continue" sent out.
//internal bool Expect100Continue = false;
//internal void ResetExpects() => this.SendExpect100Continue = this.Expect100Continue = false;
private bool isDisposed;
/// <summary>
/// Called every time the request is sent out (redirected or retried).
/// </summary>
/// <param name="request">The <see cref="HTTPRequest"/> being prepared.</param>
/// <param name="dispatchHeadersSentCallback"><c>true</c> if the <see cref="OnHeadersSent"/> can be fired.</param>
public virtual void SetupRequest(HTTPRequest request, bool dispatchHeadersSentCallback)
{
if (isDisposed)
throw new ObjectDisposedException(nameof(UploadSettings));
if (this.UploadStream is UploadStreamBase upStream)
upStream.BeforeSendHeaders(request);
// Decide on whether append an "expect: 100-continue" or not.
// https://www.rfc-editor.org/rfc/rfc9110#name-expect
/*
if (this.SendExpect100Continue && this.UploadStream != null)
{
request.AddHeader("expect", "100-continue");
this.Expect100Continue = true;
}
else
request.RemoveHeader("expect");
*/
if (dispatchHeadersSentCallback)
{
// Call the callback on the unity main thread
if (HTTPUpdateDelegator.Instance.IsMainThread())
call_onBeforeHeaderSend(request);
else
new RunOnceOnMainThread(() => call_onBeforeHeaderSend(request), request.Context)
.Subscribe();
}
}
protected void call_onBeforeHeaderSend(HTTPRequest request)
{
try
{
_onHeadersSent?.Invoke(request);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception(nameof(UploadSettings), nameof(OnHeadersSent), ex, request.Context);
}
}
protected virtual void Dispose(bool disposing)
{
if (!isDisposed)
{
if (disposing)
{
if (this.DisposeStream)
{
var stream = this.UploadStream;
if (stream != null)
{
this.UploadStream?.Dispose();
this.UploadStream = null;
isDisposed = true;
}
}
}
}
}
/// <summary>
/// Dispose of resources used by the UploadSettings instance.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7cf0fd5a0cd00cc40b9f5fb9f79956d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: