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-sdk 2.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
reidhoffman holds 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,
zip stops 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.data is not a list, so the method returns one ScrapeResult instead 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 |
| facebook |
2 |
| instagram |
1 |
| linkedin |
1 |
| pinterest |
1 |
| tiktok |
1 |
Expected
- Pair each row with its input by
input_url / input.url, not by position.
- Set
success from the row: False when the row has an error key.
- 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.
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
ScrapeResultcan carry one input'surland a different input'sdata. Every result is also markedsuccess=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)Reproduction
reidhoffmanholds the dead profile's error, so a caller concludes Reid Hoffman's profile is hidden or private. It is not.zz-not-a-real-profile-zz, an account that does not exist, holds Reid Hoffman's real profile.success=True.The API returns a different order each run
Fetching the raw snapshot for the same three inputs, in a second job:
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:zippairs the n-th input with the n-th row.success=Trueis a literal.Every row already says which input it belongs to
So correct pairing needs no guesswork: match each row on
input_url, falling back toinput.url.Other shapes of the same bug
zipstops at the shorter list and those inputs get no result at all, with nothing to say one is missing.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)"acrossscrapers/*/scraper.py:Expected
input_url/input.url, not by position.successfrom the row:Falsewhen the row has anerrorkey.success=False, rather than dropping it.Workaround
Ignore
ScrapeResult.urlon list input. Flatten everyresult.dataand match rows to inputs oninput_urlorinput.urlyourself.