¿Qué código de estado HTTP devolver si múltiples acciones terminan con diferentes estados?

72

Estoy creando una API donde el usuario puede pedirle al servidor que realice múltiples acciones en una solicitud HTTP. El resultado se devuelve como una matriz JSON, con una entrada por acción.

Cada una de estas acciones puede fallar o tener éxito independientemente una de la otra. Por ejemplo, la primera acción puede tener éxito, la entrada a la segunda acción puede estar mal formateada y no puede validarse y la tercera acción puede causar un error inesperado.

Si hubiera una solicitud por acción, devolvería los códigos de estado 200, 422 y 500 respectivamente. Pero ahora, cuando solo hay una solicitud, ¿qué código de estado debo devolver?

Algunas opciones:

  • Siempre devuelva 200 y brinde información más detallada en el cuerpo.
  • ¿Quizás siga la regla anterior solo cuando hay más de una acción en la solicitud?
  • ¿Quizás devolver 200 si todas las solicitudes tienen éxito, de lo contrario 500 (o algún otro código)?
  • Simplemente use una solicitud por acción y acepte la sobrecarga adicional.
  • Algo completamente diferente?
Anders
fuente
3
Su pregunta me hizo pensar en otra: programmers.stackexchange.com/questions/309147/…
AilurusFulgens
77
Ligeramente relacionado también: programmers.stackexchange.com/questions/305250/… (consulte la respuesta aceptada sobre la separación entre los códigos de estado HTTP y los códigos de aplicación)
44
¿Cuál es la ventaja que logra al agrupar esas solicitudes? ¿Se trata de lógica empresarial, como una transacción a través de múltiples recursos, o se trata de rendimiento? ¿O algo mas?
Luc Franken
55
Ok, en ese caso, sugeriría encarecidamente mejorar ese rendimiento en otras áreas. Pruebe cosas como una interfaz de usuario optimista, solicite lotes, almacenamiento en caché, etc. antes de implementar esta complejidad en su capa empresarial. ¿Tienes una idea clara de dónde pierdes más tiempo?
Luc Franken el
44
... no tengas demasiadas esperanzas de que las personas vean correctamente esos estados. La mayoría de los programas solo buscan los más comunes y fallan o se portan mal si obtienen un código de estado inesperado. (Recuerdo que también hubo una presentación en DefCon sobre cómo proteger su sitio de los rastreadores mediante el envío de estados de salida aleatorios que el navegador ignora y simplemente muestra por qué los rastreadores a veces se consideran errores y, por lo tanto, dejan de rastrear esa parte de su sitio web).
Bakuriu

Respuestas:

21

La respuesta corta y directa

Dado que la solicitud habla de ejecutar la lista de tareas (las tareas son el recurso del que estamos hablando aquí), entonces, si el grupo de tareas se ha adelantado a la ejecución (es decir, independientemente del resultado de la ejecución), sería sensato que el estado de respuesta será 200 OK. De lo contrario, si hubiera un problema que impidiera la ejecución del grupo de tareas, como una falla en la validación de los objetos de la tarea , o algún servicio requerido no está disponible, por ejemplo, el estado de respuesta debería indicar ese error. Después de eso, cuando comienza la ejecución de las tareas, ya que las tareas a realizar se enumeran en el cuerpo de la solicitud, entonces esperaría que los resultados de la ejecución se enumeren en el cuerpo de la respuesta.


La respuesta larga y filosófica.

Experimenta este dilema porque se está desviando de lo que HTTP fue diseñado para. No lo está interactuando para administrar recursos, más bien, lo está utilizando como medio de invocación de método remoto (lo cual no es muy extraño, pero funciona mal sin un esquema preconcebido).

Dicho lo anterior, y sin valor para convertir esta respuesta en una guía larga y obstinada, el siguiente es un esquema de URI que se ajusta a un enfoque de gestión de recursos:

  • /tasks
    • GET enumera todas las tareas, paginado
    • POST agrega una sola tarea
  • /tasks/task/[id]
    • GET responde con un objeto de estado de una sola tarea
    • DELETE cancela / elimina una tarea
  • /tasks/groups
    • GET enumera todos los grupos de tareas, paginados
    • POST agrega un grupo de tareas
  • /tasks/groups/group/[id]
    • GET responde con el estado de un grupo de tareas
    • DELETE cancela / elimina el grupo de tareas

