Repository navigation
Description
Summary
When a scraper method is given a list of URLs, the SDK pairs the returned rows with the input URLs by position. The API does not return rows in input order, so each ScrapeResult can carry one input's url and a different input's data. Every result is also marked success=True, including error rows.
On LinkedIn this attaches one person's profile to a different identity. The dataset marks those fields as personal data (pii: true), so this is a data-integrity problem, not only a wrong-label one.
The same code is in six scrapers: amazon (2 places), facebook (2), instagram, linkedin, pinterest, tiktok.
Environment
brightdata-sdk2.5.2 (latest on PyPI)- Python 3.12.13, macOS
- Verified 18 Sep 2026
Reproduction
from brightdata import SyncBrightDataClient
urls = [
"https://www.linkedin.com/in/reidhoffman/",
"https://www.linkedin.com/in/zz-not-a-real-profile-zz/",
"https://www.linkedin.com/in/satyanadella/",
]
with SyncBrightDataClient(auto_create_zones=False) as c:
results = c.scrape.linkedin.profiles(urls, timeout=420)
for r in results:
d = r.data if isinstance(r.data, dict) else {}
print(r.url, "| success", r.success, "| name", d.get("name"), "| error", d.get("error"))https://www.linkedin.com/in/reidhoffman/ | success True | name None | error The profile is hidden or private.
https://www.linkedin.com/in/zz-not-a-real-profile-zz/ | success True | name Reid Hoffman | error None
https://www.linkedin.com/in/satyanadella/ | success True | name Satya Nadella | error None
- The result labelled
reidhoffmanholds the dead profile's error, so a caller concludes Reid Hoffman's profile is hidden or private. It is not. - The result labelled
zz-not-a-real-profile-zz, an account that does not exist, holds Reid Hoffman's real profile. - All three are
success=True.
The API returns a different order each run
Fetching the raw snapshot for the same three inputs, in a second job:
raw[0] reidhoffman
raw[1] satyanadella
raw[2] zz-not-a-real-profile-zz (error row)
The first job returned the error row first. The order is not stable, so there is no input ordering a caller could choose to avoid this.
Why it happens
scrapers/linkedin/scraper.py, _scrape_urls:
results = []
for url_item, data_item in zip(url_list, result.data):
results.append(
ScrapeResult(
success=True,
data=data_item,
url=url_item,
...zip pairs the n-th input with the n-th row. success=True is a literal.
Every row already says which input it belongs to
success row: input_url = "https://www.linkedin.com/in/reidhoffman/"
error row: input = {"url": "https://www.linkedin.com/in/zz-not-a-real-profile-zz/"}
error row keys: ["error", "error_code", "input", "timestamp"]
So correct pairing needs no guesswork: match each row on input_url, falling back to input.url.
Other shapes of the same bug
- If the API returns fewer rows than inputs,
zipstops at the shorter list and those inputs get no result at all, with nothing to say one is missing. - When the job fails or times out,
result.datais not a list, so the method returns oneScrapeResultinstead of a list. A caller iterating the list-input return gets a single object.
Affected
grep -c "zip(url_list, result.data)" across scrapers/*/scraper.py:
| scraper | occurrences |
|---|---|
| amazon | 2 |
| 2 | |
| 1 | |
| 1 | |
| 1 | |
| tiktok | 1 |
Expected
- Pair each row with its input by
input_url/input.url, not by position. - Set
successfrom the row:Falsewhen the row has anerrorkey. - Return one result per input even when a row is missing, with
success=False, rather than dropping it. - Return a list for list input on every path, including failure and timeout.
Workaround
Ignore ScrapeResult.url on list input. Flatten every result.data and match rows to inputs on input_url or input.url yourself.
Activity