El servidor de la lliçó anterior té un defecte que fa vergonya ensenyar: respon exactament el mateix a GET /esdeveniments, a POST /comandes i a DELETE /esborra-tot, sempre amb un 200 OK. No mira el mètode, no mira la ruta i no mira els paràmetres. És un servidor sord.
En aquesta lliçó li donem oïda i veu. Desmuntarem els dos objectes que rep el gestor —req i res—, aprendrem a extreure bé la informació de la petició, i construirem el vocabulari amb què Escena Viva contestarà durant la resta del curs: els codis d'estat, les capçaleres que importen i un mòdul d'ajudants, src/servidor/respostes.js, amb la peça que arribarà més lluny: la taula que tradueix els error.codi del nostre domini a estats HTTP. Aquesta taula sobreviurà a l'arribada d'Express al Mòdul 6.
Contingut
req: anatomia d'IncomingMessagereq.urlno és una URL: analitzar-la ambnew URLres: anatomia deServerResponseERR_HTTP_HEADERS_SENT: l'error més freqüent del mòdul- Els codis d'estat que farà servir el curs
- Les capçaleres que importen
src/servidor/respostes.js- D'
error.codide domini a estat HTTP - Redireccions i el mètode
HEAD
req: anatomia d'IncomingMessage
req: anatomia d'IncomingMessageEl primer argument del gestor és una instància d'http.IncomingMessage. I el primer que cal saber-ne és que és un stream de lectura: IncomingMessage estén stream.Readable, amb tot el que això implica des de la lliçó 03-04 (esdeveniments data i end, for await...of, contrapressió). Node et lliura l'objecte tan bon punt ha acabat de llegir les capçaleres; el cos pot continuar viatjant per la xarxa mentre el teu gestor ja s'està executant. Per això el cos s'ha de llegir amb paciència, i per això té la seva pròpia lliçó (04-05).
Les propietats que es fan servir cada dia:
console.error(req.method); // 'GET' (SEMPRE en majuscules)
console.error(req.url); // '/esdeveniments?sala=Teatro%20Almendra'
console.error(req.httpVersion); // '1.1'
console.error(req.headers['user-agent']); // 'curl/8.5.0'
console.error(req.headers.host); // 'localhost:3000'
console.error(req.socket.remoteAddress); // '::ffff:127.0.0.1'| Propietat | Tipus | Detall que sorprèn |
|---|---|---|
req.method |
Cadena | Sempre en majúscules; compara-la tal qual, sense toUpperCase() |
req.url |
Cadena | Només ruta i query. Mai no porta l'esquema ni el domini |
req.headers |
Objecte | Claus sempre en minúscules, les enviï com les enviï el client |
req.headers['set-cookie'] |
Array | És l'única capçalera que Node lliura sempre com a array |
req.socket.remoteAddress |
Cadena | La IP de l'últim salt, no necessàriament la de l'usuari |
req.rawHeaders |
Array | Parells plans amb les majúscules originals del client |
Dos advertiments que valen diners. El primer: les claus de req.headers estan normalitzades a minúscules, així que req.headers['Content-Type'] val undefined i req.headers['content-type'] funciona. L'errada és subtil perquè no dóna error, només un undefined silenciós. El segon: req.socket.remoteAddress darrere d'un proxy invers o d'un balancejador et tornarà la IP del proxy; la del client real arriba a X-Forwarded-For, una capçalera que només és fiable si controles el proxy que l'escriu. Hi tornarem en limitar peticions per IP a la lliçó 08-06.
req.url no és una URL: analitzar-la amb new URL
req.url no és una URL: analitzar-la amb new URLAquí és on gairebé tothom escriu el seu primer error. req.url és la URL de petició del protocol: una ruta amb la seva cadena de consulta, sense origen. I la temptació és tractar-la com a text:
// MALAMENT: no facis aixo
const [ruta, consulta] = req.url.split('?');
const sala = consulta.split('=')[1];Aquest codi falla en quatre escenaris reals:
- No descodifica. Amb
GET /esdeveniments?sala=Sala%20B%C3%B3vedaobtens la cadena literalSala%20B%C3%B3veda, que no casa amb'Sala Boveda'ni amb res. - Es trenca amb més d'un paràmetre.
?sala=X&ordre=datadeixasalavalentX&ordre=data. - Ignora els paràmetres repetits.
?categoria=jazz&categoria=humorés vàlid en HTTP i significa dos valors. - No contempla el fragment ni les barres redundants, ni normalitza res.
L'eina correcta és la classe URL, global des de Node 10 i estàndard del navegador. Necessita una URL absoluta, així que se li passa una base:
// La base nomes serveix per completar l'origen: el que importa es el pathname.
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
url.pathname; // '/esdeveniments' (ja descodificat en gran part)
url.searchParams.get('sala'); // 'Sala Boveda' <- descodificat
url.searchParams.get('inexistent'); // null
url.searchParams.getAll('categoria'); // ['jazz', 'humor']
url.searchParams.has('detall'); // true encara que vingui buit: '?detall='searchParams és un URLSearchParams: descodifica el percentatge i el +, admet claus repetides amb getAll, i és iterable. Comprovació pràctica amb el nostre catàleg:
curl -s 'http://localhost:3000/esdeveniments?sala=Sala%20B%C3%B3veda&categoria=humor&categoria=jazz'Un detall important sobre pathname: la classe URL no descodifica del tot el camí (per exemple %2F continua sent %2F, i amb raó: una barra codificada no s'ha de convertir en un separador de segments). Si un identificador pot portar caràcters codificats, aplica decodeURIComponent a cada segment per separat, mai a la ruta completa. És una precaució que reprendrem en extreure paràmetres de ruta a la lliçó següent.
Un patró útil per llegir paràmetres numèrics amb valor per defecte i validació:
function llegirEnterPositiu(searchParams, nom, perDefecte) {
const cru = searchParams.get(nom);
if (cru === null) return perDefecte;
const valor = Number(cru);
if (!Number.isInteger(valor) || valor < 1) {
const error = new Error(`El parametre "${nom}" ha de ser un enter positiu`);
error.codi = 'QUANTITAT_INVALIDA'; // Vocabulari de domini: sera un 400
throw error;
}
return valor;
}
res: anatomia de ServerResponse
res: anatomia de ServerResponseEl segon argument és un http.ServerResponse, i la seva naturalesa és simètrica a la de req: és un stream d'escriptura, ServerResponse estén stream.Writable. Això vol dir que res.write() torna false quan la memòria intermèdia s'omple, que emet drain, i que li pots connectar un pipeline —exactament el que farem a la lliçó 04-04 per servir fitxers.
Una resposta es construeix en tres passos, i l'ordre no és negociable:
res.statusCode = 200; // 1. Estat
res.setHeader('Content-Type', 'application/json; charset=utf-8'); // i capcaleres
res.write('{"esdeveniments":'); // 2. Cos,
res.write('3}'); // en 1 o N escriptures
res.end(); // 3. Fi (OBLIGATORI)Els mètodes, amb el seu matís:
| Mètode | Què fa | Quan fer-lo servir |
|---|---|---|
res.statusCode = 200 |
Fixa l'estat | Sempre; explícit encara que sigui 200 |
res.setHeader(n, v) |
Fixa una capçalera; es pot sobreescriure i consultar | L'habitual |
res.writeHead(200, obj) |
Estat + capçaleres i les envia ja | Drecera, per a una resposta curta |
res.write(dades) |
Escriu un tros; envia les capçaleres el primer cop | Respostes llargues o en streaming |
res.end([dades]) |
Escriu l'últim tros i tanca | Sempre, a totes les branques |
res.headersSent |
true si les capçaleres ja han sortit |
Abans d'intentar canviar-les |
writeHead i setHeader es poden combinar —writeHead guanya en cas de conflicte—, però barrejar estils confon. A Escena Viva farem servir setHeader per a l'acumulatiu i writeHead només quan la resposta es tanca immediatament.
ERR_HTTP_HEADERS_SENT: l'error més freqüent del mòdul
ERR_HTTP_HEADERS_SENT: l'error més freqüent del mòdulHTTP envia les capçaleres abans del cos, perquè així viatja el missatge. I un cop un byte ha sortit pel sòcol, no hi ha manera de recuperar-lo. Per això aquest codi peta:
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('Tot be');
res.setHeader('X-Tard', 'si');
// Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the clientA la pràctica, l'error gairebé mai és tan obvi: apareix quan una funció respon i l'execució continua fins a una altra que també respon.
async function gestionar(req, res) {
if (!req.url.startsWith('/esdeveniments')) {
respondreError(res, 404, 'Ruta no trobada');
// FALTA UN return: l'execucio segueix i a sota es respon un altre cop
}
const esdeveniments = await obtenirCataleg();
respondreJson(res, 200, esdeveniments); // <- peta si ja s'ha respost
}Les tres regles que ho eviten per sempre:
- Respondre és acabar. Tota crida a un ajudant de resposta va precedida de
return(return respondreError(...)), o dins d'unif/elsesense escapatòria. - Un gestor respon exactament una vegada. Si has de decidir entre diverses branques, calcula primer i respon al final.
- Als
catch, comprovares.headersSentabans d'intentar respondre amb un 500: si la fallada s'ha produït enmig d'un stream, l'única cosa que es pot fer ésres.end()ores.destroy(), perquè el client ja ha rebut un200que no podràs desmentir.
- Els codis d'estat que farà servir el curs
El primer dígit dóna la família: 2xx ha anat bé, 3xx mira en un altre lloc, 4xx t'has equivocat tu (el client), 5xx m'he equivocat jo (el servidor). Aquesta frontera entre 4xx i 5xx és la més important de totes: un 500 és un avís per al desenvolupador; un 4xx no.
| Codi | Nom | Significat exacte a Escena Viva |
|---|---|---|
| 200 | OK | GET /esdeveniments amb el catàleg, GET /esdeveniments/evt-001 amb l'esdeveniment |
| 201 | Created | Un POST /comandes ha creat la comanda; s'acompanya de Location |
| 204 | No Content | DELETE /comandes/ped-7 amb èxit. Sense cos, i sense Content-Type |
| 304 | Not Modified | El navegador ja té estils.css a la memòria cau i continua sent vàlid |
| 400 | Bad Request | JSON mal format o quantitat que no és un enter positiu |
| 401 | Unauthorized | Falta el testimoni o no és vàlid: no sé qui ets (Mòdul 8) |
| 403 | Forbidden | Sé qui ets, però org-boveda no pot editar un esdeveniment d'org-ribera |
| 404 | Not Found | evt-999 no existeix, o la ruta no està registrada |
| 405 | Method Not Allowed | DELETE /esdeveniments: la ruta existeix, el mètode no. Exigeix capçalera Allow |
| 409 | Conflict | Aforament insuficient: la petició és vàlida, l'estat actual la impedeix |
| 422 | Unprocessable Content | Sintaxi correcta i semàntica impossible: 8 entrades amb un màxim de 6 |
| 429 | Too Many Requests | Massa peticions des d'una IP (limitació de taxa, 08-06) |
| 500 | Internal Server Error | Una excepció que no vam saber classificar. És una fallada nostra |
| 503 | Service Unavailable | L'API de divises no respon i no podem degradar (04-06) |
Tres distincions que es pregunten a qualsevol entrevista i, més important, que s'equivoquen a qualsevol API mal feta:
- 400 davant de 422.
400és "no entenc el que m'envies" (JSON trencat, tipus equivocat).422és "t'entenc perfectament i la teva petició no té sentit" (demanes 8 entrades amb un límit de 6). Moltes API fan servir400per a totes dues coses; nosaltres distingirem, perquè el missatge d'error resultant és molt més útil. - 401 davant de 403.
401és "no sé qui ets" i sol acompanyar-se deWWW-Authenticate;403és "sé qui ets i no et deixo". Els noms oficials estan canviats respecte del que semblen i per això es confonen. - 404 davant de 409. Si la sessió no existeix,
404. Si existeix però no queden entrades,409: el recurs hi és, el que falla és l'estat.
- Les capçaleres que importen
Content-Type declara què estàs enviant. Per a text, amb charset sempre:
Sense charset=utf-8, un navegador antic pot interpretar Bóveda com Bóveda. En JSON l'estàndard ja obliga a UTF-8, però declarar-ho no costa res i en text/html i text/plain és imprescindible.
Content-Length davant de Transfer-Encoding: chunked. Si coneixes la mida exacta en bytes, declara-la: el client pot mostrar una barra de progrés i reutilitzar millor la connexió. Si no la coneixes —perquè estàs generant la resposta sobre la marxa o enviant un stream—, Node fa servir chunked automàticament, trossejant el cos amb marques de longitud.
const cos = JSON.stringify(dades, null, 2);
// CORRECTE: Buffer.byteLength compta BYTES, no caracters (llico 03-06).
res.setHeader('Content-Length', Buffer.byteLength(cos, 'utf8'));Fer servir cos.length aquí és un error clàssic i molt perjudicial: 'Bóveda' té 6 caràcters i 7 bytes en UTF-8. Anunciar un byte de menys fa que el client talli la resposta i falli el JSON.parse, amb un missatge incomprensible.
Cache-Control diu quant temps es pot guardar la resposta. En una API de dades vives, no-store; en un fitxer estàtic, segons o anys (04-04). Location indica on és el recurs: obligatòria en un 201 (on ha quedat allò creat) i en un 3xx (cap on anar).
src/servidor/respostes.js
src/servidor/respostes.jsEscriure statusCode, setHeader i end a cada branca és repetitiu i, sobretot, és on s'esmunyen les incoherències: una ruta que oblida el charset, una altra que torna l'error com a text pla. Centralitzem-ho.
// src/servidor/respostes.js
// Ajudants per construir respostes HTTP coherents a tot el servidor.
const JSON_UTF8 = 'application/json; charset=utf-8';
const TEXT_UTF8 = 'text/plain; charset=utf-8';
// Escriu un cos ja serialitzat amb el seu Content-Type i la longitud exacta.
function respondreCos(res, estat, tipus, cos, capcaleres = {}) {
if (res.headersSent) {
console.error("[respostes] s'ha intentat respondre dos cops; s'ignora");
return;
}
res.statusCode = estat;
res.setHeader('Content-Type', tipus);
res.setHeader('Content-Length', Buffer.byteLength(cos, 'utf8'));
for (const [nom, valor] of Object.entries(capcaleres)) res.setHeader(nom, valor);
res.end(cos);
}
function respondreJson(res, estat, dades, capcaleres = {}) {
respondreCos(res, estat, JSON_UTF8, JSON.stringify(dades, null, 2), capcaleres);
}
function respondreText(res, estat, text, capcaleres = {}) {
respondreCos(res, estat, TEXT_UTF8, text, capcaleres);
}
// 204 i 304 NO porten cos: enviar-lo viola la norma i confon els proxis.
function respondreSenseContingut(res, estat = 204, capcaleres = {}) {
if (res.headersSent) return;
res.statusCode = estat;
for (const [nom, valor] of Object.entries(capcaleres)) res.setHeader(nom, valor);
res.end();
}
// Format d'error unic per a tota l'API. Que el client pugui programar
// contra "codi" es mes util que llegir-li el "missatge" a un huma.
function respondreError(res, estat, missatge, codi = 'ERROR', extra = {}) {
respondreJson(res, estat, { error: { codi, missatge, estat, ...extra } });
}
function respondreRedireccio(res, estat, desti) {
respondreSenseContingut(res, estat, { Location: desti });
}
module.exports = { respondreJson, respondreText, respondreError, respondreSenseContingut, respondreRedireccio };Tres decisions deliberades. Tots els errors tenen la mateixa forma ({ error: { codi, missatge, estat } }): un client pot programar contra codi, que és estable, en comptes de contra missatge, que canviarà. Content-Length es calcula sempre amb Buffer.byteLength, mai amb length. I headersSent es comprova en un únic lloc, de manera que una resposta doble embruta el registre però no tomba el procés.
- D'
error.codi de domini a estat HTTP
error.codi de domini a estat HTTPArribem a la peça clau del mòdul. Des del Mòdul 2, el nostre domini llança errors amb un codi propi: SESSIO_NO_TROBADA, AFORAMENT_INSUFICIENT, QUANTITAT_INVALIDA. Aquest vocabulari és d'Escena Viva i no sap res d'HTTP: és exactament el que volem, perquè GestorDeVendes ha de poder fer-se servir des d'una CLI, des d'una tasca programada o des d'una API.
El pont entre els dos mons és una taula, i viu a la capa HTTP, no al domini:
// src/servidor/errors-http.js
// Tradueix el vocabulari d'errors del DOMINI al d'HTTP.
// El domini no coneix HTTP; aquesta taula es l'unica que els coneix tots dos.
const ESTAT_PER_CODI = {
// 400: peticio mal formada o mal tipada
QUANTITAT_INVALIDA: 400, PARAMETRE_INVALID: 400, JSON_INVALID: 400,
// 403: prohibit per politica
RUTA_NO_PERMESA: 403,
// 404: el recurs no existeix
ESDEVENIMENT_NO_TROBAT: 404, SESSIO_NO_TROBADA: 404,
COMANDA_NO_TROBADA: 404, RECURS_NO_TROBAT: 404,
// 409: peticio valida que xoca amb l'estat actual
AFORAMENT_INSUFICIENT: 409, ESTAT_INVALID: 409, COMANDA_JA_PAGADA: 409,
// 422: s'enten, pero ho prohibeixen les regles de negoci
LIMIT_PER_COMANDA: 422,
// 503: depenem d'alguna cosa externa que ara mateix no hi es
SERVEI_EXTERN_CAIGUT: 503
};
// Sense traduccio coneguda -> 500: es una fallada NOSTRA i cal veure-la.
function estatPerError(error) {
return ESTAT_PER_CODI[error?.codi] ?? 500;
}
// Un 5xx mai revela detalls interns al client, pero SI que es registra.
function cosPerError(error) {
const estat = estatPerError(error);
if (estat >= 500) {
console.error('[error] fallada no classificada:', error);
return { estat, codi: 'ERROR_INTERN', missatge: 'Error intern del servidor' };
}
return { estat, codi: error.codi, missatge: error.message };
}
module.exports = { estatPerError, cosPerError, ESTAT_PER_CODI };Per què això és el correcte i no un luxe d'arquitecte:
- El domini no es contamina.
Esdeveniment.reservarcontinua llançantAFORAMENT_INSUFICIENTsense saber que existeix el número 409, així que es pot continuar fent servir des desrc/cataleg.jsa la terminal. - La fallada per defecte és
500i és sorollosa. Uncodidesconegut significa que algú ha inventat un error nou i ha oblidat registrar-lo aquí: ens en volem assabentar, no que es converteixi en un400silenciós. - Els
5xxno filtren res. El missatge intern va astderrper a nosaltres; al client li arriba un text genèric. Una traça de pila a la resposta és un regal per a qui busca la teva versió de Node i les teves rutes de disc. - La taula és una de sola. Quan arribem a Express a la lliçó 06-07, el gestor d'errors canviarà de forma, però continuarà consultant aquest mateix fitxer.
Fer-lo servir és una línia:
try {
respondreJson(res, 200, await cercarEsdeveniment(id)); // llanca ESDEVENIMENT_NO_TROBAT
} catch (error) {
const { estat, codi, missatge } = cosPerError(error);
respondreError(res, estat, missatge, codi);
}A la lliçó següent aquest try/catch deixarà de repetir-se a cada ruta: pujarà una sola vegada al despatxador de l'enrutador.
- Redireccions i el mètode
HEAD
HEADUna redirecció és un 3xx amb la capçalera Location. Les quatre que importen:
| Codi | Nom | Mètode en reintentar | Quan |
|---|---|---|---|
| 301 | Moved Permanently | Pot canviar a GET |
La URL ha canviat per sempre; el navegador ho desa a la memòria cau |
| 302 | Found | Pot canviar a GET |
Trasllat temporal |
| 307 | Temporary Redirect | Es conserva | Temporal preservant un POST |
| 308 | Permanent Redirect | Es conserva | Permanent preservant un POST |
// /esdeveniment/evt-001 ha quedat obsolet: la ruta bona es /esdeveniments/evt-001
respondreRedireccio(res, 301, `/esdeveniments/${id}`);Compte amb el 301: els navegadors el guarden de manera agressiva i, si t'equivoques de destinació, els usuaris continuaran anant al lloc dolent encara que arreglis el servidor, perquè ni tan sols t'ho preguntaran. En cas de dubte, 302.
El mètode HEAD demana una resposta idèntica a la de GET però sense cos: serveix per consultar la mida o la data d'un recurs abans de descarregar-lo. La bona notícia és que Node el gestiona gairebé sol: si el mètode és HEAD, descarta el cos que escriguis i envia només les capçaleres. Tot i així, convé ser explícit per no generar feina inútil:
// Registrar HEAD al costat de GET: mateixes capcaleres, cos nomes si es GET.
const cos = JSON.stringify(dades, null, 2);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Content-Length', Buffer.byteLength(cos, 'utf8'));
res.end(req.method === 'HEAD' ? undefined : cos);Fixa't que el Content-Length es continua enviant encara que no hi hagi cos: és justament la dada que el client venia a buscar.
Errors Comuns i Consells
- Llegir
req.headers['Content-Type']amb majúscules. Tornaundefinedsense error. Les claus són sempre en minúscules. - Partir
req.urlambsplit('?')osplit('/'). Es trenca amb paràmetres codificats, repetits i amb més d'un. Fes servirnew URL(req.url, base). - Calcular
Content-Lengthambcos.length. Compta caràcters, no bytes; amb un accent o una ç, la resposta arriba tallada. Fes servirBuffer.byteLength. - Respondre sense
return. És la causa número u d'ERR_HTTP_HEADERS_SENT. - Enviar cos en un
204o un304. Està prohibit per la norma i alguns proxis s'hi ennueguen. - Tornar
200amb{ "error": ... }a dins, o la traça de pila en un500. L'estat és part de la resposta, i la traça va al registre, mai al client. - Consell: decideix des del principi un únic format d'error per a tota l'API i no el canviïs. Els teus clients t'ho agrairan més que qualsevol funcionalitat.
- Consell: als
catchque embolcallen streams, comprovares.headersSent; si ja han sortit, l'única cosa honesta ésres.destroy().
Exercicis
Exercici 1: filtre per sala i categoria
Amplia el gestor perquè GET /esdeveniments accepti ?sala= i ?categoria= (aquesta última repetible) i torni el catàleg filtrat, amb la forma { total, filtres, esdeveniments }. Valida que sala no estigui buida i respon 400 amb codi: 'PARAMETRE_INVALID' si ho està. Comprova-ho amb curl -s 'http://localhost:3000/esdeveniments?sala=Sala%20B%C3%B3veda'.
Exercici 2: ampliar la taula d'errors
Afegeix a src/servidor/errors-http.js els codis DADES_CORRUPTES (el JSON del catàleg està malament: és fallada nostra) i FORMAT_NO_ACCEPTAT (el client demana un format que no servim). Tria l'estat de cadascun i justifica'l. Després escriu src/servidor/provar-errors.js, un script que recorri ESTAT_PER_CODI i imprimeixi una taula codi | estat | familia, més el recompte per família.
Exercici 3: HEAD i Content-Length correctes
Fes que el gestor respongui a HEAD /esdeveniments amb les mateixes capçaleres que GET /esdeveniments però sense cos, i comprova amb curl -I que Content-Length coincideix exactament amb el nombre de bytes que torna curl -s ... | wc -c. Afegeix una sala amb accent al catàleg i verifica que continua quadrant.
Solucions
Solució 1. Tota la informació surt de searchParams, i la validació llança amb error.codi perquè la tradueixi la taula:
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
const sala = url.searchParams.get('sala');
const categories = url.searchParams.getAll('categoria');
if (sala !== null && sala.trim() === '') {
const error = new Error('El parametre "sala" no pot estar buit');
error.codi = 'PARAMETRE_INVALID';
throw error;
}
const esdeveniments = (await obtenirCataleg())
.filter((esdeveniment) => sala === null || esdeveniment.sala === sala)
.filter((esdeveniment) => categories.length === 0 || categories.includes(esdeveniment.categoria));
respondreJson(res, 200, { total: esdeveniments.length, filtres: { sala, categories }, esdeveniments });Amb ?sala=Sala%20B%C3%B3veda torna 1 esdeveniment (evt-002, Noche de Monologos). Si haguessis fet servir split('='), la comparació seria contra 'Sala%20B%C3%B3veda' i el resultat, zero esdeveniments: un filtre que "no troba res" i sembla un problema de dades.
Solució 2. DADES_CORRUPTES és un 500: el fitxer del catàleg és responsabilitat nostra i el client no hi pot fer res, així que a més volem que quedi registrat. FORMAT_NO_ACCEPTAT és un 406 Not Acceptable, l'estat específic per a "no puc produir cap dels formats que acceptes"; si prefereixes no ampliar el vocabulari del curs, 400 és defensable, però 406 és més precís. L'script es basa en el fet que la família és el primer dígit:
const { ESTAT_PER_CODI } = require('./errors-http.js');
const recompte = {};
for (const [codi, estat] of Object.entries(ESTAT_PER_CODI)) {
const familia = `${Math.floor(estat / 100)}xx`;
recompte[familia] = (recompte[familia] ?? 0) + 1;
console.log(`${codi.padEnd(24)} | ${estat} | ${familia}`);
}
console.log(JSON.stringify(recompte, null, 2));Solució 3. La clau és calcular el cos igual en tots dos mètodes i decidir només al final si s'envia:
const cos = JSON.stringify(dades, null, 2);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Content-Length', Buffer.byteLength(cos, 'utf8'));
res.end(req.method === 'HEAD' ? undefined : cos);curl -I (que envia HEAD) i curl -s ... | wc -c han de donar el mateix número. En afegir una sala amb accent, Buffer.byteLength puja més que el nombre de caràcters —cada caràcter accentuat ocupa dos bytes en UTF-8—, i aquí es veu per què cos.length hauria mentit.
Conclusió
Ja saps escoltar i contestar amb propietat. req és un IncomingMessage que, a més de method, headers —sempre en minúscules—, httpVersion i socket.remoteAddress, és un stream de lectura el cos del qual encara pot estar viatjant. La seva url no és una URL completa, sinó ruta més consulta, i l'única manera sensata d'interpretar-la és new URL(req.url, base) amb searchParams: descodifica el percentatge, admet claus repetides amb getAll i no es trenca amb ?sala=Sala%20B%C3%B3veda.
res és un ServerResponse i un stream d'escriptura, amb un ordre inviolable: estat i capçaleres primer, cos després, end() sempre. D'aquí surt l'error més freqüent del mòdul, ERR_HTTP_HEADERS_SENT, que es cura amb una regla simple: respondre és acabar, i es respon una sola vegada. Tens el catàleg complet d'estats que farà servir el curs, amb les fronteres que més s'equivoquen —400 davant de 422, 401 davant de 403, 404 davant de 409— i les capçaleres essencials, inclosa la disciplina de calcular Content-Length amb Buffer.byteLength i no amb String.length.
I Escena Viva s'emporta dos mòduls que l'acompanyaran fins al final: src/servidor/respostes.js, amb respondreJson, respondreText, respondreError, respondreSenseContingut i respondreRedireccio, tots amb un format d'error únic; i src/servidor/errors-http.js, amb la taula que tradueix error.codi a estat HTTP —ESDEVENIMENT_NO_TROBAT a 404, AFORAMENT_INSUFICIENT a 409, QUANTITAT_INVALIDA a 400— i un 500 sorollós per defecte per a allò que no sapiguem classificar.
Ens falta l'evident: decidir quin gestor atén cada petició. A la lliçó següent, Enrutament Manual, començarem amb l'if/else ingenu, veurem exactament on es trenca, i construirem src/servidor/enrutador.js amb una taula de rutes, patrons tipus /esdeveniments/:id compilats a expressions regulars, extracció de paràmetres, respostes 405 amb capçalera Allow i un try/catch central que farà servir la taula que acabes d'escriure.
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
