Runtime: HttpClient lanza TaskCanceledException en el tiempo de espera

Creado en 25 may. 2017  ·  119Comentarios  ·  Fuente: dotnet/runtime

HttpClient genera una excepción TaskCanceledException en el tiempo de espera en algunas circunstancias. Esto nos sucede cuando el servidor está bajo una gran carga. Pudimos solucionarlo aumentando el tiempo de espera predeterminado. El siguiente hilo del foro de MSDN captura la esencia del problema experimentado: https://social.msdn.microsoft.com/Forums/en-US/d8d87789-0ac9-4294-84a0-91c9fa27e353/bug-in-httpclientgetasync-should-throw -webexception-not-taskcanceledexception?forum=netfxnetcom&prof=requerido

Gracias

area-System.Net.Http enhancement

Comentario más útil

@karelz agregando una nota como otro cliente afectado por esta excepción engañosa :)

Todos 119 comentarios

@awithy , ¿qué versión de .NET Core está usando?

1.1

Ya sea el diseño correcto o no, por diseño OperationCanceledExceptions se lanzan por tiempos de espera (y TaskCanceledException es una OperationCanceledException).
CC: @davidsh

Nuestro equipo encontró esto poco intuitivo, pero parece estar funcionando según lo diseñado. Desafortunadamente, cuando llegamos a esto, perdimos un poco de tiempo tratando de encontrar una fuente de cancelación. Tener que manejar este escenario de manera diferente a la cancelación de tareas es bastante feo (creamos un controlador personalizado para manejar esto). Esto también parece ser un uso excesivo de la cancelación como concepto.

Gracias de nuevo por tu rápida respuesta.

Parece que por diseño + solo hubo 2 quejas de clientes en los últimos 6 meses.
Clausura.

@karelz agregando una nota como otro cliente afectado por esta excepción engañosa :)

Gracias por la información, vote a favor también en la publicación principal (los problemas se pueden ordenar por ella), ¡gracias!

Ya sea el diseño correcto o no, por diseño OperationCanceledExceptions se lanzan por tiempos de espera

Este es un mal diseño de la OMI. No hay forma de saber si la solicitud se canceló realmente (es decir, se canceló el token de cancelación que se pasó a SendAsync ) o si transcurrió el tiempo de espera. En el último caso, tendría sentido volver a intentarlo, mientras que en el primer caso no lo haría. Hay un TimeoutException que sería perfecto para este caso, ¿por qué no usarlo?

Sí, esto también hizo que nuestro equipo se volviera loco. Hay un hilo completo de Stack Overflow dedicado a tratar de solucionarlo: https://stackoverflow.com/questions/10547895/how-can-i-tell-when-httpclient-has-timed-out/32230327#32230327

Genial gracias.

Reapertura ya que el interés ahora es mayor: el hilo StackOverflow ahora tiene 69 votos a favor.

cc @dotnet/ncl

Hay noticias ? Todavía sucede en dotnet core 2.0

Está en nuestra lista de problemas principales para post-2.1. Sin embargo, no se solucionará en 2.0/2.1.

Tenemos varias opciones de qué hacer cuando se agota el tiempo de espera:

  1. Lanza TimeoutException en lugar de TaskCanceledException .

    • Pro: fácil de distinguir el tiempo de espera de la acción de cancelación explícita en tiempo de ejecución

    • Desventaja: cambio técnico importante: se lanza un nuevo tipo de excepción.

  2. Lanza TaskCanceledException con excepción interna como TimeoutException

    • Pro: Es posible distinguir el tiempo de espera de la acción de cancelación explícita en tiempo de ejecución. Tipo de excepción compatible.

    • Pregunta abierta: ¿Está bien tirar la pila original TaskCanceledException y tirar una nueva, mientras se conservan InnerException (como TimeoutException.InnerException )? ¿O deberíamos conservar y simplemente envolver el TaskCanceledException original?



      • O bien: nueva TaskCanceledException -> nueva TimeoutException -> original TaskCanceledException (con InnerException original que puede ser nulo)


      • O: nueva TaskCanceledException -> nueva TimeoutException -> original TaskCanceledException.InnerException (puede ser nulo)



  3. Lanza TaskCanceledException con un mensaje que menciona el tiempo de espera como el motivo

    • Pro: Es posible distinguir el tiempo de espera de la acción de cancelación explícita del mensaje/registros

    • Con: No se puede distinguir en tiempo de ejecución, es "solo depuración".

    • Pregunta abierta: Igual que en [2]: ¿deberíamos envolver o reemplazar la TaskCanceledException original (y apilar)?

Me inclino por la opción [2], descartando la pila original de TaskCanceledException originales.
@stephentoub @davidsh ¿Alguna idea?

Por cierto: el cambio debería ser bastante sencillo en HttpClient.SendAsync donde configuramos el tiempo de espera:
https://github.com/dotnet/corefx/blob/30fb78875148665b98748ede3013641734d9bf5c/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L433 -L461

La excepción de nivel superior siempre debe ser una HttpRequestException. Ese es el modelo API estándar para HttpClient.

Dentro de eso, como excepción interna, los "tiempos de espera" probablemente deberían ser una TimeoutException. De hecho, si tuviéramos que combinar esto con los otros problemas (sobre la modificación de HttpRequestion para incluir una nueva propiedad de enumeración similar a WebExceptionStatus), los desarrolladores solo tendrían que verificar esa enumeración y no tendrían que inspeccionar las excepciones internas en absoluto.

Esta es básicamente la opción 1), pero continúa conservando el modelo de API compatible con la aplicación actual de que las excepciones de nivel superior de las API de HttpClient siempre son HttpRequestException.

Nota: cualquier "tiempo de espera" de la lectura directa de un flujo de respuesta HTTP de las API de flujo como:

```c#
var cliente = nuevo HttpClient();
Stream responseStream = await client.GetStreamAsync(url);

int bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length);
```

debería continuar arrojando System.IO.IOException, etc. como excepciones de nivel superior (de hecho, tenemos algunos errores en SocketsHttpHandler sobre esto).

@davidsh , ¿está sugiriendo lanzar HttpRequestException incluso en los casos en que hay una cancelación explícita del usuario? Esos arrojan TaskCanceledException hoy y parece estar bien. No estoy seguro si es algo que queremos cambiar...
Estaría bien exponer los tiempos de espera del lado del cliente como HttpRequestException en lugar de TaskCanceledException .

@davidsh , ¿está sugiriendo lanzar HttpRequestException incluso en los casos en que hay una cancelación explícita del usuario? Esos lanzan TaskCanceledException hoy y parece estar bien. No estoy seguro si es algo que queremos cambiar...

Necesito probar los diferentes comportamientos de las pilas, incluido .NET Framework, para verificar los comportamientos actuales exactos antes de poder responder esta pregunta.

Además, en algunos casos, algunas de nuestras pilas lanzan HttpRequestException con la excepción interna de OperationCanceledException. TaskCanceledException es un tipo derivado de OperationCanceledException.

cuando hay cancelación explícita del usuario

También necesitamos una definición clara de lo que es "explícito". Por ejemplo, ¿la configuración de la propiedad HttpClient.Timeout es "explícita"? Creo que estamos diciendo que no. ¿Qué hay de llamar a .Cancel() en el objeto CancellationTokenSource (que afecta el objeto CancellationToken pasado)? ¿Es eso "explícito"? Probablemente hay otros ejemplos en los que podemos pensar.

