How I pulled data out of a cross-origin iframe I couldn't touch
The feature was supposed to be the easy one. NotebookLM generates flashcards. My extension reads them off the page. You export them to Anki. I'd already built the hard-looking part - the export - so I figured reading the cards was an afternoon of work. It took two days, and by the end I was intercepting a private Google API and peeling a 1.5MB blob apart by hand.
I opened the flashcard view, hit F12, ran my scraper. Zero cards. Every selector came back empty - not wrong text, nothing. So I did the honest thing and actually looked at the DOM instead of trusting what I thought was there. The cards render inside an iframe. And the iframe is served from a different domain - scf.usercontent.goog, if you want the specifics. That's the same-origin policy doing exactly what it's built to do: my code runs on notebooklm.google.com, and the browser flatly refuses to let it reach into a frame from another origin. I could see the cards with my own eyes. My script was standing on the wrong side of a wall it isn't allowed to climb.
Two ways over that wall
One: inject a script directly into the iframe and read it from the inside.
Two: forget the iframe entirely and catch the data on the network, before it ever gets there. Because the main page has to fetch that flashcard data from somewhere to build the iframe in the first place.
Finding the network call
I went looking for the network call. Opened the Network tab, regenerated the cards, and watched. There it was: a request to a thing called batchexecute - Google's internal RPC endpoint - with rpcid=v9rmvd. The response was 1,596,933 bytes. One and a half megabytes for 56 flashcards. Somewhere in there was my data.
Parsing Google's chunked format
Then the fun part: that response is not JSON you can just parse. It's Google's chunked format - a )]}' safety prefix, then alternating lines of "here's a length" and "here's a blob." Parse the whole thing and it throws immediately. You have to walk it line by line, find the one line that mentions v9rmvd, and parse just that. Inside that line is another JSON string. Parse that, and you get a nested array. Buried in the array is a full HTML document - the entire iframe page, escaped, doctype and all. And inside that HTML, finally, is an attribute called data-app-data holding the thing I'd been chasing for two days:
{
"flashcards": [
{
"f": "front text",
"b": "back text",
"c": 1
},
...
]
}
Clean JSON. f is the front, b is the back - which map perfectly onto Anki's own two fields - and c marks the card type (1 for normal, 2 for cloze/fill-in-the-blank). Fifty-six of them, in order, nothing truncated.
The isolated world trap
One last trap before it worked. A Chrome extension's content script lives in an "isolated world" - it can see the page's DOM but not the page's own JavaScript, which means it can't see the page's network calls either. To hook the XHR that carries the flashcards, I had to inject a second script into the page's MAIN world, catch the response there, and hand it back across the boundary with postMessage. Miss that detail and you hook nothing and can't figure out why.
Takeaways
What I keep taking away from this: the wall was real, but it only guarded one door. The same-origin policy stops you reading the iframe - it does nothing to stop you reading the request that feeds the iframe. Most "you can't get there" walls in browser land are like that. There's usually a side entrance, and it's usually the network tab.
The honest catch is that I'm now parsing an undocumented Google format held together with an rpcid I don't control. The day they rename v9rmvd, this breaks. So I wrapped every step in a fail-to-empty and wired it to my telemetry, so I hear about it from a dashboard instead of an angry email the week before someone's exam. Building on a platform you don't own means picking which fragile thing you'd rather babysit. I picked this one on purpose - a clean JSON payload beats scraping rendered HTML that redesigns itself every quarter.
Ever had to go around the front door like this - where the official "no" had an unofficial "sure, over here"? I'd love to hear the weirdest one you've pulled off.
- building NotebookBloom in public, #6
Top comments (0)
Comments
No comments yet. Start the discussion.