DEV Community

Ethan Walker
Ethan Walker

Posted on

Validate Google Search API Inputs Before Sending a Request

Cover Image
A request can contain the right parameter names and still express something different from what the caller intended. A full Google Search URL combined with a separate country field is one example: the caller sees both settings, while only the URL controls the search.

Input validation gives your application a place to catch that mismatch. It can also catch a misspelled field, reject conflicting location settings, and prevent an empty query from entering a collection job.

This tutorial builds a local Python validator for Scrapeless Google Search API. It returns a request body that the caller can inspect. It makes no HTTP calls, requires no API key, and uses only Python's standard library.

Separate the API Contract From Your Application Policy

Some checks come directly from the API parameter reference:

  • Parameter-based input requires q.
  • A full url is an alternative input mode; other input parameters are ignored when it is supplied.
  • location and uule cannot be combined.
  • gl, hl, google_domain, and the other named fields have different roles.

Other checks are decisions made by the application in this article. It supports a small country and language set, accepts selected Google hosts, and rejects fields that have not been reviewed for this integration. Those restrictions make the example easier to understand and test. They do not describe the service's full supported range.

Keep this distinction visible in error messages. “Outside this application's supported values” tells a developer where the restriction lives. “The API does not support this country” would claim something the validator has not established.

Choose One Input Mode

In parameter mode, callers supply a query and optional fields inside input. Country, language, and location stay individually visible to forms, logs, and downstream storage.

In URL mode, callers supply a complete Google Search URL inside input.url. The service ignores other input fields in that mode. This validator therefore rejects a mixed object instead of silently dropping the extra keys.

The same principle applies to location. When both location and uule are present, the validator raises an error. It does not pick one based on dictionary order or remove a field behind the caller's back.

Rejecting conflicts is especially useful in a form backed by saved configuration. A previously selected city can remain in state when a user switches input modes. A visible validation error gives the application a clear reason to ask the user to resolve the configuration.

Define a Reviewed Parameter Allowlist

The validator accepts the following parameter names in parameter mode:

Group Fields Local behavior
Query q Require a nonempty string
Search context gl, hl, google_domain Check against this example's configured values
Origin location, uule Require text and reject simultaneous use
Result restrictions cr, lr, tbs Require text; detailed syntax remains unchecked
Filtering safe, nfpr, filter Check the selected string or integer values
Search type tbm Accept isch; omit the field for regular web search
Pagination start Require a nonnegative integer
Device device Accept desktop, matching the current reference

An allowlist catches spelling errors such as langauge or a field named country mistakenly placed where gl was intended. It also provides a review point when your application adds another parameter.

The example does not include every field in the reference. Adding a new entry should include its documented meaning, local type rules, and an example that your consumer can handle. A field's presence in a reference table does not automatically establish an output mapping for every search scenario.

Install Nothing and Run the Complete Validator

Save the following file as validate_search_input.py. Run it with python3 validate_search_input.py in a terminal. It uses dictionary operations, JSON serialization, regular expressions, and the standard URL parser.

The three built-in inputs demonstrate a parameter-based web search, a city-origin request, and URL mode. The program prints constructed request bodies. These are local outputs, not responses from Google or from Scrapeless.

"""Local input policy for a small Google Search API integration; no network calls."""
import json
import re
from urllib.parse import parse_qs, urlsplit

PARAMETERS = {
    "q", "gl", "hl", "location", "uule", "google_domain", "cr", "lr",
    "tbs", "safe", "nfpr", "filter", "tbm", "start", "device",
}
COUNTRIES = {"us", "uk", "fr"}
LANGUAGES = {"en", "es", "fr"}
DOMAINS = {"google.com", "google.co.uk", "google.fr"}
URL_HOSTS = DOMAINS | {"www." + domain for domain in DOMAINS}


def nonempty_text(value, field):
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"{field} must be a nonempty string")


def validate_search_url(value):
    nonempty_text(value, "url")
    if any(char.isspace() for char in value):
        raise ValueError("url must encode whitespace")
    if re.search(r"%(?![0-9a-fA-F]{2})", value):
        raise ValueError("url contains an invalid percent escape")
    parts = urlsplit(value)
    if parts.scheme != "https" or parts.hostname not in URL_HOSTS:
        raise ValueError("url must use HTTPS and an approved Google host")
    if parts.username is not None or parts.password is not None or parts.port is not None:
        raise ValueError("url must not contain credentials or an explicit port")
    if parts.path != "/search" or parts.fragment:
        raise ValueError("url must use /search without a fragment")
    query = parse_qs(parts.query, keep_blank_values=True, strict_parsing=True)
    if any(not key or len(values) != 1 for key, values in query.items()):
        raise ValueError("url query keys must be nonempty and unique")
    nonempty_text(query.get("q", [None])[0], "url query q")


