¿Cómo debe una API REST manejar solicitudes PUT a recursos parcialmente modificables?

46

Supongamos que una API REST, en respuesta a una GETsolicitud HTTP , devuelve algunos datos adicionales en un subobjeto owner:

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'Programmer'
  }
}

Claramente, no queremos que nadie pueda PUTrespaldar

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'CEO'
  }
}

y que tenga éxito. De hecho, probablemente ni siquiera vamos a implementar una forma para que eso tenga éxito, en este caso.

Pero esta pregunta no se trata solo de los subobjetos: ¿qué se debe hacer, en general, con los datos que no se pueden modificar en una solicitud PUT?

¿Debería ser necesario que falte en la solicitud PUT?

¿Debería descartarse en silencio?

¿Debería verificarse y, si difiere del valor anterior de ese atributo, devolver un código de error HTTP en la respuesta?

¿O deberíamos usar parches JSON RFC 6902 en lugar de enviar el JSON completo?

Robin Green
fuente
2
Todo esto funcionaría. Supongo que depende de tus requerimientos.
Robert Harvey
Diría que el principio de menor sorpresa indicaría que debería faltar en la solicitud PUT. Si no es posible, verifique y si es diferente y regrese con el código de error. El descarte silencioso es peor (el envío de usuarios espera que cambie y usted les dice "200 OK").
Maciej Piechotka
2
@MaciejPiechotka el problema con eso es que no puedes usar el mismo modelo en el put como en el inserto u obtener, etc., preferiría que se usara el mismo modelo y simplemente habrá reglas de autorización de campo, así que si ingresan un valor para un campo que no deberían cambiar, obtienen un 403 Prohibido, y si más adelante se configura la autorización para permitirlo, obtienen un 401 No autorizado si no están autorizados
Jimmy Hoffa
@JimmyHoffa: ¿Por modelo te refieres al formato de datos (ya que podría ser posible reutilizar el modelo en MVC Rest framework dependiendo de la elección, si se usa alguno, OP no mencionó ninguno)? Iría con la capacidad de detección si el marco no me limitara y el error temprano es un poco más fácil de detectar / implementar que verificar el cambio (ok, no debería tocar el campo XYZ). En cualquier caso, el descarte es peor.
Maciej Piechotka

Respuestas:

46

No existe una regla, ya sea en la especificación W3C o en las reglas no oficiales de REST, que diga que a PUTdebe usar el mismo esquema / modelo que su correspondiente GET.

Es agradable si son similares , pero no es inusual PUTque las cosas se hagan de manera ligeramente diferente. Por ejemplo, he visto muchas API que incluyen algún tipo de ID en el contenido devuelto por GET, por conveniencia. Pero con a PUT, esa identificación está determinada exclusivamente por el URI y no tiene ningún significado en el contenido. Cualquier identificación encontrada en el cuerpo será ignorada en silencio.

REST y la web en general están fuertemente vinculados al Principio de Robustez : "Sea conservador en lo que hace [enviar], sea liberal en lo que acepta". Si está de acuerdo filosóficamente con esto, entonces la solución es obvia: ignore los datos no válidos en las PUTsolicitudes. Eso se aplica tanto a los datos inmutables, como en su ejemplo, como a las tonterías reales, por ejemplo, campos desconocidos.

PATCHes potencialmente otra opción, pero no debe implementar a PATCHmenos que realmente sea compatible con actualizaciones parciales. PATCHsignifica solo actualizar los atributos específicos que incluyo en el contenido ; sí no significa reemplazar toda la entidad, pero excluye algunos campos específicos . De lo que realmente estás hablando no es realmente una actualización parcial, es una actualización completa, idempotente y todo, es solo que parte del recurso es de solo lectura.

Una buena cosa si elige esta opción sería enviar un 200 (OK) con la entidad actualizada real en la respuesta, para que los clientes puedan ver claramente que los campos de solo lectura no se actualizaron.

