using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
namespace NBF
{
///
/// UnityWebRequest GET/POST 工具类:
/// - GET(query 参数)
/// - POST JSON
/// - POST 表单(x-www-form-urlencoded / multipart)
/// - 上传文件(multipart)
/// - 自定义 Header
/// - 超时、取消
/// - 统一返回文本 / 原始字节
///
public static class HttpUtil
{
// ========== 公共返回结构 ==========
public readonly struct HttpResult
{
public readonly bool ok;
public readonly long statusCode;
public readonly string text;
public readonly byte[] data;
public readonly string error; // 包含网络错误/HTTP错误/取消/超时等信息
public readonly Dictionary responseHeaders;
public HttpResult(bool ok, long statusCode, string text, byte[] data, string error,
Dictionary responseHeaders)
{
this.ok = ok;
this.statusCode = statusCode;
this.text = text;
this.data = data;
this.error = error;
this.responseHeaders = responseHeaders;
}
public override string ToString() =>
$"ok={ok}, status={statusCode}, error={error}, textLen={text?.Length ?? 0}";
}
///
/// 用于取消请求(调用 Cancel() 会中止 UnityWebRequest)
///
public sealed class RequestHandle
{
internal UnityWebRequest req;
public bool IsDone => req == null || req.isDone;
public void Cancel()
{
try
{
req?.Abort();
}
catch
{
/* ignore */
}
}
}
// ========== 对外 API(协程) ==========
public static IEnumerator Get(
string url,
Dictionary query = null,
Dictionary headers = null,
int timeoutSeconds = 15,
Action callback = null,
RequestHandle handle = null)
{
var finalUrl = BuildUrlWithQuery(url, query);
using (var req = UnityWebRequest.Get(finalUrl))
{
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req));
}
}
///
/// POST JSON(Content-Type: application/json)
///
public static IEnumerator PostJson(
string url,
string jsonBody,
Dictionary headers = null,
int timeoutSeconds = 15,
Action callback = null,
RequestHandle handle = null)
{
var bodyBytes = string.IsNullOrEmpty(jsonBody) ? Array.Empty() : Encoding.UTF8.GetBytes(jsonBody);
using (var req = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{
req.uploadHandler = new UploadHandlerRaw(bodyBytes);
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json; charset=utf-8");
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req));
}
}
///
/// POST 表单(application/x-www-form-urlencoded)
///
public static IEnumerator PostFormUrlEncoded(
string url,
Dictionary form,
Dictionary headers = null,
int timeoutSeconds = 15,
Action callback = null,
RequestHandle handle = null)
{
// Unity 内部会自动按 x-www-form-urlencoded 处理字段
var formData = new List();
if (form != null)
{
foreach (var kv in form)
formData.Add(new MultipartFormDataSection(kv.Key, kv.Value));
}
// 这里用 Post + 手动覆盖 content-type
using (var req = UnityWebRequest.Post(url, formData))
{
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req));
}
}
///
/// POST multipart(可用于普通表单 + 文件上传)
/// files: (fieldName, fileName, bytes, mimeType)
///
public static IEnumerator PostMultipart(
string url,
Dictionary fields,
List<(string fieldName, string fileName, byte[] bytes, string mimeType)> files,
Dictionary headers = null,
int timeoutSeconds = 30,
Action callback = null,
RequestHandle handle = null)
{
var form = new List();
if (fields != null)
{
foreach (var kv in fields)
form.Add(new MultipartFormDataSection(kv.Key, kv.Value));
}
if (files != null)
{
foreach (var f in files)
{
var mime = string.IsNullOrEmpty(f.mimeType) ? "application/octet-stream" : f.mimeType;
form.Add(new MultipartFormFileSection(f.fieldName, f.bytes, f.fileName, mime));
}
}
using (var req = UnityWebRequest.Post(url, form))
{
req.downloadHandler = new DownloadHandlerBuffer();
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req));
}
}
///
/// 下载二进制(比如图片、ab、配置包等)
///
public static IEnumerator GetBytes(
string url,
Dictionary query = null,
Dictionary headers = null,
int timeoutSeconds = 30,
Action callback = null,
RequestHandle handle = null)
{
var finalUrl = BuildUrlWithQuery(url, query);
using (var req = UnityWebRequest.Get(finalUrl))
{
req.downloadHandler = new DownloadHandlerBuffer();
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req, preferBytes: true));
}
}
// ========== 便捷:返回 JSON 反序列化(JsonUtility) ==========
// 注意:JsonUtility 不支持 Dictionary / 顶层数组等,你若需要更强建议用 Newtonsoft.Json
public static IEnumerator GetJson(
string url,
Dictionary query = null,
Dictionary headers = null,
int timeoutSeconds = 15,
Action callback = null,
RequestHandle handle = null)
{
yield return Get(url, query, headers, timeoutSeconds, r =>
{
if (!r.ok)
{
callback?.Invoke(false, default, r);
return;
}
try
{
var obj = JsonUtility.FromJson(r.text);
callback?.Invoke(true, obj, r);
}
catch (Exception e)
{
callback?.Invoke(false, default, new HttpResult(false, r.statusCode, r.text, r.data,
"JSON parse error: " + e.Message, r.responseHeaders));
}
}, handle);
}
// ========== 内部工具 ==========
static void ApplyCommon(UnityWebRequest req, Dictionary headers, int timeoutSeconds,
RequestHandle handle)
{
req.timeout = Mathf.Max(1, timeoutSeconds);
req.downloadHandler ??= new DownloadHandlerBuffer();
if (headers != null)
{
foreach (var kv in headers)
{
if (!string.IsNullOrEmpty(kv.Key))
req.SetRequestHeader(kv.Key, kv.Value);
}
}
if (handle != null) handle.req = req;
}
static HttpResult BuildResult(UnityWebRequest req, bool preferBytes = false)
{
// Unity 2020+ : result enum
bool isNetworkOrHttpError =
req.result == UnityWebRequest.Result.ConnectionError ||
req.result == UnityWebRequest.Result.ProtocolError ||
req.result == UnityWebRequest.Result.DataProcessingError;
var code = req.responseCode;
var headers = req.GetResponseHeaders();
// 下载内容
byte[] data = null;
string text = null;
if (req.downloadHandler != null)
{
data = req.downloadHandler.data;
// preferBytes==false 时优先给 text
if (!preferBytes)
{
try
{
text = req.downloadHandler.text;
}
catch
{
text = data != null ? Encoding.UTF8.GetString(data) : null;
}
}
else
{
// preferBytes==true 时 text 也尽量给一份(方便日志)
try
{
text = req.downloadHandler.text;
}
catch
{
/* ignore */
}
}
}
// 错误信息(包含 HTTP 错误)
string err = null;
if (isNetworkOrHttpError)
{
err = req.error;
// 有些后端会把错误细节放在 body
if (!string.IsNullOrEmpty(text))
err = $"{err}\nBody: {TrimForLog(text, 1200)}";
}
bool ok = !isNetworkOrHttpError && code >= 200 && code < 300;
return new HttpResult(ok, code, text, data, err, headers);
}
static string BuildUrlWithQuery(string url, Dictionary query)
{
if (query == null || query.Count == 0) return url;
var sb = new StringBuilder(url);
sb.Append(url.Contains("?") ? "&" : "?");
bool first = true;
foreach (var kv in query)
{
if (!first) sb.Append('&');
first = false;
sb.Append(UnityWebRequest.EscapeURL(kv.Key));
sb.Append('=');
sb.Append(UnityWebRequest.EscapeURL(kv.Value ?? ""));
}
return sb.ToString();
}
static string TrimForLog(string s, int maxLen)
{
if (string.IsNullOrEmpty(s)) return s;
if (s.Length <= maxLen) return s;
return s.Substring(0, maxLen) + "...(truncated)";
}
}
}