static class HttpResponseMessageExtensions
internal static void WriteRequestToConsole(this HttpResponseMessage response)
if (response is null)
return;
var request = response.RequestMessage;
Console.Write($"{request?.Method} ");
Console.Write($"{request?.RequestUri} ");
Console.WriteLine($"HTTP/{request?.Version}");
此功能用于以以下形式将请求详细信息写入控制台:
<HTTP Request Method> <Request URI> <HTTP/Version>
例如,
GET
请求
https://jsonplaceholder.typicode.com/todos/3
输出以下消息:
GET https://jsonplaceholder.typicode.com/todos/3 HTTP/1.1
HTTP Get from JSON
https://jsonplaceholder.typicode.com/todos 终结点返回“todo”对象的 JSON 数组。 这些对象的 JSON 结构如下所示:
"userId": 1,
"id": 1,
"title": "example title",
"completed": false
"userId": 1,
"id": 2,
"title": "another example title",
"completed": true
C# Todo
对象定义如下:
public record class Todo(
int? UserId = null,
int? Id = null,
string? Title = null,
bool? Completed = null);
它是 record class
类型,具有可选的 Id
、Title
、Completed
和 UserId
属性。 有关 record
类型的详细信息,请参阅 C# 中的记录类型简介。 若要自动将请求反序列化GET
为强类型 C# 对象,请使用GetFromJsonAsync属于 System.Net.Http.Json NuGet 包的扩展方法。
static async Task GetFromJsonAsync(HttpClient httpClient)
var todos = await httpClient.GetFromJsonAsync<List<Todo>>(
"todos?userId=1&completed=false");
Console.WriteLine("GET https://jsonplaceholder.typicode.com/todos?userId=1&completed=false HTTP/1.1");
todos?.ForEach(Console.WriteLine);
Console.WriteLine();
// Expected output:
// GET https://jsonplaceholder.typicode.com/todos?userId=1&completed=false HTTP/1.1
// Todo { UserId = 1, Id = 1, Title = delectus aut autem, Completed = False }
// Todo { UserId = 1, Id = 2, Title = quis ut nam facilis et officia qui, Completed = False }
// Todo { UserId = 1, Id = 3, Title = fugiat veniam minus, Completed = False }
// Todo { UserId = 1, Id = 5, Title = laboriosam mollitia et enim quasi adipisci quia provident illum, Completed = False }
// Todo { UserId = 1, Id = 6, Title = qui ullam ratione quibusdam voluptatem quia omnis, Completed = False }
// Todo { UserId = 1, Id = 7, Title = illo expedita consequatur quia in, Completed = False }
// Todo { UserId = 1, Id = 9, Title = molestiae perspiciatis ipsa, Completed = False }
// Todo { UserId = 1, Id = 13, Title = et doloremque nulla, Completed = False }
// Todo { UserId = 1, Id = 18, Title = dolorum est consequatur ea mollitia in culpa, Completed = False }
在上述代码中:
向 "https://jsonplaceholder.typicode.com/todos?userId=1&completed=false"
发出 GET
请求。
查询字符串表示请求的筛选条件。
如果成功,响应会自动反序列化为 List<Todo>
。
请求详细信息连同每个 Todo
对象将写入控制台。
HTTP Post
POST
请求将数据发送到服务器进行处理。 请求的 Content-Type
标头表示正文发送的 MIME 类型。 若要在给定 和 Uri的情况下HttpClient
发出 HTTP POST
请求,HttpClient.PostAsync请使用 方法:
static async Task PostAsync(HttpClient httpClient)
using StringContent jsonContent = new(
JsonSerializer.Serialize(new
userId = 77,
id = 1,
title = "write code sample",
completed = false
Encoding.UTF8,
"application/json");
using HttpResponseMessage response = await httpClient.PostAsync(
"todos",
jsonContent);
response.EnsureSuccessStatusCode()
.WriteRequestToConsole();
var jsonResponse = await response.Content.ReadAsStringAsync();
Console.WriteLine($"{jsonResponse}\n");
// Expected output:
// POST https://jsonplaceholder.typicode.com/todos HTTP/1.1
// {
// "userId": 77,
// "id": 201,
// "title": "write code sample",
// "completed": false
// }
前面的代码:
准备一个包含请求的 JSON 正文的 StringContent 实例(MIME 类型为 "application/json"
)。
向 "https://jsonplaceholder.typicode.com/todos"
发出 POST
请求。
确保响应成功,并将请求详细信息写入控制台。
将响应正文以字符串形式写入控制台。
HTTP Post as JSON
要自动将 POST
请求参数序列化并将响应反序列化为强类型 C# 对象,请使用 System.Net.Http.Json NuGet 包中的 PostAsJsonAsync 扩展方法。
static async Task PostAsJsonAsync(HttpClient httpClient)
using HttpResponseMessage response = await httpClient.PostAsJsonAsync(
"todos",
new Todo(UserId: 9, Id: 99, Title: "Show extensions", Completed: false));
response.EnsureSuccessStatusCode()
.WriteRequestToConsole();
var todo = await response.Content.ReadFromJsonAsync<Todo>();
Console.WriteLine($"{todo}\n");
// Expected output:
// POST https://jsonplaceholder.typicode.com/todos HTTP/1.1
// Todo { UserId = 9, Id = 201, Title = Show extensions, Completed = False }
前面的代码:
将 Todo
实例序列化为 JSON,并向 "https://jsonplaceholder.typicode.com/todos"
发出 POST
请求。
确保响应成功,并将请求详细信息写入控制台。
将响应正文反序列化为 Todo
实例,并将 Todo
写入控制台。
HTTP Put
PUT
请求方法会替换现有资源,或使用请求正文有效负载创建一个新资源。 要在给定 HttpClient
和 URI 的情况下发出 HTTP PUT
请求,请使用 HttpClient.PutAsync 方法:
static async Task PutAsync(HttpClient httpClient)
using StringContent jsonContent = new(
JsonSerializer.Serialize(new
userId = 1,
id = 1,
title = "foo bar",
completed = false
Encoding.UTF8,
"application/json");
using HttpResponseMessage response = await httpClient.PutAsync(
"todos/1",
jsonContent);
response.EnsureSuccessStatusCode()
.WriteRequestToConsole();
var jsonResponse = await response.Content.ReadAsStringAsync();
Console.WriteLine($"{jsonResponse}\n");
// Expected output:
// PUT https://jsonplaceholder.typicode.com/todos/1 HTTP/1.1
// {
// "userId": 1,
// "id": 1,
// "title": "foo bar",
// "completed": false
// }
前面的代码:
准备一个包含请求的 JSON 正文的 StringContent 实例(MIME 类型为 "application/json"
)。
向 "https://jsonplaceholder.typicode.com/todos/1"
发出 PUT
请求。
确保响应成功,并将请求详细信息和 JSON 响应正文写入控制台。
HTTP Put as JSON
要自动将 PUT
请求参数序列化并将响应反序列化为强类型 C# 对象,请使用 System.Net.Http.Json NuGet 包中的 PutAsJsonAsync 扩展方法。
static async Task PutAsJsonAsync(HttpClient httpClient)
using HttpResponseMessage response = await httpClient.PutAsJsonAsync(
"todos/5",
new Todo(Title: "partially update todo", Completed: true));
response.EnsureSuccessStatusCode()
.WriteRequestToConsole();
var todo = await response.Content.ReadFromJsonAsync<Todo>();
Console.WriteLine($"{todo}\n");
// Expected output:
// PUT https://jsonplaceholder.typicode.com/todos/5 HTTP/1.1
// Todo { UserId = , Id = 5, Title = partially update todo, Completed = True }
前面的代码:
将 Todo
实例序列化为 JSON,并向 "https://jsonplaceholder.typicode.com/todos/5"
发出 PUT
请求。
确保响应成功,并将请求详细信息写入控制台。
将响应正文反序列化为 Todo
实例,并将 Todo
写入控制台。
HTTP Patch
PATCH
请求是对现有资源执行部分更新。 它不会创建新资源,并且不用于替换现有资源。 而是只更新部分资源。 要在给定 HttpClient
和 URI 的情况下发出 HTTP PATCH
请求,请使用 HttpClient.PatchAsync 方法:
static async Task PatchAsync(HttpClient httpClient)
using StringContent jsonContent = new(
JsonSerializer.Serialize(new
completed = true
Encoding.UTF8,
"application/json");
using HttpResponseMessage response = await httpClient.PatchAsync(
"todos/1",
jsonContent);
response.EnsureSuccessStatusCode()
.WriteRequestToConsole();
var jsonResponse = await response.Content.ReadAsStringAsync();
Console.WriteLine($"{jsonResponse}\n");
// Expected output
// PATCH https://jsonplaceholder.typicode.com/todos/1 HTTP/1.1
// {
// "userId": 1,
// "id": 1,
// "title": "delectus aut autem",
// "completed": true
// }
前面的代码:
准备一个包含请求的 JSON 正文的 StringContent 实例(MIME 类型为 "application/json"
)。
向 "https://jsonplaceholder.typicode.com/todos/1"
发出 PATCH
请求。
确保响应成功,并将请求详细信息和 JSON 响应正文写入控制台。
System.Net.Http.Json
NuGet 包中没有针对 PATCH
请求的扩展方法。
HTTP Delete
DELETE
请求会删除现有资源。 请求是幂等但不安全的,这意味着对同一DELETE
资源的多个DELETE
请求会产生相同的结果,但该请求会影响资源的状态。 要在给定 HttpClient
和 URI 的情况下发出 HTTP DELETE
请求,请使用 HttpClient.DeleteAsync 方法:
static async Task DeleteAsync(HttpClient httpClient)
using HttpResponseMessage response = await httpClient.DeleteAsync("todos/1");
response.EnsureSuccessStatusCode()
.WriteRequestToConsole();
var jsonResponse = await response.Content.ReadAsStringAsync();
Console.WriteLine($"{jsonResponse}\n");
// Expected output
// DELETE https://jsonplaceholder.typicode.com/todos/1 HTTP/1.1
// {}
前面的代码:
向 "https://jsonplaceholder.typicode.com/todos/1"
发出 DELETE
请求。
确保响应成功,并将请求详细信息写入控制台。
对 DELETE
请求的响应(与 PUT
请求一样)可能包含正文,也可能不包含。
HTTP Head
HEAD
请求类似于请求 GET
。 它只返回与资源关联的标头,而不返回资源。 对 HEAD
请求的响应不会返回正文。 要在给定 HttpClient
和 URI 的情况下发出 HTTP HEAD
请求,请使用 HttpClient.SendAsync 方法并将 HttpMethod 设置为 HttpMethod.Head
:
static async Task HeadAsync(HttpClient httpClient)
using HttpRequestMessage request = new(
HttpMethod.Head,
"https://www.example.com");
using HttpResponseMessage response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode()
.WriteRequestToConsole();
foreach (var header in response.Headers)
Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
Console.WriteLine();
// Expected output:
// HEAD https://www.example.com/ HTTP/1.1
// Accept-Ranges: bytes
// Age: 550374
// Cache-Control: max-age=604800
// Date: Wed, 10 Aug 2022 17:24:55 GMT
// ETag: "3147526947"
// Server: ECS, (cha / 80E2)
// X-Cache: HIT
前面的代码:
向 "https://www.example.com/"
发出 HEAD
请求。
确保响应成功,并将请求详细信息写入控制台。
循环访问所有响应标头,将每个标头写入控制台。
HTTP Options
OPTIONS
请求用于标识服务器或终结点支持哪些 HTTP 方法。 要在给定 HttpClient
和 URI 的情况下发出 HTTP OPTIONS
请求,请使用 HttpClient.SendAsync 方法并将 HttpMethod 设置为 HttpMethod.Options
:
static async Task OptionsAsync(HttpClient httpClient)
using HttpRequestMessage request = new(
HttpMethod.Options,
"https://www.example.com");
using HttpResponseMessage response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode()
.WriteRequestToConsole();
foreach (var header in response.Content.Headers)
Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
Console.WriteLine();
// Expected output
// OPTIONS https://www.example.com/ HTTP/1.1
// Allow: OPTIONS, GET, HEAD, POST
// Content-Type: text/html; charset=utf-8
// Expires: Wed, 17 Aug 2022 17:28:42 GMT
// Content-Length: 0
前面的代码:
向 "https://www.example.com/"
发送 OPTIONS
HTTP 请求。
确保响应成功,并将请求详细信息写入控制台。
循环访问所有响应内容标头,将每个标头写入控制台。
HTTP Trace
TRACE
请求可用于调试,因为它提供请求消息的应用程序级环回。 要发出 HTTP TRACE
请求,请使用 HttpMethod.Trace
创建 HttpRequestMessage:
using HttpRequestMessage request = new(
HttpMethod.Trace,
"{ValidRequestUri}");
并非所有 HTTP 服务器都支持 TRACE
HTTP 方法。 如果使用不当,可能会暴露安全漏洞。 有关详细信息,请参阅 Open Web Application Security Project (OWASP):跨站点跟踪。
处理 HTTP 响应
每当处理 HTTP 响应时,都与 HttpResponseMessage 类型进行交互。 在评估响应的有效性时会使用多个成员。 HTTP 状态代码通过 HttpResponseMessage.StatusCode 属性提供。 假设你已在给定客户端实例的情况下发送了一个请求:
using HttpResponseMessage response = await httpClient.SendAsync(request);
要确保 response
为 OK
(HTTP 状态代码 200),可以对其进行评估,如以下示例所示:
if (response is { StatusCode: HttpStatusCode.OK })
// Omitted for brevity...
还有其他表示成功响应的 HTTP 状态代码,例如 CREATED
(HTTP 状态代码 201) 、 ACCEPTED
(HTTP 状态代码 202) 、 NO CONTENT
(HTTP 状态代码 204) , (RESET CONTENT
HTTP 状态代码 205) 。 也可使用 HttpResponseMessage.IsSuccessStatusCode 属性评估这些代码,这可确保响应状态代码在 200-299 范围内:
if (response.IsSuccessStatusCode)
// Omitted for brevity...
如果需要框架引发 HttpRequestException,可调用 HttpResponseMessage.EnsureSuccessStatusCode() 方法:
response.EnsureSuccessStatusCode();
如果响应状态代码不在 200-299 范围内,则此代码将引发 HttpRequestException
。
HTTP 有效内容响应
如果响应有效,可使用 Content 属性访问响应正文。 正文以 HttpContent 实例形式提供,可用于访问流、字节数组或字符串形式的正文:
await using Stream responseStream =
await response.Content.ReadAsStreamAsync();
在前面的代码中,responseStream
可用于读取响应正文。
byte[] responseByteArray = await response.Content.ReadAsByteArrayAsync();
在前面的代码中,responseByteArray
可用于读取响应正文。
string responseString = await response.Content.ReadAsStringAsync();
在前面的代码中,responseString
可用于读取响应正文。
最后,如果知道 HTTP 终结点返回 JSON,可使用 System.Net.Http.Json NuGet 包将响应正文反序列化为任何有效的 C# 对象:
T? result = await response.Content.ReadFromJsonAsync<T>();
在前面的代码中,result
是反序列化为 T
类型的响应正文。
HTTP 错误处理
当 HTTP 请求失败时,HttpRequestException 将引发。 仅捕获该异常可能是不够的,因为可能要考虑处理引发的其他潜在异常。 例如,调用代码可能使用了在请求完成之前取消的取消令牌。 在此场景中,你将捕获 TaskCanceledException:
using var cts = new CancellationTokenSource();
// Assuming:
// httpClient.Timeout = TimeSpan.FromSeconds(10)
using var response = await httpClient.GetAsync(
"http://localhost:5001/sleepFor?seconds=100", cts.Token);
catch (TaskCanceledException ex) when (cts.IsCancellationRequested)
// When the token has been canceled, it is not a timeout.
Console.WriteLine($"Canceled: {ex.Message}");
同样,在发出 HTTP 请求时,如果服务器没有在 HttpClient.Timeout 超过之前响应,则会引发相同的异常。 但是,在此场景中,可以通过在捕获 TaskCanceledException 时评估 Exception.InnerException 来辨别超时的发生:
// Assuming:
// httpClient.Timeout = TimeSpan.FromSeconds(10)
using var response = await httpClient.GetAsync(
"http://localhost:5001/sleepFor?seconds=100");
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException tex)
Console.WriteLine($"Timed out: {ex.Message}, {tex.Message}");
在前面的代码中,当内部异常是 TimeoutException 时,超时发生,且取消令牌未取消请求。
若要在捕获 HttpRequestException 时评估 HTTP 状态代码,可以评估 HttpRequestException.StatusCode 属性:
// Assuming:
// httpClient.Timeout = TimeSpan.FromSeconds(10)
using var response = await httpClient.GetAsync(
"http://localhost:5001/doesNotExist");
response.EnsureSuccessStatusCode();
catch (HttpRequestException ex) when (ex is { StatusCode: HttpStatusCode.NotFound })
// Handle 404
Console.WriteLine($"Not found: {ex.Message}");
在前面的代码中 EnsureSuccessStatusCode() ,调用 方法以在响应不成功时引发异常。 HttpRequestException.StatusCode 属性随后被评估以确定响应是否是 404
(HTTP 状态代码 404)。 HttpClient
上有几个帮助程序方法可以隐式地代表你调用 EnsureSuccessStatusCode
,请考虑以下 API:
HttpClient.GetByteArrayAsync
HttpClient.GetStreamAsync
HttpClient.GetStringAsync
用于发出 HTTP 请求且不返回 HttpResponseMessage
的所有 HttpClient
方法代表你隐式调用 EnsureSuccessStatusCode
。
调用这些方法时,可以处理 HttpRequestException
并评估 HttpRequestException.StatusCode 属性以确定响应的 HTTP 状态代码:
// These extension methods will throw HttpRequestException
// with StatusCode set when the HTTP request status code isn't 2xx:
// GetByteArrayAsync
// GetStreamAsync
// GetStringAsync
using var stream = await httpClient.GetStreamAsync(
"https://localhost:5001/doesNotExists");
catch (HttpRequestException ex) when (ex is { StatusCode: HttpStatusCode.NotFound })
// Handle 404
Console.WriteLine($"Not found: {ex.Message}");
在某些情况下,可能需要在代码中引发 HttpRequestException。 HttpRequestException() 构造函数是公共的,你可以使用它引发异常,自定义消息为:
using var response = await httpClient.GetAsync(
"https://localhost:5001/doesNotExists");
// Throw for anything higher than 400.
if (response is { StatusCode: >= HttpStatusCode.BadRequest })
throw new HttpRequestException(
"Something went wrong", inner: null, response.StatusCode);
catch (HttpRequestException ex) when (ex is { StatusCode: HttpStatusCode.NotFound })
Console.WriteLine($"Not found: {ex.Message}");
HTTP 代理
可以采用以下两种方法之一配置 HTTP 代理。 在 HttpClient.DefaultProxy 属性上指定默认值。 或者,可以在 HttpClientHandler.Proxy 属性上指定代理。
全局默认代理
HttpClient.DefaultProxy
是一个静态属性,如果在通过其构造函数传递的 HttpClientHandler 中未显式设置代理,则该属性将确定所有 HttpClient
实例使用的默认代理。
此属性返回的默认实例会按照一组不同的规则进行初始化,具体取决于你的平台:
对于 Windows: 从环境变量读取代理配置,如果未定义这些配置,则从用户的代理设置中读取这些配置。
对于 macOS: 从环境变量读取代理配置,如果未定义这些配置,则从系统的代理设置中读取代理配置。
对于 Linux: 从环境变量读取代理配置,如果未定义这些配置,此属性将初始化绕过所有地址的非配置实例。
基于 Windows 和 Unix 的平台上用于 DefaultProxy
初始化的环境变量是:
HTTP_PROXY
:用于 HTTP 请求的代理服务器。
HTTPS_PROXY
:用于 HTTPS 请求的代理服务器。
ALL_PROXY
:未定义和/或未 HTTP_PROXY
定义时用于 HTTP 和/或 HTTPS_PROXY
HTTPS 请求的代理服务器。
NO_PROXY
:应从代理中排除的主机名的逗号分隔列表。 通配符不支持星号;如果要匹配子域,请使用前导点。 示例: NO_PROXY=.example.com
具有前导点) (将匹配 www.example.com
,但不匹配 example.com
。 NO_PROXY=example.com
没有前导点) 的 (不匹配 www.example.com
。 将来可能会重新访问此行为,以更好地匹配其他生态系统。
在环境变量区分大小写的系统上,变量名称可能全部小写或全部大写。 首先检查小写名称。
代理服务器可以是主机名或 IP 地址,(可选)后跟冒号和端口号,也可以是 http
URL,(可选)其中包括用于代理身份验证的用户名和密码。 URL 必须以 http
开头,而不是 https
开头,并且不能在主机名、IP 或端口后包含任何文本。
每个客户端的代理
HttpClientHandler.Proxy 属性标识用于处理对 Internet 资源的请求的 WebProxy 对象。 若要指定不应使用代理,请将 Proxy
属性设置为 GlobalProxySelection.GetEmptyWebProxy() 方法返回的代理实例。
本地计算机或应用程序配置文件可以指定使用默认代理。 如果指定了 Proxy 属性,则 Proxy 属性中的代理设置将替代本地计算机或应用程序配置文件,并且处理程序使用指定的代理设置。 如果未在配置文件中指定代理,并且未指定 Proxy 属性,则处理程序将使用从本地计算机继承的代理设置。 如果没有代理设置,则请求将直接发送到服务器。
HttpClientHandler 类使用从本地计算机设置继承的通配符分析代理绕过列表。 例如, HttpClientHandler
类将浏览器中的 "nt*"
绕过列表分析为 的 "nt.*"
正则表达式。 因此,http://nt.com
的 URL 将使用 HttpClientHandler
类绕过代理。
HttpClientHandler
类支持本地代理绕过。 如果满足以下任一条件,该类会将目标视为本地目标:
目标包含平面名称(URL 中不含点)。
目标包含环回地址(Loopback 或 IPv6Loopback),或者目标包含分配给本地计算机的 IPAddress。
目标的域后缀匹配本地计算机的域后缀 (DomainName)。
有关配置代理的详细信息,请参阅:
WebProxy.Address
WebProxy.BypassProxyOnLocal
WebProxy.BypassArrayList
.NET 中的 HTTP 支持
HttpClient 的使用准则
使用 .NET 的 IHttpClientFactory
将 HTTP/3 与 HttpClient 结合使用
使用 HttpRepl 测试 Web API