.NET Framework también lanza TaskCanceledException cuando configura HttpClient.Timeout .

Si no puede distinguir entre la cancelación del cliente y la cancelación de la implementación, simplemente marque ambas. Antes de usar el token del cliente, cree uno derivado:
```c#
var token de cliente = ....
var innerTokenSource = CancellationTokenSource.CreateLinkedTokenSource(clientToken);

If at some point you catch **OperationCancelledException**:
```c#
try
{
     //invocation with innerToken
}
catch(OperationCancelledException oce)
{
      if(clientToken.IsCancellationRequested)
          throw; //as is, it is client initiated and in higher priority
      if(innerToken.IsCancellationRequested)
           //wrap it in HttpException, TimeoutException, whatever, wrap it in another linked token until you process all cases.
}

Básicamente, filtra todas las capas posibles desde el token del cliente hasta el más profundo, hasta que encuentre el token que causó la cancelación.

¿Alguna actualización sobre esta creación de 2.2 o 3.0? @karelz @davidsh , cc @NickCraver . Desearía que solo hiciera la opción 1) de lanzar una TimeoutException o, alternativamente, una nueva HttpTimeoutException que se deriva de HttpRequestException. ¡Gracias!

¿Alguna actualización sobre esta creación de 2.2 o 3.0?

Demasiado tarde para 2.2, fue lanzado la semana pasada ;)

Demasiado tarde para 2.2, fue lanzado la semana pasada ;)

Argh, eso es lo que pasa cuando me tomo un descanso ;)

Aunque un poco tonto, la solución más simple que he encontrado es anidar un controlador de captura para OperationCanceledException y luego evaluar el valor de IsCancellationRequested para determinar si el token de cancelación fue o no el resultado de un tiempo de espera o un aborto de sesión real. Claramente, la lógica que realiza la llamada a la API probablemente estaría en una lógica comercial o en una capa de servicio, pero consolidé el ejemplo para simplificar:

````c sostenido
[HttpObtener]
Tarea asíncrona públicaÍndice (token de cancelación token de cancelación)
{
tratar
{
//Esto normalmente se haría en una lógica de negocios o capa de servicio en lugar de en el propio controlador, pero estoy ilustrando el punto aquí por simplicidad
tratar
{
//Uso de HttpClientFactory para instancias de HttpClient
var httpClient = _httpClientFactory.CreateClient();

        //Set an absurd timeout to illustrate the point
        httpClient.Timeout = new TimeSpan(0, 0, 0, 0, 1);

        //Perform call that requires special timeout logic                
        var httpResponse = await httpClient.GetAsync("https://someurl.com/api/long/running");

        //... (if GetAsync doesn't fail, handle the response as desired)
    }
    catch (OperationCanceledException innerOperationCanceled)
    {
        //If a canceled token exception occurs due to a timeout, "IsCancellationRequested" should be false
        if (cancellationToken.IsCancellationRequested)
        {
            //Bubble exception to global handler
            throw innerOperationCanceled;
        }
        else
        {
            //... (perform timeout logic here)
        }                    
    }                

}
catch (OperationCanceledException operationCanceledEx)
{
    _logger.LogWarning(operationCanceledEx, "Request was aborted by the end user.");
    return new StatusCodeResult(499);
}
catch (Exception ex)
{
    _logger.LogError(ex, "An unexepcted error occured.");
    return new StatusCodeResult(500);
}

}
````

He visto otros artículos sobre cómo manejar este escenario de manera más elegante, pero todos requieren profundizar en la canalización, etc. Me doy cuenta de que este enfoque no es perfecto y espero que haya un mejor soporte para las excepciones de tiempo de espera reales con un futuro. liberar.

Trabajar con el cliente http es absolutamente abismal. HttpClient debe eliminarse y comenzar de nuevo desde cero.

Aunque un poco tonto, la solución más simple que he encontrado es anidar un controlador de captura para OperationCanceledException y luego evaluar el valor de IsCancellationRequested para determinar si el token de cancelación fue o no el resultado de un tiempo de espera o un aborto de sesión real. Claramente, la lógica que realiza la llamada a la API probablemente estaría en una lógica comercial o en una capa de servicio, pero consolidé el ejemplo para simplificar:

[HttpGet]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
  try
  {
      //This would normally be done in a business logic or service layer rather than in the controller itself but I'm illustrating the point here for simplicity sake
      try
      {
          //Using HttpClientFactory for HttpClient instances      
          var httpClient = _httpClientFactory.CreateClient();

          //Set an absurd timeout to illustrate the point
          httpClient.Timeout = new TimeSpan(0, 0, 0, 0, 1);

          //Perform call that requires special timeout logic                
          var httpResponse = await httpClient.GetAsync("https://someurl.com/api/long/running");

          //... (if GetAsync doesn't fail, handle the response as desired)
      }
      catch (OperationCanceledException innerOperationCanceled)
      {
          //If a canceled token exception occurs due to a timeout, "IsCancellationRequested" should be false
          if (cancellationToken.IsCancellationRequested)
          {
              //Bubble exception to global handler
              throw innerOperationCanceled;
          }
          else
          {
              //... (perform timeout logic here)
          }                    
      }                

  }
  catch (OperationCanceledException operationCanceledEx)
  {
      _logger.LogWarning(operationCanceledEx, "Request was aborted by the end user.");
      return new StatusCodeResult(499);
  }
  catch (Exception ex)
  {
      _logger.LogError(ex, "An unexepcted error occured.");
      return new StatusCodeResult(500);
  }
}

He visto otros artículos sobre cómo manejar este escenario de manera más elegante, pero todos requieren profundizar en la canalización, etc. Me doy cuenta de que este enfoque no es perfecto y espero que haya un mejor soporte para las excepciones de tiempo de espera reales con un futuro. liberar.

Es absolutamente loco que necesitemos escribir este código atroz. HttpClient es la abstracción con más fugas jamás creada por Microsoft

Estoy de acuerdo en que parece bastante miope no incluir información de excepción más específica sobre los tiempos de espera. Parece algo bastante básico que las personas quieran manejar los tiempos de espera reales de manera diferente a las excepciones de token canceladas.

Trabajar con el cliente http es absolutamente abismal. HttpClient debe eliminarse y comenzar de nuevo desde cero.\

Eso no es un comentario muy útil o constructivo @dotnetchris . Tanto los clientes como los empleados de MSFT están aquí para intentar mejorar las cosas, y es por eso que gente como @karelz todavía está aquí escuchando los comentarios de los clientes y tratando de mejorar. No hay ningún requisito para que desarrollen abiertamente, y no quememos su buena voluntad con negatividad o podrían decidir tomar su pelota e irse a casa, y eso sería peor para la comunidad. ¡Gracias! :)

¿Por qué no simplemente crear HttpClient2 (nombre temporal) que tenga el comportamiento esperado e informar, en la documentación, que "HttpClient está obsoleto, use HttpClient2" y explique la diferencia entre los 2?

De esta manera, no se insertan cambios importantes en .NetFramework.

HttpClient no debe basarse en el lanzamiento de excepciones. No es excepcional que un host de Internet no esté disponible. En realidad se espera, dadas las infinitas permutaciones de direcciones, el caso excepcional es darle algo que funcione.