Esta estructura habla de recursos, no de qué hacer con ellos. Lo que se está haciendo con los recursos es la preocupación de otro servicio.

Otro punto importante es que es aconsejable no bloquear durante mucho tiempo en un controlador de solicitudes HTTP. Al igual que la interfaz de usuario, una interfaz HTTP debería responder, en una escala de tiempo que es un poco más lenta (porque esta capa se ocupa de IO).

Hacer el movimiento hacia el diseño de una interfaz HTTP que administre estrictamente los recursos es probablemente tan difícil como alejar el trabajo de un hilo de la interfaz de usuario cuando se hace clic en un botón. Requiere que el servidor HTTP se comunique con otros servicios para ejecutar tareas en lugar de ejecutarlas en el controlador de solicitudes. Esta no es una implementación superficial, es un cambio de dirección.


Algunos ejemplos de cómo se usaría dicho esquema URI

Ejecutar una sola tarea y seguir el progreso:

  • POST /tasks con la tarea a ejecutar
    • GET /tasks/task/[id]hasta que el objeto de respuesta completedtenga un valor positivo mientras muestra el estado / progreso actual

Ejecutando una sola tarea y esperando su finalización:

  • POST /tasks con la tarea a ejecutar
    • GET /tasks/task/[id]?awaitCompletion=truehasta que completedtenga un valor positivo (es probable que tenga un tiempo de espera, por lo que esto debería repetirse)

Ejecutar un grupo de tareas y seguir el progreso:

  • POST /tasks/groups con el grupo de tareas a ejecutar
    • GET /tasks/groups/group/[groupId]hasta que la completedpropiedad del objeto de respuesta tenga valor, mostrando el estado de la tarea individual (3 tareas completadas de 5, por ejemplo)

Solicitar una ejecución para un grupo de tareas y esperar su finalización:

  • POST /tasks/groups con el grupo de tareas a ejecutar
    • GET /tasks/groups/group/[groupId]?awaitCompletion=true hasta que responda con un resultado que denote la finalización (es probable que tenga tiempo de espera, por lo que debe colocarse en bucle)
Ben Barkay
fuente
Creo que hablar de lo que tiene sentido semánticamente es la forma correcta de abordar esto. ¡Gracias!
Anders
2
Iba a proponer esta respuesta si aún no hubiera estado allí. Es imposible realizar múltiples solicitudes en una sola solicitud HTTP. Por otro lado, es perfectamente posible hacer una sola solicitud HTTP que diga "realice las siguientes acciones y dígame cuáles son los resultados". Y esto es lo que está pasando aquí.
Martin Kochanski
Aceptaré esta respuesta incluso si tiene lejos de la mayoría de los votos. Si bien otras respuestas también son buenas, creo que esta es la única que razona sobre la semántica de HTTP.
Anders
87

Mi voto sería dividir estas tareas en solicitudes separadas. Sin embargo, si hay demasiados viajes de ida y vuelta, me encontré con el código de respuesta HTTP 207 - Estado múltiple

Copiar / pegar desde este enlace:

Una respuesta de múltiples estados transmite información sobre múltiples recursos en situaciones en las que múltiples códigos de estado podrían ser apropiados. El cuerpo de respuesta de estado múltiple predeterminado es una entidad HTTP text / xml o application / xml con un elemento raíz 'multiestatus'. Otros elementos contienen códigos de estado de las series 200, 300, 400 y 500 generados durante la invocación del método. Los códigos de estado de la serie 100 NO DEBEN registrarse en un elemento XML de 'respuesta'.

Aunque '207' se usa como el código de estado de respuesta general, el destinatario debe consultar el contenido del cuerpo de respuesta de estado múltiple para obtener más información sobre el éxito o el fracaso de la ejecución del método. La respuesta PUEDE usarse en el éxito, el éxito parcial y también en situaciones de falla.