def validate_input(value):
    if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
        raise ValueError("input must be a dictionary with string keys")
    if "url" in value:
        if set(value) != {"url"}:
            raise ValueError("Use url alone; other input parameters would be ignored")
        validate_search_url(value["url"])
        return dict(value)

    unknown = set(value) - PARAMETERS
    if unknown:
        raise ValueError("Unreviewed input fields: " + ", ".join(sorted(unknown)))
    nonempty_text(value.get("q"), "q")
    if "location" in value and "uule" in value:
        raise ValueError("Choose location or uule")

    for field in PARAMETERS - {"start", "nfpr", "filter"}:
        if field in value:
            nonempty_text(value[field], field)
    choices = {
        "gl": COUNTRIES, "hl": LANGUAGES, "google_domain": DOMAINS,
        "safe": {"active", "off"}, "tbm": {"isch"}, "device": {"desktop"},
    }
    for field, allowed in choices.items():
        if field in value and value[field] not in allowed:
            raise ValueError(f"{field} is outside this application's supported values")
    for field in ("nfpr", "filter"):
        if field in value and (type(value[field]) is not int or value[field] not in (0, 1)):
            raise ValueError(f"{field} must be integer 0 or 1")
    if "start" in value and (type(value["start"]) is not int or value["start"] < 0):
        raise ValueError("start must be a nonnegative integer")
    return dict(value)


def build_request(value):
    return {"actor": "scraper.google.search", "input": validate_input(value)}


if __name__ == "__main__":
    examples = [
        {"q": "coffee", "gl": "us", "hl": "en", "start": 0},
        {"q": "coffee", "gl": "fr", "hl": "fr", "location": "Paris, France"},
        {"url": "https://www.google.com/search?q=coffee&gl=us&hl=en"},
    ]
    for example in examples:
        print(json.dumps(build_request(example), ensure_ascii=False))
Enter fullscreen mode Exit fullscreen mode

The output contains three JSON request bodies with actor set to scraper.google.search. No credential is read, and no request is submitted. This makes the script usable in a local editor or a continuous-integration check before any account-dependent work.

Check URL Structure Without Claiming Complete URL Validation

The full-URL branch accepts HTTPS URLs on a small set of approved Google hosts, requires the /search path, rejects credentials and explicit ports, and requires a nonempty q query parameter. It also rejects fragments, unencoded whitespace, malformed percent escapes, and repeated query keys.

These are application rules. For example, rejecting an explicit default HTTPS port is a deliberate simplification, not a statement that such a URL is invalid everywhere. The domain set contains only google.com, google.co.uk, and google.fr, with optional www hosts. Expand that set only after reviewing the destinations needed by your integration.

Use exact host membership rather than a substring check. A hostname containing the text google.com can still belong to a different domain. Parsing the URL allows the application to inspect the hostname as a component.

Python's urllib.parse documentation notes that parsing does not itself provide complete validation. This example adds checks for its own narrow URL contract. It does not establish that every query-string parameter or encoded location is meaningful to Google.

URL mode also does not apply the parameter-mode country and language sets to values embedded in the URL. It preserves the complete URL as a distinct input mode. If your product needs identical value restrictions in both modes, add an explicit query-parameter policy and tests before enabling URL entry for that workflow.

Keep Validation From Changing the Query

The validator checks whether a text value is nonempty after whitespace inspection, but returns the original value. It does not translate the query, rewrite operators, silently change country codes, or insert a location.

That makes the accepted input useful as a record of caller intent. If the application wants normalization, implement it as a separate, visible step with its own rules. Preserve the original text when a transformation matters to later analysis.

The returned dictionary is a shallow copy. In this example, accepted parameter values are scalar strings or integers, so callers can work with the returned dictionary without sharing a nested mutable configuration object. The function does not accept arbitrary lists or nested objects as parameter values.

The integer checks also reject booleans. Python treats bool as a subclass of int, so a general integer-instance check would accept True as an offset. Using type(value) is int expresses the narrower policy intended here.

Exercise the Failure Paths

Try changing the first example to include both url and gl. The function raises an error before building the request. The caller can display that message beside the relevant field or record a configuration failure separately from collection outcomes.

Useful cases to cover include:

  • An empty query and an unknown parameter name.
  • Both location representations in one input.
  • A negative offset, a string offset, or a boolean offset.
  • A country outside the application's configured set.
  • A URL using an unapproved host or an ambiguous repeated query key.
  • A valid input whose text and values must remain unchanged.

