{"record":{"id":"18963f6f3612e6b7","repo":"jackwener/OpenCLI","slug":"booking-com-extractor-returned-malformed-hotel-row","errorCode":null,"errorMessage":"Booking.com extractor returned malformed hotel row","messagePattern":"Booking\\.com extractor returned malformed hotel row","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/booking/search.js","lineNumber":312,"sourceCode":"    const items = raw.items;\n    if (items.length === 0) {\n      const totalText = String(raw.totalText || '').trim();\n      if (hasPositiveResultCount(totalText)) {\n        throw new CommandExecutionError(\n          `Booking.com page declared results but no property cards were parsed: ${totalText}`,\n        );\n      }\n      throw new EmptyResultError(\n        `booking search ${JSON.stringify(destination)}`,\n        totalText\n          ? `No hotels rendered (${totalText}). Try a broader destination, different dates, or check the URL in a browser.`\n          : 'No hotels rendered. Try a broader destination, different dates, or check the URL in a browser.',\n      );\n    }\n\n    return items.slice(0, limit).map((it, i) => {\n      if (!it || typeof it !== 'object') {\n        throw new CommandExecutionError('Booking.com extractor returned malformed hotel row');\n      }\n      const name = String(it.name || '').trim();\n      const country = String(it.country || '').trim();\n      const slug = String(it.slug || '').trim();\n      const urlValue = String(it.url || '').trim();\n      const expectedUrl = country && slug\n        ? `https://www.booking.com/hotel/${country}/${slug}.html`\n        : '';\n      if (!name || !/^[a-z]{2}$/.test(country) || !slug || urlValue !== expectedUrl) {\n        throw new CommandExecutionError('Booking.com hotel row is missing stable name/url identity');\n      }\n      return {\n        rank: offset + i + 1,\n        name,\n        country,\n        slug,\n        star_rating: it.star_rating,\n        review_score: it.review_score,","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/booking/search.js#L294-L330","documentation":"This CommandExecutionError is thrown while mapping individual extractor items when an element is not a usable object (null, undefined, or a non-object). The library validates each hotel row before normalizing its fields (name, country, slug, url), so a single malformed row aborts the whole result mapping rather than silently emitting garbage.","triggerScenarios":"raw.items is an array but contains a null/undefined entry or a primitive — e.g. the extractor pushed a placeholder/falsy value, or a DOM change made its row-parser return undefined for some cards.","commonSituations":"Partially rendered page where some cards lacked expected structure and the extractor appended null; a custom/patched extractor with inconsistent row parsing; DOM updates mid-extraction (lazy-load race) producing incomplete rows; site A/B tests changing card markup for a subset of results.","solutions":["Update the library so the extractor never emits non-object rows.","Log raw.items to find the offending entry and its index.","Retry the search — a lazy-load race may produce complete rows on a second attempt.","If using a custom extractor, filter falsy rows before returning: items.filter(it => it && typeof it === 'object')."],"exampleFix":"// before (custom extractor)\nreturn { ok: true, items: cards.map(parseCard) };\n// after\nreturn { ok: true, items: cards.map(parseCard).filter(it => it && typeof it === 'object') };","handlingStrategy":"type-guard","validationCode":"// pre-filter raw items yourself if you control the extractor output\nconst safeItems = (Array.isArray(rawItems) ? rawItems : []).filter(it => it && typeof it === 'object');","typeGuard":"const isHotelRow = (it) => it !== null && typeof it === 'object' && typeof (it.name ?? it.url ?? '') !== 'undefined';","tryCatchPattern":"try {\n  return await booking.search(params);\n} catch (e) {\n  if (/malformed hotel row/.test(e.message)) {\n    await sleep(2000);\n    return booking.search(params); // lazy-load race often resolves on retry\n  }\n  throw e;\n}","preventionTips":["Retry once — partial renders during lazy-load commonly cause bad rows.","Keep the extractor's row parser returning objects for every card.","Update the library after Booking.com DOM/A-B test changes.","Filter falsy entries in items at the extractor level before mapping."],"tags":["scraping","data-mapping","schema-validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}