Incluso escribí en un blog sobre lo malo que es usar HttpClient hace más de 3 años. https://dotnetchris.wordpress.com/2015/12/11/trabajar-con-httpclient-is-absurd/

Este es un diseño flagrantemente malo.

¿Alguien en Microsoft ha usado HttpClient? Seguro que no lo parece.

Luego, más allá de esta horrible superficie API, hay todo tipo de abstracciones con fugas con respecto a su construcción y subprocesamiento. No puedo creer que sea posible salvarlo y que solo uno nuevo sea una solución viable. HttpClient2, HttpClientSlim está bien o lo que sea.

El acceso HTTP nunca debe generar excepciones. Siempre debe devolver un resultado, el resultado debe poder ser inspeccionado para su finalización, tiempo de espera, respuesta http. La única razón por la que debería permitirse lanzar es si invoca EnsureSuccess() , entonces está bien lanzar cualquier excepción en el mundo. Pero no debe haber manejo de flujo de control para un comportamiento completamente normal de Internet: host activo, host no activo, tiempo de espera del host. Ninguno de ellos es excepcional.

HttpClient no debe basarse en el lanzamiento de excepciones. No es excepcional que un host de Internet no esté disponible. En realidad se espera, dadas las infinitas permutaciones de direcciones, el caso excepcional es darle algo que funcione.

@dotnetchris , dado que parece que tiene mucha pasión por este tema, ¿por qué no se toma el tiempo de redactar la API completa propuesta para HttpClient2 y enviarla al equipo para que la analicen como un problema nuevo?

No me he molestado en sumergirme en los aspectos prácticos de lo que hace que HttpClient sea bueno o malo desde un nivel técnico más profundo, así que no tengo nada que agregar a los comentarios de @dotnetchris. Definitivamente no creo que sea apropiado criticar al equipo de desarrollo de MS, pero también entiendo lo frustrante que es pasar horas y horas buscando soluciones a situaciones que aparentemente deberían ser súper simples de manejar. Es la frustración de luchar por comprender por qué se tomaron ciertas decisiones/cambios, con la esperanza de que alguien en MS aborde la inquietud mientras intenta terminar su trabajo y entregarlo mientras tanto.

En mi caso, tengo un servicio externo al que llamo durante el proceso de OAuth para establecer roles de usuario y si esa llamada se agota dentro de un período de tiempo específico (lo que desafortunadamente sucede más de lo que me gustaría), necesito volver a un método secundario. .

Realmente hay una miríada de otras razones por las que puedo ver por qué alguien querría poder distinguir entre un tiempo de espera y una cancelación. Por qué no parece existir un mejor soporte para un escenario común como este es lo que me cuesta entender y realmente espero que sea algo en lo que el equipo de MS reflexione un poco más.

@karelz , ¿qué se necesitaría para programar esto para 3.0, una propuesta de API de alguien?

No se trata de una propuesta de API. Se trata de encontrar la implementación correcta. Está en nuestro backlog 3.0 y bastante alto en la lista (detrás de HTTP/2 y escenarios empresariales). Es una muy buena oportunidad para ser abordado en 3.0 por nuestro equipo.
Por supuesto, si alguien en la comunidad está motivado, también aceptaremos una contribución.

@karelz , ¿cómo puede ayudar la comunidad en este caso? La razón por la que mencioné si se necesitaba una propuesta de API es que no estaba seguro de su mensaje que comienza "Tenemos varias opciones sobre qué hacer cuando se agota el tiempo de espera:" si el equipo había decidido su alternativa preferida de las que usted enumeró. Si ha decidido una dirección, entonces podemos ir desde allí. ¡Gracias!

Técnicamente hablando, no es una pregunta de propuesta de API. La decisión no requerirá la aprobación de la API. Sin embargo, estoy de acuerdo en que debemos estar alineados sobre cómo exponer la diferencia; es probable que haya alguna discusión.
No espero que las opciones difieran demasiado entre sí en la implementación: si la comunidad quiere ayudar, obtener la implementación de cualquiera de las opciones nos llevará al 98% (suponiendo que la excepción se lanza desde un lugar y puede ser cambiarse fácilmente más tarde para adaptarse a las opciones [1]-[3] enumeradas anteriormente).

¡Hola! Nos encontramos con este problema recientemente también. ¿Solo por curiosidad si todavía está en su radar para 3.0?

Sí, todavía está en nuestra lista 3.0; todavía le daría un 90% de posibilidades de que se aborde en 3.0.

Me encontré con esto hoy. Establecí un tiempo de espera en HttpClient y me da esto

{System.Threading.Tasks.TaskCanceledException: A task was canceled.}
CancellationRequested = true

Estoy de acuerdo con otros, esto es confuso ya que no menciona que el tiempo de espera sea el problema.

Umm, está bien, estoy un poco confundido. Todo el mundo en este hilo parece estar diciendo que los tiempos de espera de HTTP arrojan un TaskCanceledException pero eso no es lo que obtengo... Obtengo un OperationCanceledException en .NET Core 2.1...

Si desea lanzar una OperationCanceledException para los tiempos de espera, está bien, puedo solucionar eso, pero pasé horas tratando de rastrear qué diablos estaba pasando porque no está documentado correctamente:

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync

Establece claramente que se lanza un HttpRequestException para los tiempos de espera...

TaskCanceledException se deriva de OperationCanceledException.

@stephentoub Sí, pero no obtengo un TaskCanceledException , obtengo un OperationCanceledException .

Como en un bloque catch que atrapa TaskCanceledException no atrapa la excepción, necesita específicamente un bloque catch para un OperationCanceledException .

Ahora estoy probando específicamente los tiempos de espera de HTTP filtrando el tipo de excepción, y la forma en que sé que es un tiempo de espera es si NO es un TaskCanceledException :

```c#
tratar {
usando (respuesta var = esperar s_httpClient.SendAsync(solicitud, _updateCancellationSource.Token).ConfigureAwait(false))
devuelve (int)respuesta.StatusCode < 400;
}
catch (OperationCanceledException ex) cuando (ex.GetType() != typeof(TaskCanceledException)) {
// tiempo de espera de HTTP
falso retorno;
}
captura (HttpRequestException) {
falso retorno;
}

or alternatively this works as well, which might be better:

```c#
            try {
                using (var response = await s_httpClient.SendAsync(request, _updateCancellationSource.Token).ConfigureAwait(false))
                    return (int)response.StatusCode < 400;
            }
            catch (OperationCanceledException ex) when (ex.GetType() == typeof(OperationCanceledException)) {
                // HTTP timeout
                return false;
            }
            catch (HttpRequestException) {
                return false;
            }

Umm, está bien, estoy un poco confundido. Todo el mundo en este hilo parece estar diciendo que los tiempos de espera de HTTP arrojan un TaskCanceledException pero eso no es lo que obtengo... Obtengo un OperationCanceledException en .NET Core 2.1...

Si desea lanzar una OperationCanceledException para los tiempos de espera, está bien, puedo solucionar eso, pero pasé horas tratando de rastrear qué diablos estaba pasando porque no está documentado correctamente:

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync

Establece claramente que se lanza un HttpRequestException para los tiempos de espera...