neilsimp1
fuente
22
207parece ser lo que quiere el OP, pero realmente quiero enfatizar que probablemente sea una mala idea tener este enfoque de solicitud múltiple en uno. Si la preocupación es el rendimiento, entonces debe diseñar arquitecturas escalables horizontalmente en el estilo de la nube (que es algo en lo que los sistemas basados ​​en HTTP son excelentes)
reinstale Mónica el
44
@DavidGrinberg No podría estar más en desacuerdo. Si las acciones individuales son baratas, la sobrecarga de manejar una solicitud puede ser mucho más costosa que la acción misma. Su sugerencia podría conducir a escenarios en los que la actualización de varias filas en una base de datos se realiza utilizando una transacción por fila por separado porque cada fila se envía como una solicitud por separado. Esto no solo es terriblemente ineficiente, sino que también significa que no será posible actualizar varias filas atómicamente si es necesario. El escalado horizontal es importante pero no reemplaza los diseños eficientes.
Kasperd
44
Bien dicho y señalando un problema típico de las implementaciones de API REST realizadas por personas ignorantes de las realidades de las necesidades comerciales, como el rendimiento y / o la atomicidad. Es por eso que, por ejemplo, la especificación REST de OData tiene un formato por lotes para operaciones múltiples en una sola llamada: hay una necesidad real de ello.
TomTom
8
@TomTom, el OP no quiere atomicidad. Sería mucho más fácil diseñarlo, ya que solo hay un estado de una operación atómica. Además, la especificación HTTP permite operaciones de procesamiento por lotes para el rendimiento, a través de la multiplexación HTTP / 2 (naturalmente, el soporte HTTP / 2 es otra cuestión, pero la especificación lo permite).
Paul Draper
2
@David Habiendo trabajado en algunos problemas de HPC en el pasado, en mi experiencia, el costo de enviar un solo byte es casi lo mismo que enviar mil (los diferentes medios de transferencia tienen una sobrecarga diferente, pero rara vez es mejor que esto). Entonces, si el rendimiento es una preocupación, no veo cómo enviar múltiples solicitudes no tendría una gran sobrecarga. Ahora, si pudiera multiplexar múltiples solicitudes a través de la misma conexión, este problema desaparecería, pero según tengo entendido, esa es solo una opción con HTTP / 2 y el soporte para esto es bastante limitado. ¿O me estoy perdiendo algo?
Voo
24

Aunque el estado múltiple es una opción, devolvería 200 (Todo está bien) si todas las solicitudes tuvieron éxito y un error (500 o quizás 207) de lo contrario.

El caso estándar generalmente debería ser 200: todo funciona. Y los clientes solo deberían comprobarlo. Y solo si ocurrió el caso de error, puede devolver un 500 (o un 207). Creo que el 207 es una opción válida en el caso de al menos un error, pero si ve el paquete completo como una transacción, también podría enviar 500. - El cliente querrá interpretar el mensaje de error de cualquier manera.

¿Por qué no siempre enviar 207? - Porque los casos estándar deberían ser fáciles y estándar. Mientras que los casos excepcionales pueden ser excepcionales. Un cliente solo debería leer el cuerpo de respuesta y tomar decisiones complejas adicionales, si una situación excepcional lo amerita.

Falco
fuente
66
No estoy del todo de acuerdo. Si las subrequest 1 y 3 tuvieron éxito, obtendrá un recurso combinado y tendrá que verificar la respuesta combinada de todos modos. Solo tiene un caso más para considerar. Si la respuesta = 200 o la respuesta secundaria 1 = 200, la solicitud 1 se realizó correctamente. Si la respuesta = 200 o la subrespuesta 2 = 200, entonces la solicitud 2 tiene éxito y así sucesivamente en lugar de simplemente probar la respuesta secundaria.
gnasher729
1
@ gnasher729 realmente depende de la aplicación. Me imagino una acción dirigida por el usuario, que simplemente fluirá al siguiente paso con (todo bien) cuando todas las solicitudes se hayan realizado correctamente. - Si algo salió mal (estado global <= 200), entonces debe mostrar errores detallados y cambiar el flujo de trabajo, y solo necesita una única verificación para cada subrequest, ya que está en la función "handleMixedState" y no en la función "handleAllOk" .
Falco
Realmente depende de lo que significa. Por ejemplo, tengo un punto final que controla las estrategias comerciales. Puede "iniciar" una lista de identificadores en una ejecución. Devolver 200 significa que la operación (procesarlos) tuvo éxito, pero no todos pueden comenzar con éxito. Lo cual, por cierto, ni siquiera se puede ver en el resultado inmediato (que comenzará) ya que el inicio puede tomar algunos segundos. La semántica en llamadas de operación múltiple depende del escenario.
TomTom
Lo más probable es que también envíe un 500 si hubiera un problema general (por ejemplo, la base de datos inactiva) para que el servidor ni siquiera intente solicitudes individuales, sino que solo puede devolver un error general. - Debido a que hay 3 resultados diferentes para el usuario: 1. Todo está bien, 2. Problema general, nada funciona, 3. Algunas solicitudes fallaron. -> Lo que generalmente conducirá a un flujo de programa completamente diferente.
Falco
1
Ok, entonces un enfoque sería: 207 = estado individual para cada solicitud. Cualquier otra cosa: el estado devuelto se aplica a cada solicitud. Tiene sentido para 200, 401, ≥ 500.
gnasher729
13

