{"record":{"id":"00d7cc67b89af4b7","repo":"antiwork/gumroad","slug":"please-provide-a-valid-gumroad-product-url","errorCode":null,"errorMessage":"Please provide a valid Gumroad product URL","messagePattern":"Please provide a valid Gumroad product URL","errorType":"http","errorClass":"GlobalAffiliates::ProductEligibilityController::InvalidUrl","httpStatus":200,"severity":"warning","filePath":"app/controllers/global_affiliates/product_eligibility_controller.rb","lineNumber":28,"sourceCode":"\n    product_data = fetch_and_parse_product_data\n    render json: { success: true, product: product_data }\n  rescue InvalidUrl, URI::InvalidURIError, Addressable::URI::InvalidURIError\n    render json: { success: false, error: \"Please provide a valid Gumroad product URL\" }\n  end\n\n  private\n    # Resolve the pasted URL to a product, then shape exactly the fields the\n    # affiliate-eligibility UI needs. We fetch the product's own public JSON\n    # endpoint (GET /l/:permalink.json) to leverage its URL routing — that\n    # handles short domains, subdomains, and custom permalinks for free — but\n    # we read `recommendable?` from the model directly. Recommendability is an\n    # affiliate-program eligibility concept, not part of the public product API\n    # surface (ProductPresenter::PublicApiProps), so it must not be exposed\n    # there; resolving it locally keeps that boundary clean.\n    def fetch_and_parse_product_data\n      uri = Addressable::URI.parse(params[:url])\n      raise InvalidUrl unless GUMROAD_DOMAINS.include?(uri&.domain)\n      uri.path = uri.path + \".json\"\n\n      response = HTTParty.get(uri.to_s)\n      raise InvalidUrl unless response.ok?\n\n      data = response.to_hash\n      raise InvalidUrl unless data[\"api_version\"] == ProductPresenter::PublicApiProps::API_VERSION && data[\"permalink\"].present?\n\n      id = data[\"id\"]\n      raise InvalidUrl if id.blank?\n\n      product = Link.find_by_external_id(id)\n      raise InvalidUrl if product.nil?\n\n      {\n        \"name\" => product.name,\n        \"formatted_price\" => product.price_formatted_verbose,\n        \"recommendable\" => product.recommendable?,","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/global_affiliates/product_eligibility_controller.rb#L10-L46","documentation":"First of five `InvalidUrl` raise sites in `fetch_and_parse_product_data` (global_affiliates/product_eligibility_controller.rb:28). The pasted URL parsed successfully, but `Addressable::URI#domain` is not one of `GUMROAD_DOMAINS` (built from ROOT_DOMAIN, SHORT_DOMAIN, and DOMAIN), so the URL is not on a Gumroad-owned host. The controller rescues `InvalidUrl`/`URI::InvalidURIError` and returns `{ success: false, error: \"Please provide a valid Gumroad product URL\" }`.","triggerScenarios":"GET global_affiliates product eligibility check with `url` param pointing at a non-Gumroad host (e.g. https://example.com/l/foo), a bare path with no host (\"/l/foo\"), a third-party link shortener, or a seller's custom non-Gumroad domain. Also fires when the param is missing so `uri&.domain` is nil.","commonSituations":"Users paste a Shopify/Lemon Squeezy/own-domain checkout link instead of the Gumroad product URL; the field is left empty or contains just a permalink; someone expects arbitrary redirects to be followed.","solutions":["Paste a full Gumroad product URL including scheme and host: https://gumroad.com/l/<permalink>, https://<subdomain>.gumroad.com/l/<permalink>, or https://gum.co/<permalink>.","Make sure the `url` param is non-empty and includes the protocol (https://) — bare permalinks or paths fail the domain check.","Do not use third-party shorteners or non-Gumroad mirrors; resolve them to the final gumroad.com URL first.","As a caller, pre-validate with `GUMROAD_DOMAINS.include?(Addressable::URI.parse(url)&.domain)` before submitting."],"exampleFix":"# before\nGlobalAffiliates::ProductEligibilityController show with params: { url: \"https://example.com/l/demo\" } # => error\n\n# after\nuri = Addressable::URI.parse(\"https://gumroad.com/l/demo\")\nGlobalAffiliates eligibility check with params: { url: uri.to_s } if [\"gumroad.com\", \"gum.co\"].include?(uri.domain)","handlingStrategy":"validation","validationCode":"uri = Addressable::URI.parse(input_url)\nallowed = [\"gumroad.com\", \"gum.co\"].include?(uri&.domain) && uri.path.present?\nsubmit_eligibility_check(url: uri.to_s) if allowed","typeGuard":"def gumroad_product_url?(value)\n  uri = Addressable::URI.parse(value.to_s)\n  GUMROAD_DOMAINS.include?(uri&.domain) && uri.path.to_s.match?(%r{\\A/(l/)?[^/]+})\nrescue Addressable::URI::InvalidURIError\n  false\nend","tryCatchPattern":"def show\n  render json: { success: true, product: fetch_and_parse_product_data }\nrescue InvalidUrl, URI::InvalidURIError, Addressable::URI::InvalidURIError\n  render json: { success: false, error: \"Please provide a valid Gumroad product URL\" }\nend","preventionTips":["Validate scheme + Gumroad domain client-side before submitting the form.","Autofill the field from the product's own long_url when possible instead of pasting.","Reject empty input early with a clear message.","Don't feed arbitrary user URLs in — this endpoint intentionally refuses non-Gumroad hosts."],"tags":["url-validation","affiliates","invalid-url","domain-check"],"backgroundTag":"url-validation-failed","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}