¿Leíste todo el hilo porque en realidad hay varias respuestas donde se hace referencia a OperationCanceledExecption, incluido el ejemplo de código que planteé que muestra una forma (no tan limpia como me gustaría pero es una forma) de manejar los tiempos de espera explícitamente?

@Hobray Sí, lo tengo. ¿Qué te hace pensar que no lo hice? Eso todavía no explica cuántas personas en este hilo supuestamente obtienen TaskCanceledException en tiempos de espera, pero yo no.

También vi su ejemplo de código, pero desafortunadamente tiene una condición de carrera que no creo que haya considerado. El código publicado que estoy usando evita este problema y no requiere acceso al token de cancelación, lo que puede ser importante si está tratando de aprovechar el tiempo de espera más arriba.

@mikernet Tal vez la redacción me desconcertó. Tienes razón en que no había considerado una posible condición de carrera. Me doy cuenta de que no es una solución increíble, pero si desea explicar lo que pasé por alto, me gustaría escucharlo.

Una cosa que he notado es que la excepción exacta que obtiene parece variar dependiendo de si el tiempo de espera o la cancelación ocurren o no en una invocación de Controller vs middleware. Dentro de una invocación de middleware parece ser solo TaskCanceledException . Desafortunadamente, no tengo tiempo para buscar en las bibliotecas de código de .NET Core para entender por qué, pero es algo que he notado.

@Hobray Jaja, sí, por supuesto, lo siento, no quise ser críptico sobre la condición de la carrera, solo estaba en mi teléfono mientras estaba en movimiento escribiendo el último mensaje que dificultó la explicación.

La condición de carrera en su solución ocurre si se inicia una cancelación de tarea entre el tiempo de espera HttpClient y el punto en el que verifica el estado de cancelación del token de cancelación. En mi situación, necesito una excepción de cancelación de tareas para seguir burbujeando para que el código ascendente pueda detectar la cancelación del usuario, por lo tanto, necesito poder detectar la excepción solo si en realidad se agotó el tiempo de espera; es por eso que estoy usando when para filtrar la captura. El uso de su solución de verificar el token puede dar como resultado que OperationCanceledException no controlados pasen el filtro y se borboteen y bloqueen el programa, porque alguien que cancela la operación aguas arriba esperará que se arroje un TaskCanceledException , no un OperationCanceledException .

Si se supone que el código que llama al método HttpClient maneja tanto el tiempo de espera como la cancelación de la tarea, entonces la distinción entre el tiempo de espera y la cancelación del usuario cuando ocurre la condición de carrera puede no ser importante para usted, dependiendo de la situación... pero si está escribiendo un código de nivel de biblioteca y necesita dejar que la cancelación de la tarea aumente para que se maneje a un nivel superior, definitivamente hace la diferencia.

Si lo que está diciendo es cierto y los tiempos de espera arrojan diferentes tipos de excepciones según el contexto de la llamada, definitivamente me parece un problema que debe abordarse. No hay forma de que sea o deba ser el comportamiento previsto.

Interesante sobre la posible condición de carrera. No había considerado eso. Afortunadamente, para mi caso de uso no sería un gran problema.

En cuanto a las variaciones de excepción, necesito encontrar algo de tiempo para simplificar el middleware en el que he estado trabajando para ver si es consistente con lo que he visto varias veces durante la depuración. Las estúpidas llamadas de precarga del omnibox de Chrome han estado donde las he visto en mi middleware cuando el momento ha sido el correcto. Creo que puedo crear un ejemplo simple para probarlo definitivamente de una forma u otra.

Todavía no tengo claro cuál debería ser la solución correcta.

Mi solución es la más correcta en situaciones en las que los tiempos de espera de http arrojan OperationCanceledException. No puedo reproducir la situación en la que se lanza TaskCanceledException y nadie ha proporcionado una forma de reproducir ese comportamiento, así que digo que pruebe su entorno y si coincide con el mío, entonces esa es la solución correcta.

Si en algunos contextos de ejecución se lanza una TaskCanceledException, mi solución falla como una solución de propósito general para esos contextos y la otra solución propuesta es la "mejor" solución pero contiene una condición de carrera que puede o no ser importante para su situación, eso es para usted para evaluar.

Sin embargo, me encantaría escuchar más detalles de alguien que afirma que está recibiendo TaskCanceledExceptions porque eso claramente sería un gran error. Soy algo escéptico de que esto realmente esté sucediendo.

