Per chi: chiunque chiami AgileV da codice proprio — e in particolare chi il client se lo scrive in casa, che a quanto pare siamo quasi tutti: l'harness dei box Agile vieta di eseguire codice scaricato da fuori. Da dove nasce: il 28/9 trpg, cercando una cosa che avevamo segnalato noi, ha trovato due difetti nel proprio client. Non sono difetti loro: sono difetti di classe, e li abbiamo trovati anche nei tre client di riferimento che diamo noi. Questa pagina è quello che abbiamo imparato, con la misura sotto; se il vostro client lo avete scritto voi, controllate questi tre punti prima di aspettare che si vedano da soli.
Il loro client leggeva detail e error dalla nostra risposta e li infilava in una stringa.
Da noi sono strutture, non stringhe: così ogni errore di AgileV diventava da loro la parola
«Array» — letteralmente Voyage API error: Array. E non era un log: quel messaggio lo legge
l'utente finale quando l'indicizzazione di un documento fallisce. In pratica, da quando sono passati a
noi, KEY_MISSING, KEY_INVALID e RATE_LIMIT non arrivavano mai a
chi guarda: se domani la loro chiave scadesse, il cliente leggerebbe «Array».
Il secondo difetto è peggiore perché non fa rumore: una nostra risposta 200 di forma inattesa passava
per successo con zero vettori, e chi chiamava riceveva NULL in silenzio.
error.code, che è una strutturaTutti gli errori di AgileV tranne uno hanno questo corpo, e il code è stabile: è l'unica cosa su cui
vale la pena ramificare (lo stato HTTP vi dice la famiglia, il code vi dice cosa fare).
{"error": {"code": "KEY_REVOKED", "message": "chiave revocata"}}
error e detail sono oggetti: in PHP json_decode($t, true) ve li
dà come array, e concatenarli in una stringa produce «Array» più un avviso. In Python, str(e["error"])
produce il repr di un dizionario. Costruite il messaggio da mostrare voi, con tre pezzi
soli: stato HTTP, error.code, error.message.
Nota storica: alcune versioni annidavano lo stesso oggetto sotto detail
({"detail":{"error":{…}}}). Un client robusto legge error in cima oppure sotto
detail. Oggi arriva sempre in cima.
Dove sta la tabella: ogni code che potete ricevere, con cosa fare, sta
nella tabella degli errori. Guardatela una volta per i 403
che non parlano della vostra chiave ma del contratto sotto cui sta: li riceve ogni chiave
dello stesso contratto nello stesso istante, e sono gli unici in cui chiederci una chiave nuova
non è il rimedio. Qui non li ricopiamo: la tabella è una sola, così non può restare indietro.
422 è diverso: non ha code, e vi rimanda indietro i vostri testiIl 422 è l'errore che un client nuovo incontra per primo (un campo sbagliato nel corpo:
input su /embed invece di texts, documents mancante su
/v1/rerank). Arriva prima dell'autenticazione — corpo sbagliato e chiave mancante
danno 422, non 401 — non consuma nulla, non porta le intestazioni X-RateLimit-*,
ed è l'unica risposta che non ha la nostra forma: è il corpo di FastAPI.
{"detail": [{"type": "missing", "loc": ["body", "input"],
"msg": "Field required", "input": {"texts": ["…IL VOSTRO TESTO…"]}}]}
Due conseguenze. La prima: se ramificate solo su error.code, per il 422 non trovate niente e
il vostro messaggio diventa muto. Leggete detail[].loc e detail[].msg: vi dicono
quale campo è sbagliato, che è esattamente l'informazione che serve.
La seconda: detail[].input era l'eco di quello che avete inviato, e dal 17/10 alle 03:37:52Z non c'è più. Fino a quel giorno un lotto da 64 testi (89.600 caratteri) tornava come 89.909 byte di errore; trpg l'aveva misurato dal proprio box con 16 testi, 25.969 byte inviati e 26.085 byte di errore tornati. Adesso la risposta pesa 402 byte per un testo e per sessantaquattro, misurati al bordo, e dentro non c'è nessun pezzo del vostro corpo. Quindi ritiriamo la messa in guardia che questa riga vi dava — «non registrate i corpi d'errore per intero» — perché era un rischio nostro che scaricavamo su di voi: oggi registrare un corpo d'errore nostro per intero non vi fa finire nei log nemmeno un carattere dei vostri testi.
200 non vuol dire «i vettori sono tutti»Contate. data deve avere tanti elementi quanti i testi che avete inviato, e gli
index devono essere 0…n-1. Se ne arrivano meno e voi accoppiate i testi ai vettori con uno
zip (o un foreach parallelo), indicizzate il testo sbagliato col vettore sbagliato
e non ve ne accorgete: il guasto si vede mesi dopo, come qualità di ricerca che peggiora. Sul rerank vale la stessa
regola con un'avvertenza: results ha min(top_n, documents) elementi quando
top_n è positivo — meno di documents è lecito se avete chiesto
top_n, mai di più — e ogni index deve stare nell'intervallo dei documenti che avete
mandato. Un top_n zero o negativo non si applica: tornano tutti i
documenti e la risposta lo dice in ignored_values: ["top_n"], quindi non calcolate l'atteso con un
min nudo — min(-1, 4) fa -1, e un client che si fermava lì accusava il servizio
(attesi -1 elementi in results, ricevuti 3) per un valore sbagliato suo. Era il caso dei nostri
due client pubblicati, corretto il 7/10 insieme al servizio.
Questa l'ha trovata trpg in casa propria il 28/9, tirando il filo di un nostro avviso, ed è la più
cattiva delle quattro perché non produce nessun errore e nessun risultato. La loro ricerca scartava in
silenzio — un continue nudo, in due punti — ogni frammento la cui dimensione non combaciava con quella
della domanda. Conseguenza: un indice costruito con un modello e interrogato con un altro non dà un errore,
dà zero risultati, e l'assistente risponde senza fonti, plausibilmente e a vuoto. Misurato da loro in sola
lettura, con un servizio finto al posto nostro: domanda a 512 valori su indice a 1024, nessuna eccezione, 0 risultati;
indice vecchio dopo un cambio di modello, 0 risultati su 471 frammenti tutti scartati, senza una
traccia da nessuna parte. L'ingresso del danno, da loro, erano i pacchetti d'installazione col nome del modello
cablato e l'indirizzo del servizio assente: un'installazione presso un cliente poteva avere AgileV in
configurazione e costruire comunque l'indice sul fornitore storico a 512 valori, senza nessun segnale.
Cinque contromisure, tutte a costo zero tranne l'ultima. La (a) e la (e) valgono sull'indice che avete GIÀ: la (b), la (c) e la (d) proteggono quello che scrivete da domani, mentre la (a) si applica ai frammenti scritti mesi fa, perché la loro dimensione si misura e non serve che qualcuno l'abbia dichiarata. Ce l'ha fatto notare trpg mettendola in opera, e per loro vale più delle altre proprio per questo. La (e) è arrivata dopo, ed è il rimedio per chi la (d) non ce l'ha.
GET /health non
vuole la chiave, non consuma niente e dichiara "dim" (oggi 1024). Confrontatelo con la
dimensione del vostro indice e fermatevi subito se non combacia, invece di scoprirlo a indice costruito.
/health il campo model è il modello sottostante
(bge-m3), mentre /v1/models elenca il nome di listino
(agilev-m3), che è anche quello che vi torna da /v1/embeddings. Sono
due vocabolari e i due valori non sono confrontabili: chi legge questa riga come «confrontate
model e dim» e confronta il nome si trova un disallineamento a ogni avvio, con il
servizio perfettamente sano. La controprova, misurata da noi: /v1/embeddings dice
agilev-m3, /embed dice bge-m3, e i due vettori sono identici bit per
bit. Il nome si confronta solo dentro la stessa rotta — ed è la (b).
output_dtype binary o
ubinary un vettore da 1024 arriva come 128 valori (1024 bit impacchettati), e con
encoding_format: base64 arriva come una stringa: len(vettore) == dim regge solo su
float. Misurato: 7 combinazioni su 10 farebbero scattare la guardia a torto — e una guardia che scarta a
torto, in silenzio, è esattamente il danno di questa regola.model della RISPOSTA, non quello della richiesta: ogni risposta dichiara
il modello usato (/v1/embeddings → agilev-m3, /v1/rerank →
agilev-rerank, /embed → bge-m3). Quello che avete spedito non è una garanzia:
il campo oggi non è validato e /embed non lo guarda affatto. Il confronto regge perché i due valori
vengono dallo stesso vocabolario: quello della rotta che avete chiamato. Non confrontate il nome di una rotta
con quello di un'altra.float16 0,99999998, che tiene perfino il verdetto esatto; int8 0,9998, un bit 0,75) e un testo ritoccato dopo pure
(0,9755). Fra i nostri e gli estranei ci sono 7 volte e in mezzo non c'è niente:
il verdetto è decidibile. Bastano 29 frammenti a campione per accorgersi con fiducia
95 % di una contaminazione del 10 %, e un indice da 471 frammenti si etichetta tutto in due minuti di
servizio. Soglie, tranelli (il più insidioso: rifare l'embedding del testo grezzo quando l'indicizzatore ne
preparava un altro) e protocollo da riscrivere in casa:
la provenienza dei vettori.Questa l'abbiamo trovata noi, il 29/9, contando quante forme può prendere una risposta di AgileV. Il 422
della Regola 2 non è un caso isolato: le risposte senza error.code erano quattro, il 5/10 ne abbiamo trovata una quinta e l'8/10 una sesta che non avevamo contato,
e non le scrive AgileV — le scrive il framework sotto di noi o il bordo davanti a noi, prima che il nostro codice veda
la richiesta.
| Quando | Cosa ricevete | Chi la scrive |
|---|---|---|
| corpo della richiesta non valido | 422 con {"error":{"code":"BODY_INVALID"},"detail":[{...}]} — chiusa il 17/10 alle 03:37:52Z (Regola 2) | AgileV |
metodo HTTP sbagliato (GET su /v1/embeddings) — è quella del 13/9 qui sotto | 405 con {"detail":"Method Not Allowed"} — chiusa il 9/10 alle 07:31Z: adesso è {"error":{"code":"METHOD_NOT_ALLOWED",…}}, ed è la stessa riga che fino a ieri prometteva quel codice «col prossimo caddy reload» | FastAPI, riscritta dal bordo |
| corpo oltre gli 8.000.000 byte del bordo — ci si arriva restando dentro ai limiti pubblicati, con testo multibyte: 128 testi da 32.000 caratteri CJK sono 24,5 MB | 413 con zero byte di corpo, nessun Content-Type, Connection: close — chiusa il 9/10 alle 07:31Z: adesso è {"error":{"code":"BODY_TOO_LARGE",…}}, ed è la stessa riga che fino a ieri prometteva quel codice «col prossimo caddy reload» | il bordo (Caddy) |
| il motore dietro il bordo non risponde: la riceve chiunque mandi una richiesta mentre il motore si riavvia, cioè a ogni rilascio | 502/503/504 con zero byte di corpo, nessun Content-Type — chiusa il 9/10 alle 07:31Z: adesso è {"error":{"code":"SERVICE_UNAVAILABLE",…}}, ed è la stessa riga che fino a ieri prometteva quel codice «col prossimo caddy reload» | il bordo (Caddy) |
chiusa il 5/10 alle 06:40Z (scritta l'1/10, in linea col reload del 5/10; in mezzo ne sono uscite altre 48, 5 a chi usa il servizio): era 404 con {"detail":"Not Found"}, adesso il bordo la riscrive in {"error":{"code":"NOT_FOUND",…}} | FastAPI, riscritta dal bordo | |
chiusa il 29/9, in linea dall'1/10 alle 02:00 di Roma: era 404 con not found in testo semplice, nemmeno JSON | il bordo (Caddy) |
Erano quattro; poi misurando in gabbia ne abbiamo trovate altre due, e oggi non ne resta aperta nessuna. L'ultima era il 422 del corpo sbagliato, e l'abbiamo chiusa noi il 17/10 alle 03:37:52Z: adesso ha error.code BODY_INVALID, e non vi rimanda più indietro i testi che ci avete inviato — era la ragione per cui questa pagina vi diceva di non registrare i corpi d'errore per intero. Le tre di prima — il 405, il 413 del corpo troppo grande (trovato il 5/10) e i tre 50x del motore che non risponde (trovati l'8/10) — sono chiuse dal 9/10 alle 07:31Z. In tutte
detail era una stringa (o il corpo non era nemmeno JSON, o non c'era corpo per niente), quindi cadevano tutte
nello stesso ramo del vostro lettore d'errore: quello che non sa cosa fare. Se lì mettete ?, chi vi chiama
riceve un allarme che non si può smistare né contare — è la Regola 1 vestita da caso limite.
Le due che si sono chiuse le ha chiuse il bordo, senza toccare l'app: il 29/9 i quattro punti in cui
ferma una richiesta da sé, e il 5/10 alle 06:40:01Z — scritto l'1/10, in linea col reload del 5/10 — il 404 che gli torna dal motore — /v1/* passa intero,
quindi /v1/embed o /v1/keys arrivavano a FastAPI e tornavano senza code
(misurato: 0 su 7 → 7 su 7 fra le richieste a tentoni sotto /v1/). Il 405 fino all'8/10 restava, e la ragione era questa: la rotta esiste, e dirle «rotta inesistente» sarebbe un consiglio falso. La ragione vale ancora — il 405 non riusa il corpo del 404 — ma escludeva una cura, non la cura: dal 9/10 alle 07:31:16Z ha un code suo, METHOD_NOT_ALLOWED, e il messaggio dice quale metodo vuole quella rotta, preso dall'Allow che il framework manda già.
E non sono ipotesi: il 13/9/2026 alle 01:26:32Z un cliente ha chiamato /v1/embeddings in
GET e ha ricevuto il 405; il 15/9 alle 12:43:55Z un altro ha ricevuto un
502 del bordo perché due secondi prima avevamo riavviato il servizio. Nessuno dei due ce l'ha segnalato:
l'abbiamo letto noi nel log del bordo, e dal loro lato era un errore senza nome.
Cosa fare. Non indovinare dal corpo: quei corpi non sono nostri e nessuno ha promesso cosa ci
metterà dentro. Dedurre il codice dallo stato HTTP, che è l'unica parte garantita — e sono cinque
comportamenti diversi, che con ? erano uno solo:
| Stato | Codice che ci mettiamo noi | Ritentare? |
|---|---|---|
404 | NOT_FOUND — percorso inesistente: controllate l'URL di base e la rotta | no: è l'indirizzo |
405 | BAD_METHOD — metodo sbagliato per questa rotta. Dal 9/10, alle 07:31Z, questa risposta ha la nostra forma, METHOD_NOT_ALLOWED, col metodo giusto dentro il messaggio: questa riga resta valida com'è scritta, e il vostro client non cambia di un carattere — è il senso della regola. Il messaggio nostro potete mostrarlo, perché quello sì è nostro | no: è il verbo |
3xx | REDIRECT — un rimando: quasi sempre una barra finale di troppo nell'URL | no: togliete la barra, oppure seguite il Location |
5xx senza forma | UPSTREAM_ERROR — il bordo non è arrivato al servizio, o il servizio è caduto senza forma. Dal 9/10, alle 07:31Z, il caso più comune — il motore che si riavvia — ha la nostra forma, SERVICE_UNAVAILABLE, e questa riga resta per gli altri | sì |
| qualunque altro | NO_CODE, con lo stato dentro al messaggio — sconosciuto sì, muto no | dipende dallo stato |
Il corollario, ed è la parte che si dimentica: il corpo di quelle risposte non va
messo nel messaggio. Una pagina d'errore di un proxy di mezzo può contenere qualunque cosa, e finirebbe nei vostri log
etichettata come un errore di AgileV. È la stessa regola dell'eco del 422 (Regola 2), applicata a corpi
che non sono nostri.
307 che non è un errore (1/10)Una rotta vera chiamata con una barra finale —
POST /v1/embeddings/ invece di POST /v1/embeddings — non risponde 200
e non risponde con un nostro errore: risponde 307, corpo vuoto, con
Location: verso la stessa rotta senza barra. Lo scrive il framework (redirect_slashes),
non il nostro codice, e il 307 conserva metodo e corpo: un client che segue i rimandi
non se ne accorge nemmeno. Un client che non li segue — e tanti li disattivano sulle POST,
ed è una scelta prudente, perché un rimando su una POST rispedisce il corpo — vede uno stato che non è
200 e non ha error.code. Non è una terza forma senza codice: non è un
errore, è la stessa risposta all'indirizzo giusto.
Quello che serve sapere, tutto misurato (bench/misura-la-barra-in-fondo.py):
seguirlo è sicuro, perché il Location è assoluto e in https
e il corpo del 307 è vuoto (0 byte: non torna indietro niente di quello che avete mandato);
non consuma (la stessa chiamata conta 1 richiesta senza barra, 1
seguendo il rimando, 0 fermandosi al 307: il rimando non si paga);
non porta le intestazioni X-RateLimit-*, quindi chi regola il ritmo su
X-RateLimit-Remaining su un 307 non legge niente;
e non si comporta uguale su tutte le rotte — GET /health/ e
POST /embed/ non arrivano nemmeno all'app e ricevono il 404 NOT_FOUND del bordo, che dice
«rotta inesistente» di una rotta che esiste. Dal vivo l'1/10, sulle otto rotte pubbliche:
5 rispondono 307, due il 404 del bordo.
Cosa fare: non scrivete la barra finale. I tre client che diamo noi non possono metterla da soli
(tagliano la barra dell'indirizzo di base) e dall'1/10 hanno il ramo 3xx della tabella qui sopra, che
restituisce REDIRECT con la cura invece di un codice «non so»: misurato
1 client su 3 → 3 su 3 che ne ricavano qualcosa di smistabile.
Non è un consiglio a occhio: è quello che abbiamo trovato addosso. bench/misura-errori-client.py
cattura i corpi d'errore veri del servizio (da una copia del codice caricata in memoria, senza mandare
una richiesta alla produzione), li rimanda da un server finto locale e registra cosa vede il chiamante
nei tre client di riferimento: il 28/9 12 scene x 3 client = 36 righe, dal 29/9
15 scene x 3 client = 45 righe (le tre aggiunte sono il 405, il 404
dell'app e il 404 in testo semplice del bordo: forme reali che nessuno aveva mai passato ai client).
Le ultime due oggi non arrivano più così — il bordo le riscrive — ma le scene restano:
un client deve sapere cosa fare anche della forma vecchia, perché un client vecchio parla con un bordo nuovo e
viceversa, e perché la Regola 5 (il codice si deduce dallo stato) è scritta proprio per non dipendere dal corpo.
| Cosa si misura | Prima (28/9) | Dopo (28/9) | Dopo (29/9) |
|---|---|---|---|
| Righe in cui il chiamante non vede un codice nostro | 15 su 36 | 3 su 36 (il 502 HTML del bordo) | 0 su 45 — con le tre scene nuove erano diventate 12 su 45 |
| Righe in cui il messaggio visibile contiene l'eco del testo inviato | 9 | 0 | 0 |
| Righe in cui tornano meno vettori dei testi, in silenzio | 3 | 0 | 0 |
I tre erano tutti da correggere, e nello stesso modo. Il 422 diventa il codice
BAD_BODY, col messaggio
corpo della richiesta non valido - body.input: Field required
(solo loc, msg e type: input non si legge mai). Un
data mutilato o assente diventa BAD_RESPONSE, col messaggio
attesi 2 elementi in data, ricevuti 1.
Dal 29/9 le quattro forme senza code escono con NOT_FOUND,
BAD_METHOD, UPSTREAM_ERROR e
NO_CODE, e il corpo non entra più nel messaggio.
Questi codici li mette il client, non il servizio: servono a voi per ramificare, e sono uguali nel
Python, nel PHP e nell'adattatore LangChain — parola per parola.
Le stesse 45 righe misurate catturando i corpi d'errore dal codice 1.1 che è in produzione oggi e dalla 1.2 danno lo stesso risultato: potete ricopiare i client adesso, non dovete aspettare un nostro deploy.
E quante volte è successo davvero, letto dal log del nostro bordo di settembre in sola lettura
(nessuna richiesta al servizio, nessun indirizzo nell'uscita): su 4.310 richieste servite,
20 risposte senza un codice del contratto sono arrivate a qualcuno che il servizio lo usa — 13 su una
rotta vera e 7 mentre cercava la rotta. Il conto onesto è costato due correzioni: il percorso non
basta a dire chi è (uno scanner che prova /v1/.env non è un cliente) e 131 di quei "clienti in
difficoltà" erano il nostro stesso banco di accettazione, che chiede apposta dei 404 per
verificarli. Da 151 a 20. Se non ve lo diciamo così, il numero sembra un allarme e non è.
17 test nuovi tengono il comportamento (fra questi uno che passa otto errori veri del servizio ai tre lettori e pretende che nessuno faccia uscire l'eco), e 11 mutazioni del codice dei client le fanno cadere tutte: se qualcuno rimette il lettore vecchio, il banco lo vede. Per la Regola 5, altri 9 test e 16 mutazioni (15 che devono cadere e una che, non cambiando niente, non deve cadere), provate su tutti e tre i client — tre copie della stessa regola sono tre posti dove può restare quella vecchia.
Questa non ce l'ha insegnata un errore: ce l'ha insegnata trpg misurando in casa propria il 29/9. Nel loro
contenitore, sotto Apache $_SERVER['LLM_API_URL'] è null mentre
getenv() e $_ENV portano il valore. Da riga di comando, sul nostro box,
è l'opposto: getenv() e $_SERVER portano il valore e
$_ENV è vuoto (variables_order predefinito, senza «E»). E
php-fpm con clear_env = yes, che è il valore predefinito, svuota l'ambiente del
processo: lì resta solo $_SERVER, riempito dalle righe env[...] del pool.
Tre stanze, tre risultati diversi per la stessa variabile: chi ne legge una sola sta scommettendo sulla
stanza, e quando perde la scommessa non c'è nessun errore — c'è una configurazione che funziona in ogni
prova da riga di comando e non c'è più dentro il server web.
Perché per AgileV è peggio che per gli altri: la variabile che sparisce è l'indirizzo. Se
avete puntato il client su un fronte privato — una WireGuard, un secondo box, lo smistatore — e
AGILEV_URL non arriva, il client non si ferma: prende il predefinito pubblico e i
vostri testi escono su Internet, senza una parola, con tutte le prove verdi.
Cosa abbiamo trovato in casa nostra, prima di scrivere questa regola. Censiti i sei esempi che
pubblichiamo: la chiave si fermava 6 volte su 6, l'indirizzo mai. Quattro celle
finivano in silenzio sull'indirizzo pubblico; due esempi, con AGILEV_KEY vuota (non assente:
vuota), spedivano il testo alla produzione pubblica con un Bearer vuoto; e in
ricerca_ibrida.php l'indirizzo si decideva dentro embed(), quindi non era controllabile
senza fare una richiesta, nemmeno da noi. Adesso è 0, 0 e 0
(bench/misura-indirizzo-che-sparisce.py, gruppo di controllo preso da git, 22 mutazioni su 22 viste).
Le quattro regole, se il client lo scrivete voi: (1) leggete l'ambiente da tutte le
sorgenti che la stanza può avere — in PHP getenv(), $_SERVER, $_ENV, in
quest'ordine, perché getenv() è l'ambiente del processo e non qualcosa che arriva con la richiesta;
(2) un valore presente ma vuoto è un errore che nomina la variabile, mai un ripiego verso
il predefinito; (3) dite sempre da dove arriva l'indirizzo, e della chiave dite la sorgente e mai il
valore; (4) se l'indirizzo deve essere privato, mettete una guardia che si ferma sul predefinito — e
scrivetela nel codice, non in una variabile d'ambiente: una guardia che vive nello stesso ambiente
sparisce insieme a ciò che deve proteggere, proprio nella stanza in cui serve.
La diagnosi in un comando, quando «da riga di comando funziona e sotto il server web no»:
php ricerca_ibrida.php --dove oppure python agilev_client.py --prova stampano indirizzo e
sorgente (argomento|getenv|server|env|ambiente|predefinito) e non stampano mai la chiave.
Quello che non affermiamo: la stanza di php-fpm non l'abbiamo misurata noi —
php-fpm non è installato sul nostro box, e quella riga è documentazione, non una nostra misura. Quella di Apache
è di trpg. La nostra è la riga di comando. Le tre righe della tabella dicono chi ha misurato cosa, apposta.
Se usate i nostri client (agilev_client.py, agilev_client.php,
agilev_langchain.py): ricopiateli. La correzione è dentro, non cambia nessuna firma e non cambia niente
sul percorso felice. Dentro c'è anche la (a), fatta come regge — una riga sola, all'avvio:
av.controlla_dim(1024) # Python $av->controllaDim(1024) # PHP emb.controlla_dim(1024) # LangChain (AgileVEmbeddings)
Legge dim da /health (senza chiave, senza consumo), si ferma con
DIM_MISMATCH se non è la dimensione del vostro indice, e non guarda il nome. Da quel
momento fa anche la (c) per voi: un vettore della misura sbagliata diventa un BAD_RESPONSE
(«attesi vettori da 1024 valori, ricevuti da 512») invece di entrare nell'indice, e due lunghezze diverse nello stesso
lotto sono un errore anche senza aver chiamato controlla_dim.
Se il client lo avete scritto voi: i punti sopra sono da guardare uno per uno. Il più rapido è la Regola 2 — cercate nel vostro codice se registrate il corpo di una risposta d'errore per intero. Il più importante è la Regola 4, perché è quella che sporca un indice senza far rumore; e leggetela tutta, compreso il paragrafo sul nome: la guardia scritta sul valore sbagliato è peggio che nessuna guardia, perché vi fa disattivare anche quella buona.
In entrambi i casi, una prova che dura un minuto: fate fallire una chiamata per davvero (una chiave
sbagliata, e un corpo con un campo mancante) e guardate con gli occhi il messaggio che arriva a chi lo legge —
un utente, un ticket, un log. Se contiene la parola «Array», un repr di dizionario, un ? o un
pezzo dei vostri testi, avete trovato il difetto. È quello che ha fatto trpg.
Niente di tutto questo cambia il vostro client, ed è il punto. Il 404 la nostra forma ce l'ha dal 5/10 e il 405 dal 9/10 alle 07:31:16Z: li ha chiusi il bordo, senza toccare l'app e senza che voi ricopiate niente, perché il codice lo deducete dallo stato. E il 422 non resta: la nostra forma gliel'abbiamo data noi, nell'app, il 17/10 alle 03:37:52Z — code stabile (BODY_INVALID) e senza
eco dei vostri testi — e non è stato un cambio di contratto: il contratto è rimasto 1.2,
e nessuna chiave si è dovuta riemettere. Dettagli in API.md, sezione «Errori». Questa pagina è autonoma, senza script e senza
risorse remote: potete leggerla e ricopiarla dove volete.