Una opción sería devolver siempre un código de estado 200 y luego devolver errores específicos en el cuerpo de su documento JSON. Así es exactamente cómo se diseñan algunas API (siempre devuelven un código de estado 200 y envían el error en el cuerpo). Para obtener más detalles sobre los diferentes enfoques, consulte http://archive.oreilly.com/pub/post/restful_error_handling.html

BigONotation
fuente
2
En este caso, me gusta la idea de usar el 200para indicar que todo está bien, la solicitud se recibió y fue válida , y luego uso el JSON para proporcionar detalles sobre lo que sucedió en esa solicitud (es decir, el resultado de las transacciones).
rickcnagy
4

Creo que neilsimp1 es correcto, pero recomendaría un rediseño de los datos que se envían de tal manera que pueda enviar 206 - Acceptedy procesar los datos más adelante. Quizás con devoluciones de llamada.

El problema al intentar enviar múltiples acciones en una sola solicitud es exactamente el hecho de que cada acción debe tener su propio "estado"

En cuanto a la importación de un CSV (no sé realmente de qué trata el OP, pero es una versión simple). PUBLICA el CSV y recupera un 206. Luego, el CSV puede importarse y puedes obtener el estado de la importación con un GET (200) contra una URL que muestra los errores por fila.

POST /imports/ -> 206
GET  /imports/1 -> 200
GET  /imports/1/errors -> 200 -> Has a list of errors

Este mismo patrón se puede aplicar a muchas operaciones por lotes.

POST /operations/ -> 206
GET  /operations/1 -> 200
GET  /operations/1/errors -> 200 - > Has a list of errors.

El código que maneja la POST solo necesita verificar que el formato de los datos de las operaciones sea válido. Luego, en algún momento posterior, las operaciones pueden ejecutarse. En un trabajador de fondo, por lo que puede escalar más fácilmente, por ejemplo. Luego puede verificar el estado de las operaciones cuando lo desee. Puede usar encuestas o devoluciones de llamadas, o transmisiones o lo que sea para abordar la necesidad de saber cuándo se completa un conjunto de operaciones.

coteyr
fuente
2

Ya hay muchas buenas respuestas aquí, pero falta un aspecto:

¿Cuál es el contrato que esperan sus clientes?

Los códigos de retorno HTTP deben indicar al menos una distinción de éxito / fracaso y, por lo tanto, desempeñar el papel de "excepciones del pobre". Entonces 200 significa "contrato completamente cumplido", y 4xx o 5xx indican incumplimiento.

Ingenuamente, esperaría que el contrato de su solicitud de acciones múltiples sea "hacer todas mis tareas", y si una de ellas falla, entonces la solicitud no fue (completamente) exitosa. Normalmente, como cliente, entendería 200 como "todo bien", y los códigos de las familias 400 y 500 me obligan a pensar en las consecuencias de una falla (parcial). Por lo tanto, use 200 para "todas las tareas realizadas" y 500 más una respuesta descriptiva en caso de una falla parcial.

Un contrato hipotético diferente podría ser "intente hacer todas las acciones". Entonces está completamente en línea con el contrato si (algunas de) las acciones fallan. Por lo tanto, siempre devolverá 200 más un documento de resultados donde encontrará la información de éxito / fracaso para las tareas individuales.