@mikernet si se arroja TaskCanceledException o OperationCanceledException es en gran medida irrelevante. Ese es un detalle de implementación en el que no debería confiar. Lo que puede hacer es capturar OperationException (que también capturará TaskCanceledException ) y verificar si el token de cancelación que pasó está cancelado, como se describe en el comentario de Hobray (https://github.com/ dotnet/corefx/issues/20296#issuecomment-449510983).

@thomaslevesque Ya abordé por qué ese enfoque es un problema. Sin abordar eso, su comentario no es muy útil: simplemente está repitiendo declaraciones antes de unirme a la conversión sin abordar el problema que describí.

En cuanto a "ese es un detalle de implementación", sí lo es, pero desafortunadamente tengo que confiar en él porque, de lo contrario, no hay una buena manera de determinar si la tarea se canceló realmente o se agotó el tiempo de espera. Debería haber una forma de determinar al 100% a partir de la excepción cuál de esos dos escenarios ocurrió para evitar la condición de carrera descrita. OperationCanceledException es simplemente una mala elección debido a esto... ya hay un TimeoutException en el marco que lo convierte en un mejor candidato.

Las excepciones para tareas asincrónicas no deben depender de la verificación de datos en otro objeto que también se puede modificar de forma asincrónica para determinar la causa de la excepción si un caso de uso común es tomar una decisión de bifurcación basada en la causa de la excepción. Eso es rogar por problemas de condición de carrera.

Además, en términos de que el tipo de excepción es un detalle de implementación... Realmente no creo que eso sea cierto para las excepciones, al menos no en términos de prácticas de documentación .NET bien establecidas. Mencione otro ejemplo en .NET Framework donde la documentación dice "lanza una excepción X cuando sucede Y" y el marco en realidad lanza una subclase de acceso público de esa excepción, y mucho menos una subclase de acceso público que se usa para un caso de excepción completamente diferente en el mismo método ! Eso parece una mala forma y nunca antes me había encontrado con esto en mis 15 años de programación .NET.

Independientemente, TaskCanceledException debe reservarse para la cancelación de tareas iniciadas por el usuario, no para tiempos de espera; como dije, ya tenemos una excepción para eso. Las bibliotecas deberían capturar cualquier cancelación de tareas internas y lanzar excepciones más apropiadas. Los controladores de excepciones para cancelaciones de tareas que se iniciaron con tokens de cancelación de tareas no deben ser responsables de tratar otros problemas no relacionados que puedan ocurrir durante la operación asincrónica.

@mikernet lo siento, me perdí esa parte de la discusión. Sí, hay una condición de carrera en la que no había pensado. Pero puede que no sea un problema tan grande como parece... La condición de carrera ocurre si el token de cancelación se señala después de que se agote el tiempo de espera, pero antes de verificar el estado del token, ¿verdad? En este caso, realmente no importa si se agotó el tiempo de espera si la operación se cancela de todos modos. Así que dejarás que aparezca OperationCanceledException , dejando que la persona que llama crea que la operación se canceló con éxito, lo cual es una mentira, ya que en realidad se agotó el tiempo de espera, pero no le importa a la persona que llama, ya que quería cancelar la operación de todos modos.

Debería haber una forma de determinar al 100% a partir de la excepción cuál de esos dos escenarios ocurrió para evitar la condición de carrera descrita. OperationCanceledException es simplemente una mala elección debido a esto... ya hay un TimeoutException en el marco que lo convierte en un mejor candidato.

Estoy completamente de acuerdo con eso, el comportamiento actual es claramente incorrecto. En mi opinión, la mejor opción para solucionarlo es la opción 1 del comentario de @karelz . Sí, es un cambio importante, pero sospecho que la mayoría de las bases de código no están manejando este escenario correctamente de todos modos, por lo que no empeorará las cosas.

@thomaslevesque Bueno, sí, pero en cierto modo abordé eso. Puede o no ser importante para algunas aplicaciones. En el mio si lo es. El usuario de mi método de biblioteca no espera OperationCanceledException cuando cancela una tarea; espera TaskCanceledException y debería poder detectarlo y estar "seguro". Si verifico el token de cancelación, veo que se canceló y paso la excepción, entonces acabo de pasar una excepción que no será detectada por su controlador y bloqueará el programa. Se supone que mi método no genera tiempos de espera.

Claro, hay formas de evitar esto... Puedo lanzar un nuevo TaskCanceledException envuelto, pero luego pierdo el seguimiento de la pila de nivel superior, y los tiempos de espera de HTTP se tratan de manera muy diferente a las cancelaciones de tareas en mi servicio: los tiempos de espera de HTTP van en un caché durante mucho tiempo, mientras que las cancelaciones de tareas no lo harán. No quiero perderme el almacenamiento en caché de los tiempos de espera reales debido a la condición de carrera.

Supongo que podría capturar OperationCanceledException y verificar el token de cancelación; si se ha cancelado, también verifique el tipo de excepción para asegurarme de que sea un TaskCanceledException antes de volver a lanzar para evitar que todo el programa se bloquee. Si alguna de esas condiciones es falsa, entonces debe ser un tiempo de espera. El único problema que queda en ese caso es la condición de carrera que es sutil, pero cuando tiene miles de solicitudes HTTP por minuto en un servicio muy cargado con tiempos de espera ajustados, sucede.

De cualquier manera, eso es complicado y, como ha indicado, debería resolverse de alguna manera.

(La condición de carrera solo ocurrirá en entornos donde se lanza TaskCanceledException en lugar de OperationCanceledException , dondequiera que sea, pero al menos la solución está a salvo de generar un tipo de excepción inesperado y no confiar en los detalles de la implementación para seguir funcionando razonablemente bien si los detalles de la implementación cambian).

La excepción de tiempo de espera lanzada por HttpClient en caso de tiempo de espera de http no debería ser del tipo System.Net.HttpRequestException (¿o un descendiente?) en lugar del genérico System.TimeoutException ? Por ejemplo, el viejo WebClient lanza WebException con una propiedad Status que se puede establecer en WebExceptionStatus.Timeout .

Lamentablemente, no podremos terminar esto para 3.0. Moviéndose al Futuro.
Todavía está en la parte superior de nuestra cartera de pedidos, pero, de manera realista, no podremos hacerlo para 3.0 :(.

@karelz oh no :( Solo iba a volver a publicar diciendo cómo me encontré con esto nuevamente en la segunda compañía consecutiva. ¿Cuánto tiempo sería la ventana para enviar un PR para 3.0 si quisiera abordarlo yo mismo? Saludos

Tengo dificultades para rastrear exactamente lo que está sucediendo en el grupo de comentarios anteriores con personas que informan diferencias entre obtener TaskCancelledException y OperationCancelledException según el contexto, ¿alguien de MSFT puede aclarar eso?

Me gusta este comentario de @mikernet :

Como en un bloque catch que atrapa TaskCanceledException no atrapa la excepción, necesita específicamente un bloque catch para OperationCanceledException.

cc @davidsh @stephentoub tal vez?

TaskCanceledException se deriva de OperationCanceledException y contiene un poco de información adicional. Solo captura OperationCanceledException.

@karelz , ¿sería completamente desagradable la opción 1 que publicaste originalmente, solo haciendo que arroje una excepción de tiempo de espera?

Cosas como StackExchange.Redis arrojan una excepción derivada de la excepción de tiempo de espera, por eso sería bueno capturar la excepción de tiempo de espera para HttpClient...

Entiendo que @stephentoub dada la herencia, solo este comentario a continuación de @mikernet me está volviendo loco... ¿Este comportamiento que describe tendría que ser algún tipo de error? Pero si es cierto, influiría en la corrección o solución alternativa...

"un bloque catch que atrapa TaskCanceledException no atrapa la excepción, necesita específicamente un bloque catch para una OperationCanceledException"

¿O está diciendo que en algunos lugares arroja un OperationEx y en otros lugares un TaskEx? ¿Quizás la gente está viendo algo proveniente del middleware ASP y pensando que es de HttpClient?

solo este comentario a continuación de @mikernet me está volviendo loco... ¿Este comportamiento que describe tendría que ser algún tipo de error?

¿Por qué es eso "volar tu mente"? Si la excepción B se deriva de la excepción A, un bloque catch para B no detectará una excepción que sea concretamente A, por ejemplo, si tengo un bloque catch para ArgumentNullException, no detectará " throw new ArgumentException(...)".

¿O está diciendo que en algunos lugares arroja un OperationEx y en otros lugares un TaskEx?

Correcto. En general, el código debe asumir solo OCE; De la forma en que están escritas las cosas, algún código puede terminar arrojando el TCE derivado, por ejemplo, una tarea completada a través de TaskCompletionSource.TrySetCanceled producirá una TaskCanceledException.

Gracias Stephen, por el sonido de las cosas en el hilo, varias personas tenían la impresión de que TaskCE se lanzaba por todas partes, lo que habría hecho que el comportamiento mencionado por mikernet no tuviera sentido... Ahora que lo ha aclarado, hace que su observación lógica :)

@stephentoub , ¿esta guía está documentada en alguna parte? Haciendo ping a @guardrex

En general, el código debe asumir solo OCE ...

Trabajo el conjunto de documentos ASP.NET en estos días. Abra un problema para los gatos de dotnet/docs...

https://github.com/dotnet/docs/issues

¿Es posible reiniciar esta conversación donde hubo algunas propuestas concretas con @karelz y @davidsh ? Simplemente se siente como si alguien (s) que tiene la propiedad del modelo HttpClient API necesita tomar decisiones sobre la dirección a seguir. Hubo una discusión sobre las excepciones de nivel superior que siempre son HttpRequestException (¿aparte de las cancelaciones explícitas?), Mezclado con un tema aparente a más largo plazo sobre un refactor más grande para modificar HttpRequestion para incluir una nueva propiedad de enumeración similar a WebExceptionStatus...

Parece que todo este problema se está complicando en relación con el cambio de código real que se necesita. ¿Hay otras personas que tienen una participación/propiedad en el modelo de la API de HttpClient que deberían incluirse para tomar algunas decisiones? ¡Muchas gracias a todos!

Tengo el mismo problema aquí; sin embargo, cancellationToken.IsCancellationRequested está devolviendo true :

    public class TimeoutHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            try
            {
                var response = await base.SendAsync(request, cancellationToken);
                response.EnsureSuccessStatusCode();

                return response;
            }
            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
            {
                throw new TimeoutException("Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.");
            }
        }

Estoy forzando un tiempo de espera. La excepción planteada es TaskCancelledException con IOException como excepción interna:

System.Threading.Tasks.TaskCanceledException: The operation was canceled. ---> System.IO.IOException: Unable to read data from the transport connection: A operação de E/S foi anulada devido a uma saída de thread ou a uma requisição de aplicativo. ---> System.Net.Sockets.SocketException: A operação de E/S foi anulada devido a uma saída de thread ou a uma requisição de aplicativo
   --- End of inner exception stack trace ---
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.ReadNextResponseHeaderLineAsync(Boolean foldedHeadersAllowed)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

...

(Perdón por el texto de excepción traducido; aparentemente, alguien pensó que sería una buena idea llevar esto también a .NET Core)

Como puede ver, esperaba verificar IsCancellationRequest para solucionar este problema como lo sugirieron otros, pero incluso en los tiempos de espera está devolviendo true .

Actualmente estoy pensando en implementar un temporizador para verificar si el tiempo de espera configurado ha pasado. Pero, obviamente, quiero morir , esta es una solución horrible.

Hola @FelipeTaveira ,

Su enfoque no funciona, porque la cancelación se maneja antes de su TimeoutHandler , por el propio HttpClient . Su controlador recibe un token de cancelación que es cancelado por HttpClient cuando ocurre un Timeout . Por lo tanto, verificar que el token de cancelación no se cancele solo funciona en el código que llama HttpClient , no en el código llamado por HttpClient .

La forma de hacer que funcione con un controlador personalizado es deshabilitar el HttpClient de Timeout configurándolo en Timeout.InfiniteTimeSpan y controlando el comportamiento del tiempo de espera en su propio controlador, como se muestra en este artículo (código completo aquí )

@thomaslevesque ¡Oh, gracias! Había leído su código y pensé que esa parte se trataba de admitir la configuración de tiempo de espera por solicitud y no me había dado cuenta de que también era necesario admitir la cosa TimeoutException .

Espero que esto se solucione en el futuro.

Por lo que vale, también encuentro que TaskCanceledException arrojado en los tiempos de espera es confuso.

Puedo dar fe de que TaskCanceledExceptions fueron una fuente importante de confusión al tratar con problemas de producción en mi trabajo anterior. Por lo general, envolvemos las llamadas a HttpClient y lanzamos TimeoutExceptions cuando corresponda.

@eiriktsarpalis gracias. Planeamos abordarlo en 5.0, por lo que puede recogerlo en ese período de tiempo si lo desea, ahora que se unió a nuestro equipo 😇.

¿1 año? 😵 ¿Dónde está esa prometida agilidad de .NET Core?

Juzgar la agilidad de todo el proyecto en función de un problema es un poco... injusto.
Sí, es un poco vergonzoso, todavía tenemos este problema, sin embargo, es difícil de solucionar (con impacto de compatibilidad) y solo había objetivos comerciales de mayor prioridad para abordar primero (en redes): vea los cambios en hitos a lo largo de la historia de este problema

Por cierto: como en muchos otros proyectos de OSS, no hay SLA o ETA ni promesas sobre cuándo se solucionará el problema. Todo depende de las prioridades, otros trabajos, la dificultad y la cantidad de expertos que puedan abordar el problema.

Juzgar la agilidad de todo el proyecto en función de un problema es un poco... injusto.

Bueno, era una pregunta honesta. Ya han pasado dos años y cuando vi que va a tomar otro me sorprendió un poco. Sin embargo, sin ánimo de ofender 🙂. Sé que para solucionar este problema había/hay algunas preguntas que tenían/tienen que ser respondidas primero.

En un mundo de compromisos... No me gusta, pero aquí hay otra opción para masticar:

Podríamos continuar lanzando TaskCanceledException , pero establecer su CancellationToken en un valor centinela.

c# catch(TaskCanceledException ex) when(ex.CancellationToken == HttpClient.TimeoutToken) { }

Y otro: lanza un nuevo HttpTimeoutException que deriva de TaskCanceledException .

Y otro: lanza un nuevo HttpTimeoutException que deriva de TaskCanceledException .

Creo que eso resolvería todas las preocupaciones. Facilita la detección específica de un tiempo de espera, y sigue siendo un TaskCanceledException , por lo que el código que actualmente detecta esa excepción también detectará la nueva (sin cambios importantes).

Y otro: lanza una nueva HttpTimeoutException que se deriva de TaskCanceledException.

Parece bastante feo, pero probablemente sea un buen compromiso.

Y otro: lanza un nuevo HttpTimeoutException que deriva de TaskCanceledException .

Creo que eso resolvería todas las preocupaciones. Facilita la detección específica de un tiempo de espera, y sigue siendo un TaskCanceledException , por lo que el código que actualmente detecta esa excepción también detectará la nueva (sin cambios importantes).

Bueno, _podría_ haber gente por ahí escribiendo código como este:

try
{
  // ...
}
catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException))
{
}

