Skip to content
Advanced

5 JSON API Redirect Tests Before Production

·9 min read
5 JSON API Redirect Tests Before Production - Advanced guide from JSON Prettifier

Quick answer: Before a JSON API redirect reaches production, test whether the next hop keeps or changes the method, whether the body can be replayed, which credentials cross the boundary, how every Location value resolves, and whether chains terminate at the expected JSON endpoint. A successful final status alone can hide a dangerous intermediate request.

Redirects are often introduced outside application code by a gateway, trailing-slash rule, hostname migration, or HTTP-to-HTTPS upgrade. That makes them easy to miss in unit tests. The five checks below treat every hop as part of the API contract and use small fixtures that can run against the deployed route.

1. Does each redirect status produce the intended method and body?

Send the same JSON write through 301, 302, 303, 307, and 308 fixtures, then record the method and body received at the destination. A 303 deliberately becomes GET, while 307 and 308 preserve both method and body. Historical 301 and 302 behavior varies enough that non-GET APIs should not depend on it.

The current MDN redirect guide separates the ambiguous historic statuses from the explicit ones. A 303 See Other tells the client to retrieve the target using GET. A 307 Temporary Redirect and 308 Permanent Redirect retain the original method and body. RFC 9110 is the primary HTTP semantics specification.

Build a destination that echoes what it actually received:

import http from "node:http";

const server = http.createServer(async (req, res) => {
  if (req.url.startsWith("/move/")) {
    const status = Number(req.url.split("/").pop());
    res.writeHead(status, { Location: "/inspect" }).end();
    return;
  }

  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({
    method: req.method,
    body: Buffer.concat(chunks).toString("utf8")
  }));
});

Drive it with the production client, not only a generic HTTP tester:

const response = await fetch(`${base}/move/307`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ orderId: "ord-42" })
});

const received = await response.json();
console.assert(received.method === "POST");
console.assert(received.body === '{"orderId":"ord-42"}');

Repeat the assertion table for every status your infrastructure can emit. A GET-only smoke test cannot prove write behavior, and checking only the final 200 hides whether a POST was rewritten on the way.

RedirectExpected follow-up for a POSTSuitable API intent
301Client behavior may rewrite to GETPermanent move where non-GET compatibility is not assumed
302Client behavior may rewrite to GETTemporary move where non-GET compatibility is not assumed
303GET without the original bodyRetrieve a result after processing a write
307Same method and bodyTemporary method-preserving move
308Same method and bodyPermanent method-preserving move

2. Can the redirected request safely replay its JSON body?

Verify that a preserved body is replayable, unchanged, and safe to execute twice if transport failure obscures the result. Buffered JSON may replay cleanly, while a one-shot stream can fail during a 307 or 308 follow-up. A method-preserving redirect does not make a non-idempotent operation safe from duplicate processing.

Two separate problems live here. First, the client needs another readable copy of the body. Browser fetch() can replay a body it owns, but server libraries may reject or require special handling for non-rewindable streams. Test the actual upload path, including payload sizes that trigger streaming rather than buffering.

Second, the destination might receive the same logical write more than once. A client can send the request, lose the response, and retry at either the original or redirected URL. Redirect support therefore belongs beside an idempotency policy, not in place of one. The JSON API idempotency mistakes guide covers duplicate-order prevention with server-side keys and stored outcomes.

Use an operation ID in the test payload and count durable effects:

{
  "operationId": "redirect-test-001",
  "orderId": "ord-42",
  "action": "confirm"
}

Run a successful redirect, a connection interruption after the destination commits, and a retry. The acceptance test should define whether the second attempt returns the stored outcome, reports a documented conflict, or creates another effect. “The body arrived intact” is necessary but insufficient.

Also verify Content-Type and Content-Length or transfer framing at the second hop. A JSON body copied without its media type can reach the route but bypass the JSON parser. If a proxy decompresses or transforms a body, compare the application-visible bytes rather than assuming the wire representation stays identical.

3. Which credentials and sensitive fields reach the redirect target?

Redirect a credentialed request to the same origin, another subdomain, another port, and an unrelated origin under test control. Record Authorization, cookies, custom API-key headers, tenant headers, and the JSON body at every destination. Do not assume that stripping one standard header prevents other secrets or payload data from crossing origins.

The Fetch Standard defines detailed redirect processing, including removing certain credential headers when origin changes. Real client libraries add their own policies and have had security defects. A 2026 GitHub-reviewed advisory documented cross-domain forwarding of custom authentication headers in follow-redirects, even though standard authorization and cookie headers were stripped. That is evidence to test dependencies, not a claim that every client behaves the same.

Design the assertion as an allowlist:

const permittedAtCrossOrigin = new Set([
  "accept",
  "content-type",
  "user-agent"
]);

for (const name of capturedHeaderNames) {
  if (!permittedAtCrossOrigin.has(name)) {
    throw new Error(`unexpected redirected header: ${name}`);
  }
}

The exact safe set depends on the operation. A 307 or 308 can preserve the JSON body, which may contain personal data or credentials even when headers are cleaned. If a sensitive API should never change origin, configure the client to reject redirects and treat any 3xx response as a routing failure.

In browser code, cross-origin redirect behavior also meets CORS, cookie SameSite rules, and response filtering. In backend code, there is no browser CORS enforcement. Run both paths when both exist. The JSON API CORS checklist explains why a command-line success cannot establish browser behavior.

4. Does every Location value resolve to the intended trusted URL?

Test absolute URLs, root-relative paths, path-relative references, query strings, encoded segments, alternate ports, and scheme changes. Resolve Location against the current request URL at every hop, then enforce the destination policy on the result. String-prefix checks are not sufficient protection against confusing hosts, credentials, encodings, or path normalization.