Entonces, ¿cuál es el contrato que quieres seguir? Ambos son válidos, pero el primero (200 solo en caso de que se haya hecho todo) es más intuitivo para mí y está mejor en línea con los patrones de software típicos. Y para la mayoría (con suerte) de los casos en que el servicio completó todas las tareas, es sencillo para el cliente detectar ese caso.

Un último aspecto importante: ¿Cómo comunicas la decisión de tu contrato a tus clientes? Por ejemplo, en Java, usaría nombres de métodos como "doAll ()" o "tryToDoAll ()". En HTTP, puede nombrar las URL de punto final en consecuencia, con la esperanza de que los desarrolladores de sus clientes vean, lean y comprendan los nombres (no apostaría por eso). Una razón más para elegir el contrato de menor sorpresa.

Ralf Kleberhoff
fuente
0

Responder:

Simplemente use una solicitud por acción y acepte la sobrecarga adicional.

Un código de estado describe el estado de una operación. Por lo tanto, tiene sentido tener una operación por solicitud.

Varias operaciones independientes rompen el principio en el que se basan el modelo de solicitud-respuesta y los códigos de estado. Estás luchando contra la naturaleza.

HTTP / 1.1 y HTTP / 2 han hecho que la sobrecarga de solicitudes HTTP sea mucho menor. Calculo que hay muy pocas situaciones en las que sea recomendable agrupar solicitudes independientes.


Dicho eso

(1) Puede realizar múltiples modificaciones con una solicitud PATCH ( RFC 5789 ). Sin embargo, esto requiere que los cambios no sean independientes; se aplican atómicamente (todo o nada).

(2) Otros han señalado el código 207 Multi-Status. Sin embargo, esto se define solo para WebDAV ( RFC 4918 ), una extensión de HTTP.

El código de estado 207 (Multi-Status) proporciona el estado para múltiples operaciones independientes (consulte la Sección 13 para obtener más información).

...

Una respuesta de múltiples estados transmite información sobre múltiples recursos en situaciones en las que múltiples códigos de estado podrían ser apropiados. El elemento raíz [multiestatus] [XML] contiene cero o más elementos de 'respuesta' en cualquier orden, cada uno con información sobre un recurso individual.

Una respuesta 207 WebDAV XML sería tan extraña como un pato en una API que no sea WebDAV. No hagas esto.

Paul Draper
fuente
1
Básicamente estás afirmando que @Anders tiene un problema XY . Puede que tengas razón, pero desafortunadamente, eso significa que en realidad no has respondido la pregunta que hizo (qué código de estado usar para la solicitud de acción múltiple).
Azuaron
2
@Azuaron, ¿qué tipo de cinturón funciona mejor para golpear a los niños? Creo que "N / A" es una respuesta permitida. Además, Andrés incluyó múltiples solicitudes en su lista de ideas. Apoyé sinceramente esa opción.
Paul Draper
De alguna manera extrañé que él lo mencionara. En ese caso, afirmo que es una pregunta tonta, ¡señoría!
Azuaron
1
@Azuaron Creo absolutamente que esta es una respuesta válida. Si estoy haciendo todo mal, quiero que alguien lo diga y no me dé instrucciones sobre cómo conducir mejor por un precipicio.
Anders
1
Nada prohíbe enviar JSON en la respuesta 207, siempre que el encabezado Content-Type esté configurado correctamente y coincida con lo que solicitó el cliente (Aceptar encabezado).
dolmen
0

Si realmente necesita tener varias acciones en una solicitud, ¿por qué no ajustar todas las acciones en una transacción en el back-end? De esa manera, todos tienen éxito o todos fracasan.

Como cliente que usa la API, puedo lidiar con el éxito o el fracaso completos en una llamada a la API. El éxito parcial es difícil de manejar, ya que tendría que manejar todos los posibles estados resultantes.

Decano
fuente
2
Supongo que si la solicitud fuera atómica, no habría publicado esta pregunta.
Andy
@Andy Quizás, pero no puedes asumir que ha considerado todas las implicaciones de tal diseño.
Dean
La solicitud no debe ser atómica; por ejemplo, si el # 2 falla, los cambios realizados por el # 1 aún deben persistir. Entonces, envolver todo en una transacción no es una opción.
Anders