Así que esto sería técnicamente un cambio radical.

También creo que tiene un poco de lo peor de ambos mundos. Todavía no obtendremos TimeoutException generales al mismo tiempo que tenemos un cambio importante.

podría haber gente escribiendo código como este, por lo que técnicamente sería un cambio importante

FWIW, casi todos los cambios posiblemente se están rompiendo, por lo que para avanzar en cualquier cosa, uno tiene que trazar una línea en alguna parte. En corefx, no consideramos que devolver o lanzar un tipo más derivado se rompa.
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md#exceptions

En un mundo de compromisos... No me gusta, pero aquí hay otra opción para masticar:

Podríamos continuar lanzando TaskCanceledException , pero establecer su CancellationToken en un valor centinela.

catch(TaskCanceledException ex) when(ex.CancellationToken == HttpClient.TimeoutToken)
{
}

Realmente me gusta esta. ¿Tiene alguna otra desventaja además de ser ""feo""? ¿Cuánto es ex.CancellationToken hoy cuando ocurre un tiempo de espera?

¿Tiene alguna otra desventaja además de ser "feo"?

Sería muy fácil para alguien pensar que TimeoutToken se puede pasar y usar para señalar la cancelación cuando se produce un tiempo de espera, ya que este es el diseño que se usa en otros lugares para tales cosas.

Realmente me gusta esta. ¿Tiene alguna otra desventaja además de ser ""feo""? ¿Cuánto es ex.CancellationToken hoy cuando ocurre un tiempo de espera?

Es mucho menos intuitivo que capturar un tipo de excepción específico, que se puede mencionar explícitamente en la documentación.

Estoy acostumbrado a considerar OperationCanceledException y tipos derivados para indicar una ejecución interrumpida, no un error de aplicación. La excepción que arroja HttpClient en el tiempo de espera suele ser un error: hubo una llamada a una API HTTP y no estaba disponible. Este comportamiento genera complicaciones al procesar excepciones en un nivel superior, por ejemplo, en el middleware de canalización de ASP.NET Core, ya que este código debe saber que hay dos tipos de OperationCanceledException , error y no error, o te obliga a envolver cada llamada HttpClient en un bloque de lanzamiento y captura.

