Skip to content

Commit 6092fda

Browse files
authored
fix(playwright-service): seed Cookie into the context jar so it survives redirects (firecrawl#3726)
1 parent 3248877 commit 6092fda

2 files changed

Lines changed: 76 additions & 3 deletions

File tree

apps/api/src/__tests__/snips/v2/scrape.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,28 @@ describe("Scrape tests", () => {
715715
scrapeTimeout * 2 + 1 * indexCooldown,
716716
);
717717

718+
// Gated to the playwright engine (where cookies are seeded into the jar);
719+
// a Cookie passed as an extra request header is dropped on redirect hops.
720+
concurrentIf(HAS_PLAYWRIGHT && !HAS_FIRE_ENGINE)(
721+
"forwards cookies across redirects",
722+
async () => {
723+
// httpbin's /cookies echoes the cookies it received. The cookie only
724+
// survives the 302 hop if it was seeded into the browser cookie jar.
725+
const response = await scrape(
726+
{
727+
url: "https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fcookies&status_code=302",
728+
headers: { Cookie: "fc_cookie_redirect_test=1" },
729+
formats: ["rawHtml"],
730+
waitFor: 1000,
731+
},
732+
identity,
733+
);
734+
735+
expect(response.rawHtml).toContain("fc_cookie_redirect_test");
736+
},
737+
scrapeTimeout,
738+
);
739+
718740
it.concurrent(
719741
"respects mobile",
720742
async () => {

apps/playwright-service-ts/api.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,61 @@ app.post('/scrape', async (req: Request, res: Response) => {
414414
page = await requestContext.newPage();
415415

416416
if (headers) {
417-
// Remove the user-agent key before calling setExtraHTTPHeaders since
418-
// we already forwarded it to the context-level userAgent option.
417+
// A Cookie header passed through setExtraHTTPHeaders is sent on the first
418+
// request but DROPPED on any redirect hop (the browser regenerates the
419+
// redirected request from its cookie jar, which is empty). Authenticated
420+
// sites that 302 (e.g. to /signin when the session looks absent) then
421+
// land on the login page. Seed the cookie jar instead so Chromium re-sends
422+
// it on every request, including redirects — matching what a raw HTTP
423+
// client does.
424+
const cookieHeader = Object.entries(headers).find(([k]) => k.toLowerCase() === 'cookie')?.[1];
425+
if (cookieHeader) {
426+
// Scope cookies to the registrable domain (e.g. ".example.com"), not
427+
// host-only. Authenticated pages often 302 across sibling subdomains
428+
// (example.com -> app.example.com); a host-only cookie set for the
429+
// original host would not be sent to the redirect target, leaving the
430+
// request unauthenticated. The Cookie header carries no domain info, so
431+
// we apply the eTLD+1 — broad enough to follow the redirect, and these
432+
// are first-party cookies being returned to their own origin anyway.
433+
let cookieDomain: string | undefined;
434+
try {
435+
const host = new URL(url).hostname;
436+
const labels = host.split('.');
437+
cookieDomain = labels.length > 2 ? labels.slice(-2).join('.') : host;
438+
} catch {
439+
cookieDomain = undefined;
440+
}
441+
type SeedCookie = { name: string; value: string; url?: string; domain?: string; path?: string };
442+
const cookies = cookieHeader
443+
.split(';')
444+
.map(pair => pair.trim())
445+
.filter(Boolean)
446+
.map((pair): SeedCookie | null => {
447+
const eq = pair.indexOf('=');
448+
if (eq === -1) return null;
449+
const name = pair.slice(0, eq).trim();
450+
const value = pair.slice(eq + 1).trim();
451+
return cookieDomain
452+
? { name, value, domain: `.${cookieDomain}`, path: '/' }
453+
: { name, value, url };
454+
})
455+
.filter((c): c is SeedCookie => c !== null);
456+
if (cookies.length > 0) {
457+
try {
458+
await requestContext.addCookies(cookies);
459+
} catch (error) {
460+
console.warn('Failed to seed cookies from Cookie header:', error);
461+
}
462+
}
463+
}
464+
465+
// Remove user-agent (already applied at the context level) and cookie
466+
// (now seeded into the jar) before forwarding the rest verbatim.
419467
const filteredHeaders = Object.fromEntries(
420-
Object.entries(headers).filter(([k]) => k.toLowerCase() !== 'user-agent')
468+
Object.entries(headers).filter(([k]) => {
469+
const lower = k.toLowerCase();
470+
return lower !== 'user-agent' && lower !== 'cookie';
471+
})
421472
);
422473
if (Object.keys(filteredHeaders).length > 0) {
423474
await page.setExtraHTTPHeaders(filteredHeaders);

0 commit comments

Comments
 (0)