Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

When JavaScript sends JSON.stringify(data), PHP does not place that JSON in $_POST. Read the raw request with php://input, decode it, and validate the result. If php://input is empty, the problem is not JSON decoding alone: verify the browser request URL, payload, redirects, and the PHP/Apache route receiving it.

Minimal working JSON example

Use an explicit request content type and return a consistent JSON response:

async function sendData() {
  const response = await fetch("/test.php", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json"
    },
    body: JSON.stringify({
      cows: "When the cows come home",
      dogs: "Who let the dogs out?"
    })
  });

  const text = await response.text(); // useful while diagnosing
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
  console.log(JSON.parse(text));
}
<?php
header('Content-Type: application/json; charset=utf-8');

$raw = file_get_contents('php://input');

if ($raw === false || $raw === '') {
    http_response_code(400);
    echo json_encode(['error' => 'Request body is empty']);
    exit;
}

try {
    $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    http_response_code(400);
    echo json_encode(['error' => 'Request body is not valid JSON']);
    exit;
}

if (!is_array($data)) {
    http_response_code(400);
    echo json_encode(['error' => 'Expected a JSON object']);
    exit;
}

$cows = $data['cows'] ?? null;
$dogs = $data['dogs'] ?? null;

if (!is_string($cows) || !is_string($dogs)) {
    http_response_code(422);
    echo json_encode(['error' => 'cows and dogs must be strings']);
    exit;
}

echo json_encode([
    'cows' => str_replace('cows', 'alpacas', $cows),
    'dogs' => str_replace('dogs', 'cats', $dogs)
]);

PHP’s php://input stream contains the raw request body. JSON_THROW_ON_ERROR makes malformed input explicit instead of leaving you to interpret a silent null.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why $_POST is empty for JSON

PHP automatically populates $_POST for application/x-www-form-urlencoded and multipart/form-data. A raw application/json body is not one of those form encodings, so use:

$raw = file_get_contents('php://input');
$data = json_decode($raw, true);

That does not mean PHP cannot read JSON; it means JSON and form data use different APIs. The PHP documentation explains this distinction in its $_POST reference.

First determine what actually arrived

During development, temporarily inspect the request before decoding:

<?php
header('Content-Type: text/plain; charset=utf-8');
$raw = file_get_contents('php://input');
var_dump([
  'method' => $_SERVER['REQUEST_METHOD'] ?? null,
  'content_type' => $_SERVER['CONTENT_TYPE'] ?? null,
  'content_length' => $_SERVER['CONTENT_LENGTH'] ?? null,
  'post' => $_POST,
  'raw_body' => $raw
]);

Do not return raw bodies or enable verbose errors in production; log redacted diagnostics on the server instead.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Observation Likely explanation
$_POST is empty but raw body contains JSON Normal for JSON; decode php://input.
Raw body is empty No body was sent, the wrong endpoint ran, a redirect/proxy intervened, or the request was rejected before this PHP process.
Raw body is nonempty but decoding fails Malformed JSON or an encoding problem.
Response is HTML Often a 404, redirect, PHP warning/fatal error, rewrite, or server error—not a JSON response.
Request is absent from Network tools JavaScript failed before fetch(), or the browser blocked the request.

Inspect the browser Network request

  1. Open developer tools and select Network.
  2. Reload the page or trigger the action.
  3. Open the PHP request and check its final URL, method, status, redirects, request headers, payload, response headers, and response body.

You should see a POST request with Content-Type: application/json and a payload similar to {"cows":"When the cows come home","dogs":"Who let the dogs out?"}. Logging the Response object in the console cannot prove that this request body was sent.

Check the URL, not just the filename

fetch("./test.php") is resolved relative to the document URL, not the JavaScript file’s directory. A page in another directory, Apache Alias, virtual host, rewrite rule, HTTP-to-HTTPS redirect, or different hostname can send the request to a different script. A successful status code does not prove that the expected PHP file executed.

Confirm the final URL in Network tools and check web-server and PHP logs. Also verify that the response is generated by PHP rather than being a static file or an HTML error page. The SitePoint discussion that inspired this problem was closed without a verified root cause; its reported Debian update timing should not be treated as proof that Debian, TLS, or Apache caused the failure.

Separate browser issues from server issues with curl

curl -i 
  -X POST 
  -H 'Content-Type: application/json' 
  -H 'Accept: application/json' 
  --data '{"cows":"When the cows come home","dogs":"Who let the dogs out?"}' 
  https://example.test/test.php

If curl works but fetch() does not, investigate URL resolution, redirects, CORS, credentials, and JavaScript execution. If both produce an empty body, investigate the endpoint, PHP handler, Apache configuration, proxy, and logs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Use a form format when you want $_POST

For simple flat fields, URL encoding is often easier:

const body = new URLSearchParams({
  cows: "When the cows come home",
  dogs: "Who let the dogs out?"
});

fetch("/test.php", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" },
  body
});
<?php
$cows = $_POST['cows'] ?? '';
$dogs = $_POST['dogs'] ?? '';

For files or multipart form fields, use FormData and read $_POST and $_FILES:

const form = new FormData();
form.append("cows", "When the cows come home");
form.append("dogs", "Who let the dogs out?");

fetch("/test.php", { method: "POST", body: form });

Do not manually set Content-Type: multipart/form-data with FormData. The browser must add the boundary. PHP’s file-upload documentation covers this format. Also note that php://input has special limitations for multipart requests; use the parsed form variables instead.

Headers, redirects, CORS, and sessions

  • Content-Type describes the request body. Accept describes the response format preferred by the client; it does not make PHP parse JSON. See MDN’s Content-Type reference.
  • Inspect redirect chains. Host canonicalization or HTTPS redirects can expose an unexpected endpoint.
  • For cross-origin requests, application/json commonly triggers an OPTIONS preflight. The server must answer it with appropriate CORS headers; otherwise the POST may never reach PHP.
  • Use credentials: "include" only when cross-origin cookies or sessions are required, together with compatible cookie and CORS settings.

Read and validate the body once

The request body is a stream. Store the result of one file_get_contents('php://input') call rather than assuming repeated reads will independently provide the same data. Validate required keys and types after decoding; JSON syntax alone does not validate business rules.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Production checklist

  • Confirm the fetch call runs and appears in Network tools.
  • Verify the final URL, method, payload, status, redirects, and request content type.
  • Use php://input for JSON; use $_POST/$_FILES for form encodings.
  • Distinguish an empty body from invalid JSON with explicit checks or JSON_THROW_ON_ERROR.
  • Return Content-Type: application/json and appropriate 4xx/5xx status codes.
  • Use response.text() while diagnosing HTML errors, then switch to response.json() once responses are reliable.
  • Limit request sizes, authenticate sensitive endpoints, apply CSRF protection where applicable, and avoid exposing raw input or PHP errors.
  • Escape returned values when inserting them into HTML.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.