Peticiones HTTP en C#: GET, POST y errores con HttpClient

Para hablar con una API desde C# no hace falta ninguna biblioteca: HttpClient viene con .NET y, con las extensiones de System.Net.Http.Json, pide, envía y convierte JSON en una sola línea. Lo que sí conviene saber es cuándo lanza excepciones y cuándo no.

El cliente: uno para todo el programa

Todo pasa por HttpClient, del espacio de nombres System.Net.Http, que está incluido en .NET: no hay que instalar nada. La regla más importante es que se crea una vez y se reutiliza. Cada HttpClient mantiene sus propias conexiones abiertas, y crear uno nuevo por cada petición agota los puertos del sistema en un programa que hace muchas. En un programa corto como estos, uno al principio basta; en una aplicación web, se pide con IHttpClientFactory.

GET y la respuesta convertida en objetos

GetFromJsonAsync hace la petición, comprueba el código de estado y convierte el JSON en objetos de tus tipos, todo en una línea. Los record del final describen la forma del JSON que devuelve https://httpbin.org/json, un servicio público de pruebas:

#:property PublishAot=false
using System.Net.Http.Json;

using var cliente = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };

var datos = await cliente.GetFromJsonAsync<Respuesta>("https://httpbin.org/json");
Console.WriteLine(datos!.Slideshow.Title);
Console.WriteLine($"de {datos.Slideshow.Author}, {datos.Slideshow.Slides.Count} diapositivas");
foreach (var d in datos.Slideshow.Slides)
{
    Console.WriteLine($"  - {d.Title}");
}

record Respuesta(Presentacion Slideshow);
record Presentacion(string Title, string Author, List<Diapositiva> Slides);
record Diapositiva(string Title, string Type);
salidaSample Slide Show
de Yours Truly, 2 diapositivas
  - Wake up to WonderWidgets!
  - Overview

La línea #:property PublishAot=false solo hace falta en un archivo suelto ejecutado con dotnet run: .NET 10 los compila con AOT, que desactiva la conversión de JSON por reflexión. Lo explica la página de JSON en C#. El JSON trae los nombres en minúscula ("title") y los records en mayúscula (Title): GetFromJsonAsync usa las opciones «web» de System.Text.Json, que no distinguen mayúsculas. Las propiedades del JSON que no están en el record, como "date", se ignoran. Las instrucciones van con await porque una petición tarda; el programa queda libre mientras espera (lo explica la lección de LINQ y async).

POST con un cuerpo JSON

#:property PublishAot=false
using System.Net.Http.Json;
using System.Text.Json;

using var cliente = new HttpClient();

var nueva = new { Nombre = "Ana", Edad = 31 };
using var respuesta = await cliente.PostAsJsonAsync("https://httpbin.org/post", nueva);

Console.WriteLine($"{(int)respuesta.StatusCode} {respuesta.StatusCode}");
Console.WriteLine(respuesta.Content.Headers.ContentType);

var eco = await respuesta.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(JsonSerializer.Serialize(eco.GetProperty("json")));
salida200 OK
application/json
{"edad":31,"nombre":"Ana"}

PostAsJsonAsync convierte el objeto en JSON, con los nombres en camelCase (nombre, no Nombre), y pone la cabecera Content-Type. httpbin devuelve lo que recibió en la propiedad json, y por eso aparece el mismo objeto. Cuando no quieres declarar un tipo para la respuesta, JsonElement permite recorrerla a mano.

Un 404 no es una excepción

using var cliente = new HttpClient();

using var respuesta = await cliente.GetAsync("https://httpbin.org/status/404");
Console.WriteLine($"Llegó la respuesta: {(int)respuesta.StatusCode}, ¿éxito? {respuesta.IsSuccessStatusCode}");

try
{
    respuesta.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Error {(int?)ex.StatusCode}: {ex.Message}");
}
salidaLlegó la respuesta: 404, ¿éxito? False
Error 404: Response status code does not indicate success: 404 (NOT FOUND).

Con GetAsync o SendAsync, una respuesta 404 o 500 llega con normalidad: el servidor contestó, y lo que dice está en StatusCode. Tienes que comprobarlo tú con IsSuccessStatusCode o convertirlo en excepción con EnsureSuccessStatusCode(). Los métodos cómodos (GetStringAsync, GetFromJsonAsync) sí lanzan HttpRequestException si el código no es 2xx. Los mensajes de las excepciones de .NET salen en inglés.

El tiempo de espera

using var cliente = new HttpClient { Timeout = TimeSpan.FromSeconds(1) };

try
{
    // /delay/3 tarda tres segundos en contestar
    string texto = await cliente.GetStringAsync("https://httpbin.org/delay/3");
}
catch (TaskCanceledException ex)
{
    Console.WriteLine(ex.Message);
}
salidaThe request was canceled due to the configured HttpClient.Timeout of 1 seconds elapsing.
Cien segundos por omisión. Si no pones Timeout, HttpClient espera 100 segundos a un servidor que no contesta. Y cuando se agota, no lanza TimeoutException sino TaskCanceledException, la misma que si cancelas tú la petición: atrapa esa. Para tiempos distintos en cada petición, usa un CancellationTokenSource con CancelAfter y pásalo al método.

Resumen

ParaUsa
Pedir JSON y convertirloawait cliente.GetFromJsonAsync<T>(url)
Enviar JSONawait cliente.PostAsJsonAsync(url, objeto)
Mirar el código de estadoGetAsync y respuesta.StatusCode
Fallar si no es 2xxrespuesta.EnsureSuccessStatusCode()
Limitar la esperaTimeout o un CancellationToken

Todo el código de esta página se compiló con el .NET SDK 10.0.403 (C# 14) y se ejecutó con .NET 10.0.14 en un contenedor limpio antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.