How To Fix Decoding ReqBin JavaScript Error Gatekeepers Blocking Your PHP API

I remember the first time I pasted a perfectly innocent API URL into ReqBin and boom the response was a full-blown HTML page scolding me for not having JavaScript enabled. The PHP script on my server was only three lines long, yet here I was staring at a message that clearly didn’t come from PHP at all. I’ll walk you through what’s really happening, show you how to prove it to yourself, and then turn that tiny health-check script into a miniature practice API you can keep extending.

The Mystery Page Isn’t Yours It’s the Gatekeeper

Before any HTTP request reaches your PHP interpreter it usually passes through at least one protective layer: a CDN, WAF, reverse proxy, or even a single-page-app framework. If that layer isn’t satisfied it swaps your real response with a “JavaScript required” splash. Here’s what that looks like in practice:

Where the page really comes fromWhat it’s doingWhy you only see it in ReqBin
Cloudflare / Imunify / SucuriRuns a bot-challenge (“Turnstile”, JavaScript cookie, etc.)ReqBin fires a bare-bones cURL request—no cookies, no fancy headers—so the WAF assumes it’s a scraper
Reverse-proxy / load balancerInjects a maintenance or anti-bot page whenever a rule failsYour normal browser sailed through the rule earlier; ReqBin didn’t
Static-site framework (Next.js, Angular…)Serves the framework’s index.html, which politely says the site needs JSYour PHP sits in /api, but the tester hit the root path and got the SPA splash instead

Key point: your PHP never ran. Something in front intercepted the request and served that HTML instead.

Quick Fixes I Reach For First

  1. Hit the exact endpoint in ReqBin
    Include /api.php, /v1/ping, /api/health whatever the real path is.
  2. Disable or bypass the challenge on API routes
    Cloudflare → Security → WAF → Skip Turnstile for /api/*.
  3. Allow-list trusted origins/IPs if you must keep the WAF.
  4. Return real JSON so testers negotiate the right MIME type:
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 'API_CONNECTED']);

Try those, reload ReqBin, and nine times out of ten the mystery page disappears.

From “Hello World” to a Mini Practice API

Once the basics work, I like to turn that single echo statement into a safe sandbox for experiments CORS, auth, database hits, and structured error replies. Below is my go-to starter file. Copy it to api/index.php, tweak the constants in config.php, and you’re good:

<?php
declare(strict_types=1);

/* ---------- 1. HEADERS ---------- */
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Content-Type: application/json; charset=utf-8');

/* Stop CORS pre-flight right here */
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}

/* ---------- 2. CONFIG (never commit secrets) ---------- */
require_once __DIR__ . '/config.php'; // defines DB_DSN, DB_USER, DB_PASS, API_USER, API_PASS

/* ---------- 3. HELPER ---------- */
function jsonOut(array $data, int $code = 200): void
{
http_response_code($code);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;
}

/* ---------- 4. BASIC AUTH ---------- */
if (
!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) ||
$_SERVER['PHP_AUTH_USER'] !== API_USER ||
$_SERVER['PHP_AUTH_PW'] !== API_PASS
) {
header('WWW-Authenticate: Basic realm="DemoAPI"');
jsonOut(['error' => 'Unauthorized'], 401);
}

/* ---------- 5. ROUTER ---------- */
$path = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
$method = $_SERVER['REQUEST_METHOD'];

/* GET /api/ping */
if ($path === 'api/ping' && $method === 'GET') {
jsonOut(['message' => 'pong', 'epoch' => time()]);
}

/* GET /api/users[?q=alice] */
if ($path === 'api/users' && $method === 'GET') {
$q = $_GET['q'] ?? '';
try {
$pdo = new PDO(DB_DSN, DB_USER, DB_PASS, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare(
'SELECT id, name, email FROM users WHERE name LIKE :q LIMIT 20'
);
$stmt->execute([':q' => "%$q%"]);
jsonOut(['count' => $stmt->rowCount(), 'data' => $stmt->fetchAll()]);
} catch (Throwable $e) {
jsonOut(['error' => $e->getMessage()], 500);
}
}

/* ---------- 6. FALLBACK ---------- */
jsonOut(['error' => 'Route not found'], 404);

What You Can Practice With This File

TaskHow I test it
CORS pre-flightOPTIONS /api/ping → expect HTTP 204
Health checkGET /api/ping{ "message": "pong" }
Filtered lookupGET /api/users?q=ali
Auth failureWrong credentials → 401 Unauthorized
DB exceptionBreak the SQL → see structured 500 JSON
WAF behaviorcurl with/without User-Agent to watch Cloudflare flip

Ideas for Further Practice

  • POST /api/users – validate request JSON, insert a row, echo the new ID.
  • Redis rate-limit – reply with 429 Too Many Requests plus X-RateLimit-Remaining.
  • Request logging – append method, route, IP, microsecond timing to access.log.

Each add-on teaches a real-world skill while keeping the surface area small.

Final Thought

Every mysterious “JavaScript required” screen is a reminder that HTTP requests travel through a lot of layers before they hit your code. Once you understand who those gatekeepers are and why they act the way they do you’ll fix test-bench surprises in seconds. Even better, you can turn the humble “API CONNECTED” echo into a living playground where you practise auth, CORS, SQL, rate-limiting, and everything else an API does in production. That way, the next oddball response that lands in ReqBin becomes just another bug you’ve already rehearsed fixing.

Related blog posts