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

  1. req és un stream: el cos arriba a trossos
  2. Per què concatenar en una cadena trenca els caràcters
  3. La forma correcta: Buffer.concat i for await...of
  4. El límit de mida i el 413
  5. Temps límit i peticions avortades
  6. Analitzar segons el Content-Type
  7. POST /comandes: validar i cridar el domini
  8. Respondre 201 amb Location, o l'error que toqui
  9. Idempotència: per què un POST repetit duplica comandes

  1. req és un stream: el cos arriba a trossos

Node 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 req deixa dades sense llegir al sòcol, i amb keep-alive això pot desincronitzar la petició següent d'aquella connexió. Si penses ignorar el cos, consumeix-lo (req.resume()) o destrueix la petició.
  • req emet error. Una connexió que es talla a mitja transmissió produeix un error al stream, i com tot EventEmitter, si no l'escoltes et tomba el procés.

  1. 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"}       <- correcte

El 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.

  1. La forma correcta: Buffer.concat i for await...of

Amb 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.

  1. El límit de mida i el 413

llegirCosCru 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-Length abans 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-Length l'escriu el client: pot mentir, o no venir en absolut si la petició fa servir Transfer-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:

COS_MASSA_GRAN: 413,
TIPUS_NO_ACCEPTAT: 415

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.

  1. 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.

  1. Analitzar segons el Content-Type

Un 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.

  1. POST /comandes: validar i cridar el domini

Amb 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, no parseInt. Amb parseInt('2 entrades') obtindries 2 i donaries per bona una barbaritat. Si el cos és JSON, quantitat ha d'arribar com a número; una cadena "2" és un client mal escrit i mereix un 400.
  • Es distingeix 400 de 422. quantitat: 0 és un 400 (el valor no té sentit); quantitat: 8 és un 422 (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.

  1. Respondre 201 amb Location, o l'error que toqui

Un 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}'      # 400

La 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.

  1. Idempotència: per què un POST repetit duplica comandes

Executa 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:

  1. Al client: desactivar el botó després del primer clic. Necessari, insuficient: no protegeix d'un reintent de xarxa.
  2. 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.
  3. 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.concat i descodificar al final.
  • Llegir el cos sense límit de mida. Un client t'exhaureix la memòria amb una línia de curl. Comprova Content-Length i compta els bytes.
  • Refiar-se només del Content-Length. L'escriu el client i amb chunked ni tan sols existeix.
  • No destruir la petició en superar el límit. Continues rebent el que ja has decidit descartar.
  • JSON.parse sense try/catch. Un cos mal format et dóna un 500 en lloc d'un 400, i una traça al registre per cada client maldestre.
  • Comparar el Content-Type amb ===. Gairebé sempre porta ; charset=utf-8. Talla'l pel ;.
  • Respondre 200 a una creació, o analitzar multipart/form-data a mà. Un POST que crea torna 201 amb Location; per a multipart, busboy o multer.
  • 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

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats