· 6 min read · by the Devil Scrapes team
A GraphQL operationName silently gated real data
graphql operationname deep dive: how Reverb's price-guide search served total>0 with an empty list for three weeks, and how a browser HAR capture found the real fix.
Quick answer: For roughly three weeks, our Reverb Scraper — Sold Listings & Price Guide’s search mode called gql.reverb.com, got back total: 1407 matching results, and an empty csps: null list every single time. Three separate investigations confirmed the symptom and stopped there, because GraphQL introspection was disabled and nobody had a browser capture of what the real Reverb frontend sends. The eventual fix was one string: the operationName field on the request. Everything else — query text, variables, selection set — was already correct.
The bug that looked like an outage
A GraphQL request body carries three things: a query document, a set of variables, and an operationName string telling the server which named operation inside that document to execute. Most scrapers treat operationName as decoration — a label you can set to anything descriptive, since the server is supposedly just matching it against the query text. That assumption held for gql.reverb.com right up until it didn’t.
Reverb’s price-guide search — the cspSearch field — answered every request with a perfectly plausible total count. Search “gibson les paul” and you’d get back total: 1407. Search “fender stratocaster” and you’d get total: 1408. The count was real, current, and moved when the underlying inventory moved. But the csps field — the actual list of matched items — was null. Not an empty array. Not a GraphQL errors block. Just null, on a field the schema says returns a list.
That specific shape — a live, correct count next to a null list, with no error anywhere in the response — is what made this hard. A 403 or a 429 tells you immediately that something is blocking you. A null field on an otherwise-200 response with no errors array looks exactly like a data availability problem on the target’s side, not a request problem on yours.
Three investigations, same wall
The CHANGELOG for this Actor documents the sequence honestly, because it’s instructive. On 2026-08-26, a routine health check found cspSearch returning csps: null across ten-plus live probes — different proxy tiers, browser-impersonation profiles, and header sets, cookie-warmed and cookieless. Every combination came back identical, so the conclusion at the time was reasonable: this looks like an upstream defect, since varying every request-level thing we control changed nothing.
On 2026-09-01 and again on 2026-09-09, later passes re-probed live and got the same signature, the second time backed by two throwaway cloud runs against the live build to rule out a proxy or session-level explanation. All three sessions reached the same wall: Reverb’s GraphQL endpoint has introspection disabled, so there’s no schema to check a guess against. What none of those sessions had was a way to see what Reverb’s own frontend actually sends when a real user runs this same search in a browser — a real-browser HAR capture. That was the missing piece the whole time.
The capture that found it
The fix came from opening https://reverb.com/price-guide in a real headless browser, typing “gibson les paul” into the search box, and recording every request and response to gql.reverb.com as the page made them. No proxy was even needed locally — Reverb didn’t block the capture session’s IP. The frontend’s own search request came back with total: 1004 and a real, populated nine-item csps list. The search itself was never broken; only our replica of the request was.
Diffing the frontend’s request against ours turned up the answer immediately: the frontend’s operationName was "Core_SellFlow_Search". Ours was "PriceGuideSearch" — a label chosen when this mode was rewritten to GraphQL back in July, documented in the code’s own comments as “operation Core_SellFlow_Search” but never actually set to match that documentation. The constant and the comment describing it had drifted apart, and nothing caught it because no live test exercised the real endpoint against a fixture built from a real response.
To be certain operationName was the actual cause and not a coincidence riding alongside some other difference, an isolation harness ran the exact same query text and the exact same variables under both operation names, back to back:
operationName="Core_SellFlow_Search" -> total=1004, csps=list[5] # populated
operationName="PriceGuideSearch" -> total=1004, csps=null # broken
Same query. Same variables. One string different. Reproduced against two independent search terms, “fender stratocaster” and “moog synthesizer” — same result both times. Every other candidate difference between the two requests — excludedCategoryUuids, sort, fuzzy, listingsThatShipTo, hasExpressSaleBid, withAggregations, boostByClicks, selection-set fields, an @include(if:) directive — was individually added and removed from the broken request, and none of them moved the result. Only operationName did.
What this actually was
gql.reverb.com appears to gate the cspSearch.csps list behind an operation-name allowlist, while total — a cheaper, less-gated resolver — kept answering regardless of which name you sent: let the aggregate count through freely, but only release the actual item list to requests carrying the operation name the real frontend uses. It produces exactly the symptom every prior investigation saw, and it’s invisible to anyone who never captures real frontend traffic to compare against.
The fix was small once found: CSP_SEARCH_OPERATION changed from "PriceGuideSearch" to "Core_SellFlow_Search", and the query’s variable names were aligned to what the real operation declares — sellCardLimit instead of limit, fullTextQueryOperand as a variable instead of a literal, plus two fields the real operation requires but that don’t themselves affect whether csps resolves. The existing fail-loud guard that had been correctly firing an honest FAILED status for three weeks, rather than quietly billing customers for zero rows, stayed in place as defense-in-depth.
Regression coverage now pins the operation name directly and replays a fixture built from the real captured response, so a future drift between the constant and its own docstring trips a test instead of three weeks of silent search failures.
Why maintenance is the product here
Nothing about this bug was visible from the outside. The run finished; it just found nothing, over and over, for a search term that objectively had over a thousand matches. A buyer evaluating this Actor by running it once during the outage window would have seen a working scraper that happened to return zero rows for their particular search — indistinguishable from “nobody’s selling that guitar right now.” The difference between that read and the truth took someone treating a plausible-looking null as a signal worth chasing down with a real browser, three separate times, until the fourth pass had the right tool.
That’s the case for buying a maintained scraper instead of building a one-off request replay yourself: the value isn’t the code that worked on day one, it’s someone still checking three weeks later when a target quietly changes which string it’s willing to answer.
Run it on Apify:
- Reverb Scraper — Sold Listings & Price Guide
- Reverb Scraper — Sold Listings & Price Guide — guideIds mode also works if you already have Reverb’s internal price-guide IDs.
FAQ
What is a GraphQL operationName and why does it matter here?
It’s the string in a GraphQL request that tells a multi-operation query document which named operation to execute. Most APIs treat it as a label with no security relevance. Reverb’s gql.reverb.com endpoint appears to use it as part of an anti-scraping gate: a wrong operation name still gets a valid, non-error response with a real count, but the actual list field comes back null.
Why didn’t a 403 or an error message show this sooner?
Because there wasn’t one. Every response was 200 OK with total populated and no errors array — the shape of a legitimate answer, not a block. That’s what made this look like an upstream outage rather than a request-shape bug across three separate investigations.
How was the real operation name found without API documentation?
Reverb’s GraphQL introspection is disabled, so there’s no schema to inspect. The fix came from a real-browser capture of reverb.com’s own price-guide search — recording exactly what the site’s own frontend sends to the same endpoint, then diffing it against our request.
Does this affect the guideIds mode of this Actor too?
No. guideIds mode fetches Reverb’s price-guide estimate by internal ID through a separate endpoint and was unaffected throughout — it kept working the entire time query-mode was serving csps: null.