The Location field can contain a relative reference. MDN's Location header reference explains which redirect statuses use the initiating method and which switch to GET. For URL background, Wikipedia's URL redirection article also notes that unvalidated redirect targets can become open-redirect vulnerabilities; RFC 9110 remains the authority for HTTP behavior.

Use the platform URL parser rather than concatenating strings:

const current = new URL("https://api.example.com/v1/orders/42");
const next = new URL("../orders/43?view=compact", current);

console.assert(next.href ===
  "https://api.example.com/v1/orders/43?view=compact");
console.assert(next.protocol === "https:");
console.assert(next.hostname === "api.example.com");

Test rejection too: an HTTP downgrade, unexpected port, non-HTTP scheme, user-info section, or destination outside the allowlist should stop before the client sends a second request. Reapply the policy after every hop because a trusted first destination can redirect again.

Keep fragments out of server-side assumptions. URL fragments are handled by clients and are not sent in the HTTP request target. Preserve query parameters only when the redirect contract requires them; blindly carrying a signed query to a different destination can disclose it or invalidate the signature.

5. Does the chain terminate at the expected JSON response?

Set a small redirect limit and test a self-loop, a two-URL loop, an overlong chain, a missing Location field, and a final HTML response. On success, assert the final URL, status, Content-Type, and JSON shape. A client reporting 200 or response.redirected alone does not prove that it reached the intended API.

Automatic following makes a broken chain look deceptively healthy. In standards-based fetch, response.redirected shows that at least one redirect happened and response.url identifies the final URL. It does not expose a complete portable hop history. When redirects are unexpected, the Fetch Standard recommends filtering them through the API by using redirect: "error" rather than discovering them only after a possibly unsafe request.

const response = await fetch(apiUrl, { redirect: "error" });

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

const type = response.headers.get("content-type") || "";
if (!type.toLowerCase().includes("application/json")) {
  throw new Error(`expected JSON, received ${type || "no type"}`);
}

const data = await response.json();

For routes where redirects are intentional, capture hops in a controlled integration proxy or configure the client for manual handling where the runtime makes those responses visible. Store only sanitized traces: status, method, origin, path pattern, and selected safe headers. Do not log bearer tokens or full personal payloads.

The final JSON still needs validation. Use the JSON Viewer to inspect a non-sensitive captured body, but do not confuse a well-formed document with a correct redirect chain. If the final page begins with HTML, follow the unexpected-token guide to distinguish a login page, proxy error, and wrong route.

What should a JSON API redirect test report?

Report every hop's status, resolved URL, outgoing method, body presence, safe header names, and timing, followed by the final status, media type, and payload assertion. Record the client and version under test. The same redirect can behave differently across browser fetch, server runtimes, curl, mobile SDKs, proxies, and cached permanent routes.

A release matrix should include:

  1. every 3xx status the gateway or framework can return;
  2. GET plus each write method used by the API;
  3. buffered and streamed request bodies where supported;
  4. same-origin and controlled cross-origin destinations;
  5. relative, absolute, invalid, and disallowed Location values;
  6. loops, excessive hops, missing headers, and non-JSON final bodies.

Keep the fixture deterministic. Use local test servers or reserved internal hosts, never an arbitrary public redirect service with real credentials. Assert destination-side observations rather than only client-side promises. The most valuable failure message says “307 replayed POST body to an unapproved origin,” not merely “redirect test failed.”

These five tests cover redirect mechanics, not whether a redirect is the best API design. Stable machine endpoints are easier to secure and operate. When a move is unavoidable, publish the target, lifetime, credential policy, and client expectations, then keep the old-path test until supported clients have migrated.

Run the matrix again when a CDN, ingress controller, authentication layer, framework router, or client dependency changes. Any one of those components can introduce a hop or alter redirect handling without changing the endpoint's application code. Production-like integration tests are especially important for trailing-slash normalization and hostname upgrades because local development often bypasses the proxy that creates them.

Frequently Asked Questions

Which HTTP redirect preserves a POST request body?
HTTP 307 and 308 redirects preserve the request method and body: 307 is temporary and 308 is permanent. A 303 intentionally changes the follow-up request to GET, while 301 and 302 have historical method-rewrite behavior. Test the exact production client instead of inferring behavior from a browser GET.
Why does a redirected JSON POST arrive as GET?
A client may rewrite POST to GET when following 301 or 302, and 303 explicitly directs the client to retrieve another resource with GET. The redirected request then has no original JSON body. Use the intended endpoint directly or return 307 or 308 when preserving the operation is actually required.
Does fetch automatically follow API redirects?
Browser and standards-based fetch implementations normally use the follow redirect mode unless configured otherwise. The returned Response can expose redirected and the final URL, but intermediate responses may not be available. Use redirect error for endpoints that must never redirect, and test browser and server runtimes separately because their security boundaries differ.
Are authorization headers forwarded through redirects?
Do not assume one universal rule. Clients commonly remove Authorization on a cross-origin redirect, but custom credential headers have leaked through redirect implementations. Same-origin redirects may retain sensitive headers and 307 or 308 can replay the body. Restrict destinations, inspect the tested client, and avoid automatic redirects for sensitive writes.
Can JSON Prettifier test an HTTP redirect chain?
No. JSON Prettifier can format or validate a captured JSON body, and the JSON Viewer can help inspect the final payload structure. Redirect testing requires an HTTP client or browser network trace that records each status, Location value, method, destination, headers, body behavior, and the final response type.

Inspect the final body after tracing the redirect

Use the JSON Viewer to inspect a non-sensitive final payload after your HTTP client confirms the redirect chain and response type.

Open the JSON Viewer