Ciertamente, hay algunas personas que piensan lo contrario: que debería ser un error intentar actualizar una parte de solo lectura de un recurso. Hay alguna justificación para esto, principalmente sobre la base de que definitivamente devolvería un error si todo el recurso fuera de solo lectura y el usuario intentara actualizarlo. Definitivamente va en contra del principio de robustez, pero puede considerar que es más "autodocumentado" para los usuarios de su API.

Hay dos convenciones para esto, que corresponden a sus ideas originales, pero las ampliaré. El primero es prohibir que los campos de solo lectura aparezcan en el contenido y devolver un HTTP 400 (Solicitud incorrecta) si lo hacen. Las API de este tipo también deberían devolver un HTTP 400 si hay otros campos no reconocidos / inutilizables. El segundo es requerir que los campos de solo lectura sean idénticos al contenido actual y devolver un 409 (Conflicto) si los valores no coinciden.

Realmente no me gusta la verificación de igualdad con 409 porque invariablemente requiere que el cliente haga un GETpara recuperar los datos actuales antes de poder hacer un PUT. Eso no es bueno y probablemente conducirá a un bajo rendimiento, para alguien, en algún lugar. Tampoco me gusta 403 (Prohibido) para esto, ya que implica que todo el recurso está protegido, no solo una parte de él. Entonces, mi opinión es que si debe validar en lugar de seguir el principio de robustez, valide todas sus solicitudes y devuelva un 400 para cualquiera que tenga campos adicionales o no modificables.

Asegúrese de que su 400/409 / lo que sea incluye información sobre cuál es el problema específico y cómo solucionarlo.

Ambos enfoques son válidos, pero prefiero el primero de acuerdo con el principio de robustez. Si alguna vez ha experimentado trabajar con una API REST grande, apreciará el valor de la compatibilidad con versiones anteriores. Si alguna vez decide eliminar un campo existente o hacerlo de solo lectura, es un cambio compatible con versiones anteriores si el servidor simplemente ignora esos campos y los clientes antiguos seguirán funcionando. Sin embargo, si realiza una validación estricta del contenido, ya no es compatible con versiones anteriores y los clientes antiguos dejarán de funcionar. El primero generalmente significa menos trabajo tanto para el mantenedor de una API como para sus clientes.

Aaronaught
fuente
1
Buena respuesta, y votada. Sin embargo, no estoy seguro de estar de acuerdo con esto: "Si alguna vez decide eliminar un campo existente o hacerlo de solo lectura, es un cambio compatible con versiones anteriores si el servidor simplemente ignora esos campos y los clientes antiguos seguirán funcionando. " Si el cliente se basó en este campo eliminado / de solo lectura, ¿esto no afectaría el comportamiento general de la aplicación? En el caso de eliminar campos, diría que probablemente sea mejor generar explícitamente un error en lugar de ignorar los datos; de lo contrario, el cliente no tiene idea de que su actualización que funcionaba anteriormente ahora está fallando.
rinogo
Esta respuesta es incorrecta. por 2 razones de RFC2616: 1. (sección 9.1.2) PUT debe ser independiente. Ponga muchas veces y producirá el mismo resultado que poner solo una vez. 2. El
acceso
1
¿Qué sucede si realiza la verificación de igualdad solo si el valor inmutable se envió en la solicitud? Creo que esto te da lo mejor de dos mundos; no obliga a los clientes a hacer un GET, y aún así les notifica que algo está mal si enviaron un valor no válido para un inmutable.
Ahmad Abdelghany
Gracias, la comparación en profundidad que hizo en los últimos párrafos de la experiencia es exactamente lo que estaba buscando.
Dhill
9

Idem potencia

Después del RFC, un PUT tendría que entregar un objeto completo al recurso. La razón principal de esto es que PUT debe ser idempotente. Esto significa que una solicitud, que se repite, debe evaluar el mismo resultado en el servidor.

Si permite actualizaciones parciales, ya no puede ser idem-potente. Si tienes dos clientes. Cliente A y B, entonces el siguiente escenario puede evolucionar:

El cliente A obtiene una imagen de las imágenes de recursos. Contiene una descripción de la imagen, que todavía es válida. El cliente B coloca una nueva imagen y actualiza la descripción en consecuencia. La imagen ha cambiado. El cliente A ve, no tiene que cambiar la descripción, porque es lo que desea y solo coloca la imagen.

Esto conducirá a una inconsistencia, ¡la imagen tiene los metadatos incorrectos adjuntos!

Aún más molesto es que cualquier intermediario puede repetir la solicitud. En caso de que decida de alguna manera, el PUT falló.

El significado de PUT no se puede cambiar (aunque puede usarlo incorrectamente).

Otras opciones

Afortunadamente hay otra opción, esta es PATCH. PATCH es un método que le permite actualizar parcialmente una estructura. Simplemente puede enviar una estructura parcial. Para aplicaciones simples, esto está bien. No se garantiza que este método sea ideal. El cliente debe enviar una solicitud en el siguiente formulario:

PATCH /file.txt HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 20
{fielda: 1, fieldc: 2}

Y el servidor puede responder con 204 (Sin contenido) para marcar el éxito. En caso de error, no puede actualizar una parte de la estructura. El método PATCH es atómico.

La desventaja de este método es que no todos los navegadores lo admiten, pero esta es la opción más natural en un servicio REST.

Ejemplo de solicitud de parche: http://tools.ietf.org/html/rfc5789#section-2.1

Json parcheando

La opción json parece ser bastante completa y una opción interesante. Pero puede ser difícil de implementar para terceros. Tienes que decidir si tu base de usuarios puede manejar esto.

También es algo complicado, porque necesita construir un pequeño intérprete que convierta los comandos en una estructura parcial, que utilizará para actualizar su modelo. Este intérprete también debe verificar si los comandos provistos tienen sentido. Algunos comandos se cancelan entre sí. (escriba fielda, elimine fielda). Creo que desea informar esto al cliente para limitar el tiempo de depuración de su lado.

Pero si tienes tiempo, esta es una solución realmente elegante. Aún debe validar los campos, por supuesto. Puede combinar esto con el método PATCH para permanecer en el modelo REST. Pero creo que POST sería aceptable aquí.

Yendo mal

Si decide ir con la opción PUT, que es algo arriesgado. Entonces al menos no debes descartar el error. El usuario tiene una cierta expectativa (los datos se actualizarán) y si rompe esto, le dará a algunos desarrolladores un mal momento.

Podrías elegir para volver a marcar: 409 Conflict o 403 Forbidden Depende de cómo veas el proceso de actualización. Si lo ve como un conjunto de reglas (centradas en el sistema), el conflicto será mejor. Algo así, estos campos no son actualizables. (En conflicto con las reglas). Si lo ve como un problema de autorización (centrado en el usuario), debe regresar prohibido. Con: no tiene autorización para cambiar estos campos.

Todavía debe obligar a los usuarios a enviar todos los campos modificables.

Una opción razonable para hacer cumplir esto es establecerlo en un subrecurso, que solo ofrece los datos modificables.

Opinión personal

Personalmente, iría (si no tiene que trabajar con navegadores) para el modelo PATCH simple y luego lo extendería con un procesador de parches JSON. Esto se puede hacer diferenciando en los tipos mime: El tipo mime de parche json:

aplicación / parche json

Y json: application / json-patch

facilita la implementación en dos fases.

Edgar Klerks
fuente
3
Su ejemplo de idempotencia no tiene sentido. O cambias la descripción o no. De cualquier manera, obtendrás el mismo resultado, siempre.
Robert Harvey
1
Tienes razón, creo que es hora de ir a la cama. No puedo editarlo Es más un ejemplo sobre la racionalidad de enviar todos los datos en una solicitud PUT. Gracias por la anotación.
Edgar Klerks
Sé que esto fue hace 3 años ... pero ¿sabe dónde en el RFC puedo encontrar más información sobre "PUT tendría que entregar el objeto completo al recurso"? He visto esto mencionado en otra parte, pero me gustaría ver cómo se define en la especificación.
CSharper
Creo que lo encontré? tools.ietf.org/html/rfc5789#page-3
CSharper