Y otro: lanza una nueva HttpTimeoutException que se deriva de TaskCanceledException.

Parece bastante feo, pero probablemente sea un buen compromiso.

Sin embargo, ¿es realmente tan feo, dadas las restricciones de diseño involucradas y las consideraciones de compatibilidad con versiones anteriores? No sé si alguien en este hilo ha propuesto una solución obviamente superior que no tenga inconvenientes (ya sea el uso o la pureza del diseño, etc.)

Queremos profundizar más en esto que simplemente cambiar el tipo de excepción. También existe un problema de larga data de "¿dónde excedimos el tiempo de espera?" -- por ejemplo, es posible que se agote el tiempo de espera de una solicitud sin haber tocado el cable -- que debemos analizar, y una solución debe ser compatible con la dirección en la que vamos allí.

Queremos profundizar más en esto que simplemente cambiar el tipo de excepción. También existe un problema de larga data de "¿dónde excedimos el tiempo de espera?" -- por ejemplo, es posible que se agote el tiempo de espera de una solicitud sin haber tocado el cable -- que debemos analizar, y una solución debe ser compatible con la dirección en la que vamos allí.

¿Es esto realmente algo que necesitas saber? Quiero decir, ¿manejará el error de manera diferente dependiendo de dónde se agotó el tiempo de espera? Para mí, solo saber que se produjo un tiempo de espera suele ser suficiente.
No me importa si quiere hacer más, simplemente no quiero que este cambio pierda el tren de .NET 5 debido a requisitos adicionales :wink:

Creo que el problema que planteó @scalablecory es digno de mención, pero estoy de acuerdo en que no debería impedir que esto se solucione antes. Puede decidir sobre un mecanismo para permitir que se evalúe el motivo del tiempo de espera, pero no se demore en solucionar el problema de excepción para eso... siempre puede agregar una propiedad Reason a la excepción más adelante.

Estoy de acuerdo con @thomaslevesque; No he oído eso como un gran problema antes. ¿De dónde viene eso?

¿Es ese un caso de depuración bastante especializado en el que estaría bien investigar el motivo para requerir una pequeña acción explícita, mientras que para el desarrollador general solo tendrían que saber/preocuparse por manejar una sola cosa?

Queremos profundizar más en esto que simplemente cambiar el tipo de excepción. También existe un problema de larga data de "¿dónde excedimos el tiempo de espera?" -- por ejemplo, es posible que se agote el tiempo de espera de una solicitud sin haber tocado el cable -- que debemos analizar, y una solución debe ser compatible con la dirección en la que vamos allí.

Supongo que la implicación aquí es que debería ser responsabilidad de cada HttpMessageHandler lanzar el tipo de excepción de tiempo de espera correcto en lugar de que HttpClient arregle las cosas.

Supongo que la implicación aquí es que debería ser responsabilidad de cada HttpMessageHandler lanzar el tipo de excepción de tiempo de espera correcto en lugar de que HttpClient arregle las cosas.

No veo cómo eso es posible con la abstracción definida hoy. HttpMessageHandler no tiene conocimiento del tiempo de espera de HttpClient; en lo que respecta a un HttpMessageHandler, solo se entregó un token de cancelación, que podría haber solicitado la cancelación por varias razones, incluida la señalización del token de cancelación de la persona que llama, la llamada CancelPendingRequests de HttpClient, el vencimiento del tiempo de espera forzado de HttpClient, etc.

¿Es esto realmente algo que necesitas saber? Quiero decir, ¿manejará el error de manera diferente dependiendo de dónde se agotó el tiempo de espera? Para mí, solo saber que se produjo un tiempo de espera suele ser suficiente.

Es útil saber si el servidor puede haber procesado su solicitud. También es excelente para el diagnóstico: un servidor remoto que no se agota pero el cliente informa de los tiempos de espera es una experiencia frustrante.

@davidsh puede tener más información.

Estoy de acuerdo con @thomaslevesque; No he oído eso como un gran problema antes. ¿De dónde viene eso?

Fue un punto planteado durante una reunión reciente de NCL. Puede ser que decidamos que esto no es lo suficientemente importante como para exponer o retrasar las cosas, pero primero vale la pena discutirlo rápidamente.

@dotnet/ncl @stephentoub Vamos a poner esto a dormir. Sabemos que cualquier cosa que hagamos se romperá sutilmente. Esta parece la opción menos mala. Propongo avanzar con:

lanza un nuevo HttpTimeoutException que se deriva de TaskCanceledException .

¿Objeciones?

lanza una nueva HttpTimeoutException que se deriva de TaskCanceledException.

¿Cómo hacemos esto en la práctica? Probablemente signifique que tenemos que limpiar nuestro código y encontrar todos los lugares que llamen a CancellationToken.ThrowIfCancellationRequested y, en su lugar, convertirlo en:

c# if (token.IsCancellationRequested) throw new HttpTimeoutException(EXTRASTUFF, token);

así como lugares explícitos a los que también podríamos estar llamando OperationCancelledException .

¿Hay alguna opción que no requiera repasar todo el código relacionado de todos modos?

¿Hay alguna opción que no requiera repasar todo el código relacionado de todos modos?

Probablemente no, pero puede haber lugares que no controlamos (todavía no estoy seguro) y, por lo tanto, tendremos que tomar potencialmente una TaskCancelledException/OperationCancelledException aleatoria y volver a generar la nueva HttpTimeoutException.

Probablemente signifique que tenemos que limpiar nuestro código y encontrar todos los lugares que llamen a CancellationToken.ThrowIfCancellationRequested y, en su lugar, convertirlo en:

¿Hay alguna opción que no requiera repasar todo el código relacionado de todos modos?

No exactamente.

En lo que respecta a los controladores, todo lo que reciben es un token de cancelación: no saben ni les importa de dónde vino ese token, quién lo produjo o por qué se solicitó la cancelación. Todo lo que saben es que es posible que se solicite la cancelación en algún momento y, como respuesta, lanzan una excepción de cancelación. Entonces, no hay nada que hacer aquí en los controladores.

Es HttpClient mismo el que está creando el CancellationTokenSource relevante y configurando un tiempo de espera en él:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L488
El tiempo de espera es enteramente invención de HttpClient, y solo él sabe si querría tratar una excepción de cancelación que surgió como especial.

Eso significa que el código al que se hace referencia anteriormente tendría que ser refactorizado para rastrear el tiempo de espera por separado, ya sea como un CancellationTokenSource separado, o más probablemente manejando la lógica del tiempo de espera por sí solo, y además de cancelar el CTS, también estableciendo una bandera que puede entonces también revisa. Es probable que esto no sea de pago por jugar, ya que agregaremos asignaciones adicionales aquí, pero desde la perspectiva del código es relativamente sencillo.

Luego, en lugares en HttpClient como:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L536 -L541
y
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L562 -L566
cuando detecta una excepción, necesitaría una cancelación de caso especial, para verificar si el tiempo de espera expiró y, si lo hubiera hecho, asumir que fue la causa de la cancelación y lanzar una nueva excepción del tipo deseado.

necesitaría una cancelación de caso especial, para verificar si el tiempo de espera expiró y, si lo hubiera hecho, asumir que fue la causa de la cancelación y lanzar una nueva excepción del tipo deseado.

