jackwener/OpenCLI · error · CommandExecutionError

Booking.com hotel row is missing stable name/url identity

Error message

Booking.com hotel row is missing stable name/url identity

What it means

This CommandExecutionError is thrown during post-processing of scraped Booking.com hotel rows when a row lacks the stable identity the CLI guarantees: a non-empty name, a two-letter lowercase country code, a non-empty slug, and a url that exactly equals the canonical https://www.booking.com/hotel/<country>/<slug>.html form. It exists so downstream consumers can rely on every row having a deterministic, reconstructible hotel URL rather than a transient DOM-derived value. It indicates the page extractor produced a row whose DOM shape the extractor did not fully capture.

Source

Thrown at clis/booking/search.js:322

        totalText
          ? `No hotels rendered (${totalText}). Try a broader destination, different dates, or check the URL in a browser.`
          : 'No hotels rendered. Try a broader destination, different dates, or check the URL in a browser.',
      );
    }

    return items.slice(0, limit).map((it, i) => {
      if (!it || typeof it !== 'object') {
        throw new CommandExecutionError('Booking.com extractor returned malformed hotel row');
      }
      const name = String(it.name || '').trim();
      const country = String(it.country || '').trim();
      const slug = String(it.slug || '').trim();
      const urlValue = String(it.url || '').trim();
      const expectedUrl = country && slug
        ? `https://www.booking.com/hotel/${country}/${slug}.html`
        : '';
      if (!name || !/^[a-z]{2}$/.test(country) || !slug || urlValue !== expectedUrl) {
        throw new CommandExecutionError('Booking.com hotel row is missing stable name/url identity');
      }
      return {
        rank: offset + i + 1,
        name,
        country,
        slug,
        star_rating: it.star_rating,
        review_score: it.review_score,
        review_count: it.review_count,
        price_amount: it.price_amount,
        price_currency: it.price_amount == null ? '' : (currency || it.price_currency || ''),
        distance: it.distance,
        recommended_room: it.recommended_room,
        url: urlValue,
      };
    });
  },
});

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Re-run the search with a different locale (--lang en) or destination to see if only certain layouts fail
  2. Update the Booking.com extractor to pick up the new hotel-card DOM selectors so name/slug/url are captured
  3. Check Booking.com HTML for the changed card structure and add the missing selector to the in-page extraction script
  4. If a specific property type never matches the /hotel/<country>/<slug>.html pattern, extend expectedUrl construction or relax the strict equality while keeping the invariant documented

Example fix

// before (extractor misses url on new card layout)
url: card.querySelector('a.hotel-card-link')?.href || ''
// after
target_url: card.querySelector('a[data-testid="property-card-desktop-link"], a.hotel-card-link')?.getAttribute('href') || ''
Defensive patterns

Strategy: validation

Validate before calling

// Validate extracted rows before calling/consuming the search result
function hasStableHotelIdentity(it) {
  if (!it || typeof it !== 'object') return false;
  const name = String(it.name || '').trim();
  const country = String(it.country || '').trim();
  const slug = String(it.slug || '').trim();
  const url = String(it.url || '').trim();
  const expected = country && slug ? `https://www.booking.com/hotel/${country}/${slug}.html` : '';
  return !!name && /^[a-z]{2}$/.test(country) && !!slug && url === expected;
}
if (!raw.items.every(hasStableHotelIdentity)) {
  console.warn('some Booking.com rows lack stable identity; extractor may be stale');
}

Type guard

function isWellFormedHotelRow(it) {
  return (
    typeof it === 'object' && it !== null &&
    typeof it.name === 'string' && it.name.trim() !== '' &&
    typeof it.slug === 'string' && it.slug.trim() !== '' &&
    typeof it.country === 'string' && /^[a-z]{2}$/.test(it.country) &&
    typeof it.url === 'string' &&
    it.url === `https://www.booking.com/hotel/${it.country}/${it.slug}.html`
  );
}

Try / catch

try {
  const results = await runBookingSearch(opts);
} catch (err) {
  if (err.message.includes('missing stable name/url identity')) {
    // fall back to raw rows or flag extractor drift
    console.warn('Booking extractor layout drift; skipping identity check or re-extract');
  } else throw err;
}

Prevention

When it happens

Trigger: Running the booking search command when the page extractor returns an item where: it.name is empty, it.country is missing or not a 2-letter lowercase code, it.slug is empty, or it.url does not exactly match the constructed canonical hotel URL. Booking.com markup changes, alternative hotel card layouts, locale variants emitting uppercase/region country codes, or hotels whose canonical URL deviates from the /hotel/<country>/<slug>.html pattern all trigger it.

Common situations: A Booking.com A/B test or redesign changes hotel card DOM attributes so the extractor misses name/slug/url fields; scraping a non-standard property type (e.g. hostels/apartments with different URL structure); country code returned as 'GB' or 'en-gb' instead of 'gb'; new marketplace domains (e.g. .co.uk or b-hotel URLs) failing the equality check.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/d60c7fabea71e882. Report an issue: GitHub.