Fins ara Escena Viva només lliura: catàleg, sessions, HTML, CSS. Tot són GET. Falta el que converteix un web en una plataforma: rebre. Que algú pugui comprar una entrada.
I aquí apareix l'asimetria del mòdul. Llegir req.method, req.url o req.headers és immediat, perquè Node t'entrega aquestes dades ja analitzades. El cos, en canvi, no: quan el teu gestor comença a executar-se, el cos pot continuar viatjant per la xarxa. req és un stream de lectura, i llegir-lo és exactament el que vas aprendre al Mòdul 3, amb dues voltes de rosca noves: els trossos són Buffer i tallar-los malament trenca els caràcters, i qui els envia no és del teu bàndol, així que cal posar-los un límit.
En acabar tindràs src/servidor/cos.js i el primer POST d'Escena Viva: POST /comandes, amb validació, crida al domini i una resposta 201 amb la seva capçalera Location.
Contingut
reqés un stream: el cos arriba a trossos- Per què concatenar en una cadena trenca els caràcters
- La forma correcta:
Buffer.concatifor await...of - El límit de mida i el
413 - Temps límit i peticions avortades
- Analitzar segons el
Content-Type POST /comandes: validar i cridar el domini- Respondre
201ambLocation, o l'error que toqui - Idempotència: per què un
POSTrepetit duplica comandes
req és un stream: el cos arriba a trossos
req és un stream: el cos arriba a trossosNode invoca el teu gestor tan bon punt ha acabat de llegir les capçaleres. El cos arriba després, a trossos, a través dels esdeveniments del stream:
function gestionarPeticio(req, res) {
const trossos = [];
// tros es un Buffer. La seva mida la decideix la xarxa, no tu.
req.on('data', (tros) => trossos.push(tros));
req.on('end', () => {
const cos = Buffer.concat(trossos).toString('utf8');
console.error(`[cos] ${cos.length} caracters en ${trossos.length} trossos`);
res.end('rebut\n');
});
// Sense aquest oient, una connexio tallada a mitja transmissio tomba el proces.
req.on('error', (error) => {
console.error('[cos] fallada en llegir:', error);
res.statusCode = 400;
res.end();
});
}Tres coses que cal interioritzar:
- La mida de cada tros no la controles. Depèn de l'MTU de la xarxa, del client i de les memòries intermèdies del sistema. Un cos petit pot arribar en un sol
data; un de gran, en centenars. Mai no donis per fet que un tros és una unitat amb sentit. - Si no llegeixes el cos, es queda allà. Un gestor que respon sense consumir
reqdeixa dades sense llegir al sòcol, i ambkeep-aliveaixò pot desincronitzar la petició següent d'aquella connexió. Si penses ignorar el cos, consumeix-lo (req.resume()) o destrueix la petició. reqemeterror. Una connexió que es talla a mitja transmissió produeix unerroral stream, i com totEventEmitter, si no l'escoltes et tomba el procés.
- Per què concatenar en una cadena trenca els caràcters
Aquest és l'error que més vegades s'ha escrit en la història de Node:
// MALAMENT. Funciona a les teves proves i falla en produccio amb accents.
let cos = '';
req.on('data', (tros) => { cos += tros; }); // <- tros.toString() implicit
req.on('end', () => { JSON.parse(cos); });El += converteix cada Buffer a cadena per separat, i aquí hi ha el parany que vam estudiar a la lliçó 03-06: en UTF-8 un caràcter pot ocupar diversos bytes. ó són dos bytes (0xC3 0xB3); un emoji, quatre. Si la frontera entre dos trossos cau al mig d'aquesta seqüència, cada meitat es descodifica pel seu compte i cap de les dues és un caràcter vàlid: el resultat és � (U+FFFD), i aquesta substitució és irreversible.
const complet = Buffer.from('{"sala":"Sala Bóveda"}', 'utf8');
// Simulem que la xarxa parteix el buffer just entre els dos bytes de la 'o'.
const tall = complet.indexOf(0xc3) + 1;
const trosA = complet.subarray(0, tall);
const trosB = complet.subarray(tall);
console.log(trosA.toString('utf8') + trosB.toString('utf8'));
// {"sala":"Sala B��veda"} <- dos caracters de reemplacament
console.log(Buffer.concat([trosA, trosB]).toString('utf8'));
// {"sala":"Sala Bóveda"} <- correcteEl pervers és quan falla: amb cossos petits gairebé mai hi ha més d'un tros, així que en desenvolupament funciona sempre. En producció, amb cossos més grans i una xarxa real, falla de tant en tant i només amb accents, emojis o caràcters CJK. Un JSON.parse que peta "aleatòriament" i un usuari anomenat "Bóveda" que de vegades es desa malament.
La regla és absoluta: acumula Buffer i descodifica una sola vegada, al final. L'alternativa, si necessites anar processant text a mesura que arriba, és el StringDecoder de la lliçó 03-06, que es guarda els bytes incomplets entre tros i tros.
- La forma correcta:
Buffer.concat i for await...of
Buffer.concat i for await...ofAmb async/await, req es recorre amb for await...of, perquè els streams de lectura són iterables asíncrons (lliçó 03-05). Queda molt més llegible que els esdeveniments solts:
async function llegirCosCru(req) {
const trossos = [];
for await (const tros of req) {
trossos.push(tros);
}
return Buffer.concat(trossos); // un unic Buffer amb tots els bytes
}Avantatges sobre la versió amb on('data'): els errors del stream es converteixen en una excepció que captura el teu try/catch, l'end és simplement el final del bucle, i el resultat encaixa amb la resta del codi asíncron. És la forma que farem servir.
Però tal com està, aquesta funció és una invitació que et tombin el servidor. Falta el de l'apartat següent.
- El límit de mida i el
413
413llegirCosCru acumula tot el que arribi a memòria. Si un client envia un cos de 5 GB, el teu procés intenta desar-lo a la RAM i mor per falta de memòria. No cal mala fe: n'hi ha prou amb un bucle mal escrit en un client. I amb mala fe, és un atac de denegació de servei que costa una línia de curl.
Un servidor sempre limita la mida del cos, i ho fa dues vegades:
- Comprovant
Content-Lengthabans de llegir res. És barat i rebutja a l'instant el que és òbviament enorme. - Comptant els bytes rebuts mentre arriben. Imprescindible, perquè el
Content-Lengthl'escriu el client: pot mentir, o no venir en absolut si la petició fa servirTransfer-Encoding: chunked.
// src/servidor/cos.js
// Lectura del cos d'una peticio amb limit de mida obligatori.
const LIMIT_BYTES = 64 * 1024; // 64 KB: de sobres per a una comanda en JSON
function errorDomini(codi, missatge) {
const error = new Error(missatge);
error.codi = codi;
return error;
}
async function llegirCos(req, limitBytes = LIMIT_BYTES) {
// 1. Filtre barat: el que el client DIU que enviara.
const anunciat = Number(req.headers['content-length']);
if (Number.isFinite(anunciat) && anunciat > limitBytes) {
throw errorDomini('COS_MASSA_GRAN',
`El cos declara ${anunciat} bytes i el limit es ${limitBytes}`);
}
// 2. Filtre real: comptar el que arriba de veritat.
const trossos = [];
let rebuts = 0;
for await (const tros of req) {
rebuts += tros.length;
if (rebuts > limitBytes) {
// Tallar: si no, el client continua enviant el que ja hem descartat.
req.destroy();
throw errorDomini('COS_MASSA_GRAN', `El cos supera els ${limitBytes} bytes`);
}
trossos.push(tros);
}
return Buffer.concat(trossos);
}El req.destroy() no és cap detall: sense ell, el client continua transmetent i tu continues rebent bytes que descartes, gastant amplada de banda i CPU. Destruir la petició tanca l'aixeta.
COS_MASSA_GRAN s'afegeix a la taula d'errors-http.js amb l'estat 413 Payload Too Large:
Sobre el valor del límit: 64 KB és generós per a una comanda en JSON i ridícul per pujar una imatge. La resposta no és "un límit gran per si de cas", sinó un límit diferent per ruta: 64 KB a l'API, i uns quants megabytes només allà on de debò es pugen fitxers.
- Temps límit i peticions avortades
Hi ha un atac més subtil que el cos enorme: el cos lentíssim. Un client anuncia 60 KB i els envia a raó d'un byte per minut. Mai no supera el límit de mida, però manté una connexió i un gestor ocupats durant hores. Amb uns quants centenars de connexions així, exhaureixes el servidor. És l'atac conegut com a slowloris.
La defensa és un temps límit:
// Si la peticio no es completa en 10 s, es talla.
req.setTimeout(10_000, () => {
console.error('[cos] peticio massa lenta, es talla');
req.destroy();
});El cas simètric és la petició avortada: l'usuari tanca la pestanya o perd la cobertura a mig enviament. Node ho assenyala al stream, i convé detectar-ho per no fer feina inútil:
// L'esdeveniment 'aborted' esta obsolet des de Node 16: es fa servir 'close'.
req.on('close', () => {
if (!req.readableEnded) console.error("[cos] el client ha avortat l'enviament");
});La distinció clau és req.readableEnded: si és true, el cos ha arribat complet i el close és el normal de després; si és false, la connexió s'ha tallat abans. Quan això passa, no responguis: no hi ha ningú escoltant, i qualsevol feina pesada que anessis a fer (desar al disc, cridar una altra API) és temps llençat.
- Analitzar segons el
Content-Type
Content-TypeUn cos són bytes; què signifiquen ho diu la capçalera Content-Type. Aquests són els tres formats que veuràs:
Content-Type |
Qui l'envia | Com s'analitza |
|---|---|---|
application/json |
API, fetch, aplicacions mòbils |
JSON.parse dins d'un try/catch |
application/x-www-form-urlencoded |
Un <form> HTML clàssic |
new URLSearchParams(text) |
multipart/form-data |
Un <form> amb <input type="file"> |
Amb una llibreria, mai a mà |
Afegim a src/servidor/cos.js:
// Torna el cos ja interpretat segons el seu Content-Type.
async function llegirCosInterpretat(req, limitBytes = LIMIT_BYTES) {
const brut = await llegirCos(req, limitBytes);
if (brut.length === 0) return {};
// El Content-Type pot portar parametres: 'application/json; charset=utf-8'.
const tipus = (req.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase();
const text = brut.toString('utf8'); // descodificacio UNICA, al final
if (tipus === 'application/json') {
try {
return JSON.parse(text);
} catch (error) {
// Missatge util: JSON.parse diu la posicio exacta de la fallada.
throw errorDomini('JSON_INVALID', `El cos no es JSON valid: ${error.message}`);
}
}
if (tipus === 'application/x-www-form-urlencoded') {
// URLSearchParams descodifica el percentatge i els '+' per nosaltres.
return Object.fromEntries(new URLSearchParams(text));
}
throw errorDomini('TIPUS_NO_ACCEPTAT', `Content-Type no admes: ${tipus || '(absent)'}`);
}
module.exports = { llegirCos, llegirCosInterpretat, LIMIT_BYTES };Detalls que importen. El Content-Type es talla pel ;, perquè gairebé sempre ve amb paràmetres. El JSON.parse va en un try/catch i el missatge s'aprofita: Unexpected token } in JSON at position 42 li diu al client exactament on ha de mirar, i això val molt més que un "petició invàlida". I Object.fromEntries sobre URLSearchParams perd les claus repetides (es queda amb l'última): si el teu formulari té caselles múltiples, fes servir getAll explícitament.
Sobre multipart/form-data: és un format amb separadors generats a l'atzar, parts amb les seves pròpies capçaleres, fitxers binaris que no han de passar per memòria i codificacions heretades. Analitzar-lo a mà és un error de criteri: es resol amb busboy o multer, que veurem al Mòdul 6. Saber que no s'ha de fer a mà forma part de l'ofici.
POST /comandes: validar i cridar el domini
POST /comandes: validar i cridar el dominiAmb les peces a punt, el primer POST d'Escena Viva. El cos esperat és mínim: {"sessioId": "ses-001-1", "quantitat": 2, "correu": "[email protected]"}.
La validació es fa a mà i abans de tocar el domini. Cada comprovació llança amb el seu error.codi, i el gestor central de 04-03 els converteix en l'estat que toca:
// src/servidor/rutes-comandes.js
const LIMIT_ENTRADES_PER_COMANDA = 6;
function validarComanda(cos) {
const { sessioId, quantitat, correu } = cos;
if (typeof sessioId !== 'string' || sessioId.trim() === '') {
throw errorDomini('PARAMETRE_INVALID', 'El camp "sessioId" es obligatori');
}
if (!Number.isInteger(quantitat) || quantitat < 1) {
throw errorDomini('QUANTITAT_INVALIDA', 'El camp "quantitat" ha de ser un enter positiu');
}
if (quantitat > LIMIT_ENTRADES_PER_COMANDA) {
// 422: s'enten perfectament, pero ho prohibeix una regla de negoci.
throw errorDomini('LIMIT_PER_COMANDA',
`Maxim ${LIMIT_ENTRADES_PER_COMANDA} entrades per comanda, se n'han demanat ${quantitat}`);
}
if (typeof correu !== 'string' || !correu.includes('@')) {
throw errorDomini('PARAMETRE_INVALID', 'El camp "correu" no es valid');
}
return { sessioId: sessioId.trim(), quantitat, correu: correu.trim().toLowerCase() };
}Quatre criteris darrere d'aquestes comprovacions:
Number.isInteger, noparseInt. AmbparseInt('2 entrades')obtindries2i donaries per bona una barbaritat. Si el cos és JSON,quantitatha d'arribar com a número; una cadena"2"és un client mal escrit i mereix un400.- Es distingeix
400de422.quantitat: 0és un400(el valor no té sentit);quantitat: 8és un422(el valor és vàlid i la regla de negoci el prohibeix). - Es valida abans de cridar el domini. Com més aviat es rebutja el que no és vàlid, menys estat a mitges cal desfer.
- Es normalitza al final. Retallar espais i passar el correu a minúscules evita que "[email protected]" i "[email protected]" siguin dos clients diferents.
Amb les dades ja netes, el gestor només orquestra:
enrutador.post('/comandes', async (req, res) => {
const cos = await llegirCosInterpretat(req); // 413, 400 o 415
const { sessioId, quantitat, correu } = validarComanda(cos); // 400 o 422
const esdeveniments = await obtenirCataleg();
const esdeveniment = esdeveniments.find((candidat) => candidat.cercarSessio(sessioId));
if (!esdeveniment) {
throw errorDomini('SESSIO_NO_TROBADA', `No existeix la sessio ${sessioId}`); // 404
}
// El domini decideix: llanca AFORAMENT_INSUFICIENT (409) si no hi caben.
esdeveniment.reservar(sessioId, quantitat);
const gestor = new GestorDeVendes(esdeveniments);
const entrades = gestor.registrarVenda(sessioId, quantitat, `ped-${Date.now()}`);
respondreJson(res, 201, { /* ...apartat 8... */ });
});Fixa't en el que no hi ha: ni un sol try/catch, ni una sola menció a un número d'estat HTTP. El gestor llança en el vocabulari del domini i el despatxador tradueix. Aquesta és la recompensa de les dues lliçons anteriors.
- Respondre
201 amb Location, o l'error que toqui
201 amb Location, o l'error que toquiUn POST que crea un recurs respon 201 Created amb la capçalera Location apuntant a on ha quedat:
const comandaId = `ped-${Date.now()}`;
respondreJson(res, 201, {
comandaId,
sessioId,
quantitat,
correu,
entrades: entrades.map((entrada) => entrada.codi), // EV-2026-000001, ...
importCentims: quantitat * sessio.preuCentims,
entradesLliures: esdeveniment.entradesLliures(sessioId)
}, { Location: `/comandes/${comandaId}` });El Location no és decoratiu: és el que permet a un client desar la URL de la comanda sense construir-la ell a base de suposicions. I l'import va en cèntims enters, la convenció del projecte des del Mòdul 1: 2 × 2500 = 5000, mai 50.00 en coma flotant.
Tots els camins d'error, en una taula:
| Situació | error.codi |
Estat |
|---|---|---|
| Cos més gran de 64 KB | COS_MASSA_GRAN |
413 |
Content-Type no admès |
TIPUS_NO_ACCEPTAT |
415 |
| JSON mal format | JSON_INVALID |
400 |
Falta sessioId o el correu no és vàlid |
PARAMETRE_INVALID |
400 |
quantitat no és un enter positiu |
QUANTITAT_INVALIDA |
400 |
| Més de 6 entrades | LIMIT_PER_COMANDA |
422 |
| La sessió no existeix | SESSIO_NO_TROBADA |
404 |
| No queden prou entrades | AFORAMENT_INSUFICIENT |
409 |
I la comprovació completa des de la terminal:
CT='Content-Type: application/json'
# 201 Created, amb Location i els codis d'entrada
curl -i -X POST http://localhost:3000/comandes -H "$CT" \
-d '{"sessioId":"ses-001-1","quantitat":2,"correu":"[email protected]"}'
curl -s -X POST http://localhost:3000/comandes -H "$CT" \
-d '{"sessioId":"ses-001-1","quantitat":8,"correu":"[email protected]"}' # 422
curl -s -X POST http://localhost:3000/comandes -H "$CT" \
-d '{"sessioId":"ses-999-9","quantitat":1,"correu":"[email protected]"}' # 404
curl -s -X POST http://localhost:3000/comandes -H "$CT" \
-d '{"sessioId":"ses-002-1","quantitat":5,"correu":"[email protected]"}' # 409: en queden 2
curl -s -X POST http://localhost:3000/comandes -H "$CT" -d '{no es json}' # 400La sessió ses-002-1 té aforament 120 i 118 de venudes: demanar-ne 5 torna 409 amb el missatge del domini, que diu quantes en queden. Aquest detall —un error que explica l'estat real— és el que converteix una API en una cosa usable.
- Idempotència: per què un
POST repetit duplica comandes
POST repetit duplica comandesExecuta dues vegades el curl que crea la comanda de 2 entrades. N'obtindràs dues comandes diferents i quatre entrades venudes. És correcte segons la norma: POST no és idempotent per definició, i repetir-lo significa crear un altre recurs.
| Mètode | És idempotent? | Repetir-lo significa |
|---|---|---|
GET, HEAD |
Sí | Llegir un altre cop; no canvia res |
PUT, DELETE |
Sí | Deixar el recurs en el mateix estat final |
POST |
No | Crear un altre recurs |
El problema és que a la vida real les peticions es repeteixen sense voler: l'usuari prem dues vegades "Comprar", la xarxa es talla després que el servidor processés la comanda però abans que arribés la resposta, o un client mòbil reintenta automàticament. En una plataforma d'entrades això significa cobrar dues vegades.
Les tres defenses, en ordre de solidesa:
- Al client: desactivar el botó després del primer clic. Necessari, insuficient: no protegeix d'un reintent de xarxa.
- Clau d'idempotència: el client genera un identificador únic per intent i l'envia en una capçalera
Idempotency-Key. El servidor la desa amb la resposta; si arriba un altre cop la mateixa clau, torna la resposta desada sense tornar a executar res. És el que fan les passarel·les de pagament. - Transaccions a la base de dades: reservar aforament i crear la comanda en una operació atòmica, amb una restricció d'unicitat que impedeixi el duplicat.
El nostre servidor avui no pot fer bé ni la 2 ni la 3, i convé ser honestos sobre per què: l'estat viu en memòria i en un JSON de disc, sense transaccions ni panys. És més: dues peticions simultànies poden llegir el mateix aforament lliure i vendre les mateixes dues butaques, una condició de carrera clàssica. Ho resoldrem de veritat a la lliçó 07-06, amb transaccions. Fins llavors, tingues-ho present com el que és: una limitació coneguda, no un descuit.
Errors Comuns i Consells
- Acumular el cos en una cadena amb
+=. Trenca els caràcters multibyte de manera intermitent.Buffer.concati descodificar al final. - Llegir el cos sense límit de mida. Un client t'exhaureix la memòria amb una línia de
curl. ComprovaContent-Lengthi compta els bytes. - Refiar-se només del
Content-Length. L'escriu el client i ambchunkedni tan sols existeix. - No destruir la petició en superar el límit. Continues rebent el que ja has decidit descartar.
JSON.parsesensetry/catch. Un cos mal format et dóna un500en lloc d'un400, i una traça al registre per cada client maldestre.- Comparar el
Content-Typeamb===. Gairebé sempre porta; charset=utf-8. Talla'l pel;. - Respondre
200a una creació, o analitzarmultipart/form-dataa mà. UnPOSTque crea torna201ambLocation; per a multipart,busboyomulter. - Consell: valida abans de tocar el domini i normalitza al final (retallar, minúscules). És més barat rebutjar que desfer.
- Consell: als missatges d'error, inclou la dada que falla i el valor esperat.
"Maxim 6 entrades per comanda, se n'han demanat 8"val infinitament més que"Peticio invalida".
Exercicis
Exercici 1: demostrar el trencament multibyte
Escriu src/laboratori/trencar-utf8.js que construeixi el Buffer de '{"sala":"Sala Bóveda","esdeveniment":"Concierto de Otoño"}', el parteixi en trossos de 7 bytes, i compari dues reconstruccions: la de concatenar cadenes i la de Buffer.concat. Mostra totes dues, compta els caràcters � de cadascuna i intenta un JSON.parse en les dues. Repeteix-ho amb trossos de 5 i de 13 bytes.
Exercici 2: el límit de mida en acció
Genera un fitxer de 100 KB (node -e "process.stdout.write('x'.repeat(102400))" > /tmp/gran.txt) i envia'l amb curl -X POST --data-binary @/tmp/gran.txt. Comprova que la resposta és 413. Després modifica llegirCos perquè registri a stderr quants bytes s'han arribat a rebre abans de tallar, i explica per què aquest número gairebé mai no coincideix exactament amb el límit.
Exercici 3: formularis i JSON a la mateixa ruta
Fes que POST /comandes accepti també application/x-www-form-urlencoded, tenint en compte que en un formulari tots els valors arriben com a cadena i que quantitat s'ha de convertir a número abans de validar-la. Comprova-ho amb curl -d 'sessioId=ses-003-1&quantitat=3&[email protected]' (sense -H, perquè curl ja posa aquest Content-Type) i assegura't que quantitat=tres continua tornant 400.
Solucions
Solució 1. Amb trossos de 7 bytes el tall cau dins de la ó de "Bóveda" i de la ñ d'"Otoño":
const original = '{"sala":"Sala Bóveda","esdeveniment":"Concierto de Otoño"}';
const complet = Buffer.from(original, 'utf8');
for (const mida of [5, 7, 13]) {
const trossos = [];
for (let i = 0; i < complet.length; i += mida) trossos.push(complet.subarray(i, i + mida));
const perCadena = trossos.reduce((text, tros) => text + tros, '');
const perBuffer = Buffer.concat(trossos).toString('utf8');
const trencats = [...perCadena].filter((caracter) => caracter === '�').length;
console.log(`${mida} bytes -> ${trencats} caracters trencats | correcte: ${perBuffer === original}`);
}Buffer.concat torna l'original en els tres casos; la concatenació de cadenes trenca caràcters sempre que un tall caigui dins d'una seqüència multibyte, i el seu JSON.parse falla perquè � no és un caràcter vàlid dins d'una cadena JSON... llevat que caigui dins d'un valor, cas en què analitza sense error i desa dades corruptes, que és l'escenari pitjor de tots.
Solució 2. El número gairebé mai no coincideix amb el límit perquè la comprovació es fa després de rebre un tros sencer: si el límit són 65.536 bytes i el tros que el creua en porta 16 KB, t'hauràs passat en uns quants milers. És el comportament correcte —no pots rebutjar mig tros—, i per això el límit es tria amb marge i no al mil·límetre.
if (rebuts > limitBytes) {
console.error(`[cos] tallat despres de ${rebuts} bytes (limit ${limitBytes})`);
req.destroy();
throw errorDomini('COS_MASSA_GRAN', `El cos supera els ${limitBytes} bytes`);
}Solució 3. La conversió ha de ser estricta: Number('tres') és NaN, i Number.isInteger(NaN) és false, així que la validació existent ja ho rebutja sense tocar-la.
function normalitzarNombres(cos, esFormulari) {
if (!esFormulari) return cos;
// En un formulari tot arriba com a cadena: es converteix el que ha de ser numero.
return { ...cos, quantitat: cos.quantitat === undefined ? undefined : Number(cos.quantitat) };
}Fer servir Number i no parseInt és deliberat: parseInt('3 entrades') torna 3 i acceptaria brossa, mentre que Number('3 entrades') torna NaN i la rebutja. La sessió ses-003-1 té aforament suficient, així que la comanda de 3 entrades torna 201.
Conclusió
Escena Viva ja ven entrades. I el camí ha deixat clar que rebre és força més delicat que lliurar. req és un stream de lectura els trossos del qual són Buffer de mida impredictible, així que la primera regla és acumular Buffer i descodificar una sola vegada amb Buffer.concat: concatenar en una cadena parteix els caràcters multibyte i produeix � irrecuperables de manera intermitent, justament el tipus de fallada que no apareix en desenvolupament. Amb for await...of el codi queda a més llegible i amb els errors integrats al try/catch.
La segona regla és que qui envia el cos no és del teu bàndol: src/servidor/cos.js comprova Content-Length com a filtre barat i compta els bytes rebuts com a filtre real, destrueix la petició quan es passa i llança COS_MASSA_GRAN → 413. A això s'hi sumen req.setTimeout contra el cos lentíssim i la detecció d'avortaments amb close més readableEnded, per no treballar per ningú. L'anàlisi es decideix pel Content-Type tallat pel ;: JSON.parse dins de try/catch amb el missatge aprofitat, URLSearchParams per als formularis, i multipart/form-data delegat a una llibreria perquè fer-ho a mà és un error de criteri.
I tens el primer POST real: validació a mà abans de tocar el domini —Number.isInteger en comptes de parseInt, 400 per al que no té sentit i 422 per al que la regla de negoci prohibeix, amb el límit de 6 entrades—, crida a reservar i a GestorDeVendes, i resposta 201 amb Location i import en cèntims. Tot això sense un sol try/catch al gestor: llança en el vocabulari del domini i el despatxador de 04-03 tradueix. Saps també el que encara no està resolt: un POST repetit duplica la comanda, i dos de simultanis poden vendre la mateixa butaca. La solució té nom —claus d'idempotència i transaccions— i data: la lliçó 07-06.
Ens queda girar el paper. Fins ara Node ha estat sempre el servidor; a la lliçó següent, Consumint APIs Externes des de Node.js, serà el client: fetch global i http.request per sota, l'error clàssic que fetch no rebutja davant d'un 404 ni d'un 500, temps límit amb AbortController, reintents amb espera exponencial reutilitzant reintentar del Mòdul 2, i un servei real d'Escena Viva —src/serveis/canvi-divises.js— amb memòria cau en memòria i degradació elegant quan el proveïdor extern falla.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