¿Cómo funciona esto si la propiedad HttpClient.Timeout no está involucrada? Tal vez esté configurado en "infinito". ¿Qué pasa con los tokens de cancelación pasados ​​directamente a las API de HttpClient (GetAsync, SendAync, etc.)? Supongo que eso funcionaría igual?

¿Cómo funciona esto si la propiedad HttpClient.Timeout no está involucrada?

No entiendo la pregunta... ese es el único tiempo muerto aquí del que estamos hablando. Si se establece en Infinito, este tiempo de espera nunca provocará la cancelación y no habrá nada que hacer. Ya tenemos un caso especial de Timeout == Infinite y continuaríamos con:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L481

¿Qué pasa con los tokens de cancelación pasados ​​directamente a las API de HttpClient (GetAsync, SendAync, etc.)? Supongo que eso funcionaría igual?

Combinamos cualquier token que se pase con uno de nuestra propia creación:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L485
Realizaríamos un seguimiento de nuestro propio manejo de HttpClient.Timeout, como se mencionó en mis comentarios anteriores. Si la persona que llama elige pasar un token que configuró con un tiempo de espera, entonces tratarlo de manera especial depende de ellos: en lo que a nosotros respecta, es solo una solicitud de cancelación.

¿Objeciones?

¿Por qué introducir un nuevo tipo de excepción derivada que "compita" con la TimeoutException existente en lugar de, por ejemplo, lanzar una excepción de cancelación que envuelve una TimeoutException como la excepción interna? Si el objetivo es permitir que un consumidor determine mediante programación si la cancelación se debió a un tiempo de espera, ¿eso sería suficiente? Parecería comunicar la misma información, sin necesidad de nueva superficie.

¿Por qué introducir un nuevo tipo de excepción derivada que "compita" con la TimeoutException existente en lugar de, por ejemplo, lanzar una excepción de cancelación que envuelve una TimeoutException como la excepción interna?

Por comodidad, sobre todo. Es más fácil e intuitivo escribir

catch(HttpTimeoutException)

que

catch(OperationCanceledException ex) when (ex.InnerException is TimeoutException)

Dicho esto, para mí está bien de cualquier manera, siempre que sea razonablemente fácil identificar un tiempo de espera.

digamos, lanzando una excepción de cancelación que envuelve una TimeoutException como la excepción interna?

Creo que es un buen compromiso. Ayudaría a eliminar la ambigüedad de los registros de excepción, que era mi fuente principal de molestias al tratar con este tipo de tiempos de espera. Probablemente también comunique mejor el hecho de que se trata de HttpClient que cancela una solicitud, en lugar de que el controlador subyacente genere un tiempo de espera. Además, no hay cambios de última hora.

triaje:

  • Actualizaremos este escenario para tener un InnerException de TimeoutException con un mensaje para registro/diagnóstico que distinga fácilmente la cancelación como causada por la activación del tiempo de espera.
  • Actualizaremos la documentación con orientación sobre las formas en que uno podría inspeccionar programáticamente para diferenciar la cancelación: verificar TimeoutException , usar un Timeout infinito y pasar un CancellationToken con un período de tiempo de espera, etc.

Reconocemos que cualquier solución aquí implica algún compromiso y creemos que esto proporcionará un buen nivel de compatibilidad con el código existente.

¿Por qué introducir un nuevo tipo de excepción derivada que "compita" con la TimeoutException existente en lugar de, por ejemplo, lanzar una excepción de cancelación que envuelve una TimeoutException como la excepción interna?

Por comodidad, sobre todo. Es más fácil e intuitivo escribir

catch(HttpTimeoutException)

que

catch(OperationCanceledException ex) when (ex.InnerException is TimeoutException)

Dicho esto, para mí está bien de cualquier manera, siempre que sea razonablemente fácil identificar un tiempo de espera.

@scalablecory Estoy desanimado porque parece que me perdí la discusión por unas horas: mi experiencia al comunicar este tipo de cosas con los desarrolladores generales de LoB .NET es que detectar una HttpTimeoutException derivada es mucho más fácil de entender/adoptar para las personas que tener que hacerlo. recuerde usar un filtro de excepción en una excepción interna. Ni siquiera todos saben qué es un filtro de excepción.

Al mismo tiempo, reconozco totalmente que este es uno de esos escenarios en los que no hay soluciones claras ganadoras/sin compromisos, como dijiste.

Estuvimos de acuerdo en ese punto, @ericsampson , pero sería un gran cambio para muchos usuarios existentes. Como mencionó @scalablecory , nos comprometimos a hacer mejoras mientras limitamos el daño de compatibilidad.

Cuando pienso en las excepciones, en general, considero mejor el enfoque de envoltura. La excepción derivada de OperationCanceledException hace que dicha excepción esté disponible solo en uso asíncrono. E incluso es ajustado a las tareas. Como vimos en el pasado, hubo varias evoluciones de enfoques asíncronos y considero la tarea como un detalle de implementación; sin embargo, TimeoutException, por ejemplo, es parte del concepto de socket que no cambiará.

Estuvimos de acuerdo en ese punto, @ericsampson , pero sería un gran cambio para muchos usuarios existentes. Como mencionó @scalablecory , nos comprometimos a hacer mejoras mientras limitamos el daño de compatibilidad.

Aunque no estoy necesariamente en desacuerdo con la opción de envoltura, creo que vale la pena señalar que, por lo que he entendido, el problema no es que la opción derivada sea un cambio importante, sino que la de envoltura es más simple. Como @stephentoub mencionó anteriormente en la discusión, lanzar una excepción derivada no se considera un cambio importante según las pautas de corefx.

Derivar HttpTimeoutException de TaskCanceledException viola la relación "es un" entre estas dos excepciones. Todavía hará que el código de llamada piense que ocurrió la cancelación, a menos que maneje específicamente HttpTimeoutException. Este es básicamente el mismo manejo que tenemos que hacer en el momento en que verificamos si se cancela el token de cancelación para determinar si se trata de un tiempo de espera o no. Además, tener dos excepciones de tiempo de espera en la biblioteca central es confuso.
Agregar TimeoutException como una excepción interna a TaskCanceledException no proporcionará ninguna información adicional en la mayoría de los casos, ya que podemos verificar el token de cancelación y determinar si se agotó el tiempo de espera o no.

Creo que la única solución sensata es cambiar la excepción lanzada en el tiempo de espera a TimeoutException. Sí, será un cambio importante, pero el problema ya abarca varios lanzamientos importantes y los cambios importantes son para lo que son los lanzamientos principales.

¿Se puede actualizar la documentación de las versiones existentes para indicar que los métodos podrían arrojar TaskCanceledException y/o OperationCanceledException (ambos tipos concretos parecen ser una posibilidad) cuando se agota el tiempo de espera? Actualmente, la documentación indica que HttpRequestException se ocupa de los tiempos de espera, lo cual es incorrecto y engañoso.

@qidydl ese es el plan... documentará completamente las formas en que se pueden manifestar los tiempos de espera y cómo detallarlos.

Gracias @alnikola y @scalablecory y @davidsh y @karelz y todos los demás involucrados en esta discusión/implementación, realmente lo aprecio.

¿Fue útil esta página
0 / 5 - 0 calificaciones