The companion test_validate_search_input.py exercises accepted inputs and these rejection paths with Python's standard unittest module. Place it beside the validator and run python3 -m unittest -v test_validate_search_input.py.

import unittest
from validate_search_input import build_request, validate_input


class ValidateInputTests(unittest.TestCase):
    def test_accepts_reviewed_inputs(self):
        cases = [
            {"q": "café, site:example.org", "gl": "fr", "hl": "fr"},
            {"q": "coffee", "location": "Paris, France", "start": 0},
            {"q": "coffee", "uule": "opaque-value"},
            {"q": "coffee", "tbm": "isch", "safe": "active", "device": "desktop"},
            {"q": "coffee", "nfpr": 1, "filter": 0},
            {"url": "https://www.google.com/search?q=coffee&gl=us&hl=en"},
            {"url": "https://google.fr/search?q=caf%C3%A9"},
        ]
        for original in cases:
            with self.subTest(original=original):
                result = validate_input(original)
                self.assertEqual(result, original)
                self.assertIsNot(result, original)
                self.assertEqual(build_request(original)["actor"], "scraper.google.search")

    def test_rejects_invalid_inputs(self):
        cases = [
            [], {1: "coffee"}, {}, {"q": " "}, {"q": None},
            {"q": "coffee", "langauge": "en"},
            {"url": "https://www.google.com/search?q=coffee", "gl": "us"},
            {"q": "coffee", "location": "Paris", "uule": "encoded"},
            {"q": "coffee", "start": -1}, {"q": "coffee", "start": "10"},
            {"q": "coffee", "start": True}, {"q": "coffee", "filter": False},
            {"q": "coffee", "nfpr": 2}, {"q": "coffee", "gl": "de"},
            {"q": "coffee", "hl": []}, {"q": "coffee", "safe": "blur"},
            {"q": "coffee", "device": "mobile"}, {"q": "coffee", "tbm": "nws"},
            {"url": "https://google.com.example.org/search?q=coffee"},
            {"url": "http://www.google.com/search?q=coffee"},
            {"url": "https://user@www.google.com/search?q=coffee"},
            {"url": "https://www.google.com:443/search?q=coffee"},
            {"url": "https://www.google.com/search?q=coffee&q=tea"},
            {"url": "https://www.google.com/search?q="},
            {"url": "https://www.google.com/search?q=coffee#section"},
            {"url": "https://www.google.com/images?q=coffee"},
            {"url": "https://www.google.com/search?q=coffee grinder"},
            {"url": "https://www.google.com/search?q=%ZZ"},
        ]
        for original in cases:
            with self.subTest(original=original):
                with self.assertRaises(ValueError):
                    validate_input(original)


if __name__ == "__main__":
    unittest.main()
Enter fullscreen mode Exit fullscreen mode

These tests check local behavior. Passing them does not establish that an API request will succeed, that a location resolves as intended, or that a returned module has the schema your application expects. Those are separate integration checks.

Put the Validator Before Authentication and Submission

The intended call sequence is straightforward: obtain the user's configuration, validate it, build the request body, and then pass that body to the authenticated client. The request workflow documents the POST endpoint and the x-api-token authentication header for that later step.

Keep credentials outside this input object. The validator reviews search configuration, while authentication belongs in the HTTP client. That separation also allows the validated body to be inspected without placing the API key in a configuration log.

After submission, response handling has its own responsibilities. HTTP 200 carries task data; HTTP 201 indicates a pending task with a taskId. Valid input does not make those states interchangeable. Save task state and inspect the returned data before applying a result mapping.

The application should also record the configuration it actually sent. Country, language, origin, and offset are useful context when a colleague later asks why two observations differ. URL-mode observations should retain the complete URL and their input-mode label.

Conclusion

Start with the input rules that can be checked locally: one mode, one location representation, reviewed parameter names, and explicit value types. Keep application restrictions separate from claims about the API. The resulting request body is easier to inspect, and configuration mistakes can be corrected before collection begins.

FAQ

Does this script call the API or require an API key?

No. It validates local data and prints request bodies. Authentication and submission happen in a separate client.

Why does it reject a documented parameter?

The allowlist covers this example's reviewed subset. Add other fields after checking their meaning and the application's needs.

Does accepting a location prove that it resolves correctly?

No. The local check establishes that it is nonempty text and does not conflict with uule. It does not perform location lookup or encoded-location validation.

Does URL mode enforce every parameter-mode restriction?

No. It checks the URL structure and query-key rules described above. Add an explicit policy for embedded query values if your application needs one.

Why does the example accept only image mode in tbm?

The sample supports regular web search when tbm is omitted and image search with isch. That is the scope of this application, rather than a claim about every search type the service may support.

Top comments (0)