Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Error Stories: Nyomozás az elveszett multipart form adatok után

Szűrő megjelenítése

Error Stories: Nyomozás az elveszett multipart form adatok után

Szóval a következő helyzet állt elő: a Leaflet stacket érintő átalakítások miatt éppen egy új komponensen dolgozok, mely néhány meglévő API endpoint új otthonaként funkcionál majd. A régi kód Java volt, az új komponenst azonban TypeScriptben fejlesztem, így természetesen az API struktúráján kívül semmit nem tudtam újrahasznosítani. Az API konkrétan a Leaflet által támogatott fájlműveleteket jelenti, ami magában foglalja a fájlok feltöltését is a szerverre. Az API létrehozásához a JS/TS berkekben népszerű Express libraryt használtam, és mivel a fájlfeltöltéshez multipart/form-data típusú formot kell küldeni az alkalmazásnak, a Formidable nevű library segítségével kezelem azt (önmagában az Express erre nem képes).

Az új API már kész, persze eddig csak Postmanből tudtam azt tesztelni, így nyilván benne volt a pakliban néhány váratlan hiba. Legnagyobb meglepetésemre azonban, összedrótozva az API-t az admin alkalmazással, minden funkció hibátlanul működött első próbálkozásra - kivéve a fájlok feltöltését. Az admin alkalmazás és az API közti kapcsolatot Jersey-ben írt REST kliens biztosítja, mely - mint ahogy azt már említettem - multipart/form-data formátumban küldi el a requestet. A klienst már évek óta használom a Leafletben levő eredeti file API-jal, így nem számítottam különösebb problémákra, a logban felbukkanó HTTP 500 azonban másról árulkodott. Nem mellesleg a logolt error teljesen irreleváns is volt eredetileg, mivel aszerint a "buffer azután is írva volt, hogy az már le lett zárva". Ez valószínűleg inkább annak volt köszönhető, hogy a Formidable alapbeállításokkal egy temp fájlt hoz létre a fájlrendszerben és onnan lehet a végleges helyére mozgatni, ehelyett azonban úgy döntöttem, hogy a memóriában tárolom a fájlt a feltöltés végéig és onnan írom ki rögtön a végleges helyére. A hibát azonban nem ez okozta, sokkal inkább az, hogy a valós hiba megpróbálta újrahasznosítani a fájlfeltöltésre szánt buffert - amit nyilván nem lett volna szabad megtennie, de elő sem fordulhatott volna.

Tehát muszáj volt mélyebbre ásni a problémában és az első kirajzolódó tünetek inkább voltak félrevezetőek, mint hogy segítettek volna a megoldás megtalálásában. A kérdéses form egy fájlt és két egyszerű input mezőt tartalmaz, tehát egy három elemű multipart formot vár az API. Ezzel szemben csupán egy mező volt látható a feldolgozott formban, és az a "description" mező volt (ami csak egy text input) - az azonban a Formidable által készített VolatileFile objektum volt. Azaz, a két másik mező teljesen elveszett, a descriptiont pedig hibásan ismerte fel. Postmannel és curl-lel továbbra is hibátlanul működött az API, azok azonban a fájlt küldték először a formban (ezt Wiremockkal sikerült kideríteni), így próbálkozásképp a kliensben is megcseréltem a sorrendet. Legnagyobb meglepetésemre, így már felismerte mindhárom mezőt, azonban mindegyiket VolatileFile objektumként értelmezte. Legrosszabb esetben a VolatileFile objektumok tartalmát kiolvasva már feldolgozhatóak lettek volna a text mezők, de valljuk be, ez azért erősen kimerítette volna a "dirty hack" fogalmát.

