Cos'e HTTP 428 Precondition Required
Il codice HTTP 428 Precondition Required, definito dalla RFC 6585, indica che il server origin richiede che la richiesta sia condizionale. In pratica, il server rifiuta di processare PUT/DELETE/PATCH che non includono header come If-Match o If-Unmodified-Since, per prevenire il problema del lost update.
Il problema del lost update
Senza precondizioni obbligatorie, due client possono sovrascriversi a vicenda:
- Alice fa GET /article/1 -> testo "v1", ETag "abc".
- Bob fa GET /article/1 -> testo "v1", ETag "abc".
- Alice fa PUT con nuovo testo. ETag diventa "def". Alice non e a conoscenza di Bob.
- Bob fa PUT con il suo testo. ETag diventa "ghi". Le modifiche di Alice sono perse.
Il 428 evita questo scenario: il server impone che ogni PUT includa If-Match: "etag-corrente". Bob avrebbe inviato If-Match: "abc", ricevendo 412 Precondition Failed (perché ora e "def"), e sarebbe stato costretto a rileggere prima di scrivere.
Esempio
PUT /api/documents/42 HTTP/1.1
Content-Type: application/json
{"title": "Nuovo titolo"}
HTTP/1.1 428 Precondition Required
Content-Type: application/json
{
"error": "If-Match header is required for write operations",
"current_etag": "doc42-v18",
"hint": "Retry with: If-Match: \"doc42-v18\""
}
Il client deve includere If-Match e ritentare:
PUT /api/documents/42 HTTP/1.1
Content-Type: application/json
If-Match: "doc42-v18"
{"title": "Nuovo titolo"}
HTTP/1.1 200 OK
ETag: "doc42-v19"
428 vs 412
- 428 Precondition Required: il server pretende una precondizione ma il client non l'ha inviata.
- 412 Precondition Failed: il client ha inviato una precondizione che non e soddisfatta.
Sequenza tipica: prima PUT senza If-Match -> 428; client aggiunge If-Match -> se ETag corrente: 200, altrimenti 412.
Quando usare il 428
- API che gestiscono risorse mutevoli su cui collaborano più client.
- Sistemi di document management dove il rischio di sovrascritture e alto.
- API finanziarie/sanitarie che richiedono garanzie di concorrenza.
- Endpoint critici per business logic (ordini, transazioni).
Implementazione
def require_if_match(handler):
def wrapper(request, *args, **kwargs):
if request.method in ['PUT', 'PATCH', 'DELETE']:
if 'If-Match' not in request.headers:
return Response(
status=428,
json={
"error": "If-Match required",
"current_etag": compute_etag(get_resource(...))
}
)
return handler(request, *args, **kwargs)
return wrapper
Optimistic concurrency control
Il 428 e l'enabler del pattern OCC (optimistic concurrency control) obbligatorio: rispetto al pessimistic locking (es. WebDAV 423), permette più throughput perché non blocca preventivamente. Solo al momento della scrittura si verifica la consistenza.
HATEOAS e 428
API REST mature spesso restituiscono ETag in ogni risposta GET e indicano nella documentazione che le scritture richiedono If-Match. Il 428 e usato come safety net per client mal scritti.
Configurazione granulare
Non tutti gli endpoint richiedono 428:
- POST di creazione (no risorsa precedente): inutile If-Match.
- DELETE di risorse versionate: potenzialmente utile (cancella solo se non e cambiata dal mio ultimo read).
- PATCH semantiche (add/remove array elements): puo essere meglio gestire diff lato server senza 428.
Compatibilità client
Client mal preparati al 428 mostrano errori generici. Per migliorare adoption:
- Includi un body JSON con istruzioni chiare.
- Restituisci l'ETag corrente così il client puo retry immediato.
- Documenta nelle API docs che le scritture sono condizionali.
Pattern di retry
async function updateWithRetry(url, data, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const current = await fetch(url);
const etag = current.headers.get('ETag');
const res = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'If-Match': etag
},
body: JSON.stringify(data)
});
if (res.ok) return res;
if (res.status === 412) continue; // ETag changed, retry
throw new Error(`Failed: ${res.status}`);
}
throw new Error('Max retries exceeded');
}
428 e cache
Le risposte 428 NON sono cacheable. Inoltre, il fatto che un'endpoint richieda 428 dovrebbe essere stabile (non variare a runtime), altrimenti i client memoizzano il comportamento "errato".
Best practice
- Applica 428 solo dove necessario: aggiunge complessita al client.
- Sempre fornire ETag in GET (altrimenti il client non sa cosa mettere in If-Match).
- Documenta in OpenAPI/Swagger quali endpoint richiedono If-Match.
- Testa il flow end-to-end: GET -> PUT con If-Match -> verifica 200, e con If-Match obsoleto -> verifica 412.
OCC vs pessimistic locking
Optimistic Concurrency Control (OCC) scala meglio del pessimistic locking in sistemi distribuiti: non blocca preventivamente, verifica al commit. Trade-off: in caso di high contention, retry rate aumenta. Per pochi conflitti, OCC e vincente; per moltissimi, pessimistic puo essere migliore.
Conditional GET vs Conditional Write
Mentre Conditional GET (con If-None-Match) ottimizza caching senza vincoli forti, Conditional Write (con If-Match) e su un'altro piano: enforce di safety. Server con 428 forza il pattern, evitando che client distratti dimentichino di proteggere le scritture.
UI patterns
Un'app web che opera su risorse condivise dovrebbe:
- Fetchare risorsa appena prima dell'edit (per ETag fresco).
- Mostrare warning se passa tempo tra fetch e save.
- Gestire elegantemente 412 mostrando diff con la versione corrente.
API design tradeoffs
Imporre 428 ovunque appesantisce client. Strategia: applicalo a endpoint critici (modifiche destructive su risorse condivise), lascialo opzionale altrove. Documenta chiaramente in OpenAPI.
428 e mobile sync
App mobile con sync offline-first sono il caso d'uso ideale di 428: utente modifica documenti offline, alla riconnessione la sync conditional rileva conflitti (412/428) e li risolve secondo policy applicativa.
Hai bisogno di aiuto?
Se il tuo sito mostra errori HTTP, il team di G Tech Group puo aiutarti. Contattaci tramite il modulo di contatto.