Akkor folytassuk a nyomozást. A Wiremock segítségével elcsípett requesteket összevetve feltűnt egy furcsaság: a Postman és a curl által generált requestben a "globális" Content-Type mellett (ami természetesen multipart/form-data értéket mutatott), csupán a fájl mező szekciójában volt a szekcióra vonatkozó Content-Type definiálva (ami, mivel egy képet próbáltam feltölteni, image/jpeg volt). Ezzel szemben a Jersey kliens által generált requestben a két másik mező Content-Type-ja is be volt állítva, méghozzá text/plain értékre. Gyakorlatilag ennyi volt csupán a különbség a kettő között - azonban ez már egy kiváló nyom volt a további nyomozást illetően! Ezen a ponton a feltételezésem az volt, hogy a beállított Content-Type miatt a Formidable úgy véli, a két kérdéses mezőt is fájlként kell kezelnie. Sajnos a Formidable-nek nem lehet explicit meghatározni, melyik mezőt hogyan kezelje, így kliens oldalon próbáltam orvosolni a problémát. Ez azonban sikertelennek bizonyult, ugyanis a Jersey ragaszkodik hozzá, hogy a formhoz hozzáadja a text inputok típusát is, méghozzá a már említett text/plain-re állítva. Erős patthelyzet volt ez, ugyanis látványosan nem volt javítható a dolog sem kliens, sem szerver oldalon.

Maradt a Google és a StackOverFlow, sajnos semmi hasonlót nem találtam. Közben már a multipart/form-data formokra vonatkozó RFC dokumentációt is felkutattam, ami szerint a Content-Type elhagyható a mezők saját szekcióiban, azonban ha nincs megadva, akkor text/plain-ként kell értelmezni. így valamelyest érthető volt a Jersey ragaszkodása a típus rögzítéséhez, mivel - bár nem kötelező a típus megadása - a típus alapértelmezés szerint úgyis text/plain lenne. Közben elkezdtem átfutni a Formidable forrását is, és találtam egy érdekes sort. A kód szerint, amennyiben a MIME type (azaz a Content-Type) hiányzik, akkor az adott mezőt text inputként értelmezi a library, ellenkező esetben ... fájlként. Természetesen akkor is, ha az történetesen egy text/plain, ami önmagában nem lenne hiba, hiszen egy .txt fájl az éppen text/plain, azonban a text inputok is alapértelmezés szerint. Végre megvolt a hiba, már csak a megoldás kellett rá, ami... nem volt, legalábbis úgy tűnt. Végső megoldásként már éppen nyitni akartam GitHubon egy issue-t, amikor is megláttam a listában, hogy valaki tavaly augusztusban gyakorlatilag ugyanerre a problémára nyitott egyet. Az issue azóta is nyitott állapotban van, a fejlesztő sajnos nem foglalkozott még vele, így első reakcióm az volt, hogy "nyilván ez is egy zsákutca", legnagyobb meglepetésemre azonban az utolsó kommentben egy működő workaround volt. Az egyetlen probléma az volt vele, hogy a TypeScript típusdefiníció nem pontos a Formidable-höz, így el kellett a kódban helyezni két ts-ignore címkét, de ezen kívül valóban segített. A workaround lényege, hogy a form fogadásakor, mielőtt elkezdené feldolgozni azt a Formidable, el kell távolítani a MIME type-ot (ha a Content-Disposition értékben nincs a fájl neve megadva), hogy aztán a későbbi file / text input döntés utóbbira irányítsa a logikát. Ezt a form objektum létrehozása után egy event callback-kel lehet megoldani, az alábbi módon:

const fileUploadWritable = new FileUploadWritable();
const form = formidable({
	fileWriteStreamHandler: () => fileUploadWritable,
});

// Az .onPart eventre figyelve tudjuk a MIME type-ot eltávolítani a feldolgozott formból
form.onPart = (part: Part) => {
	// @ts-ignore
	if (part.mimetype && !part.headers['content-disposition']?.match(/filename="/)) {
		// @ts-ignore
		delete part.mimetype;
	}

	form._handlePart(part);
};

A fenti kódrészlet a helyére tette a feldolgozott mezőket és csupán 4-5 órám ment el egy design-hiba miatt. :)

GitHub issue a fenti problémára

RFC dokumentum a multipart formokról

Kommentek

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.