An HTTP response has a status, headers, and a body. The browser uses all three to decide how to handle the result. PHP can send headers with header() and set status codes with http_response_code(), but those decisions must happen before output is sent. This lesson explains JSON content types, form redirects, download headers, and useful error codes. It also addresses two common bugs: a Location header followed by code that keeps executing, and headers that fail because an included file already emitted whitespace. You will learn to make response behavior explicit rather than relying on browser guesses.
Send headers before the body
Any echoed text, HTML outside PHP tags, byte-order mark, or debugging output can begin the response body. Once PHP sends the headers, later changes may trigger 'headers already sent'. Use headers_sent() to diagnose where output began, but fix the output ordering rather than simply adding buffering everywhere. Pure PHP library files commonly omit the closing ?> tag to avoid accidental trailing whitespace. A template should render after the handler has chosen its status and headers. Output buffering may postpone sending bytes, yet it is not a substitute for a clear response structure. Startup errors can also produce output before runtime code runs, so production display settings matter. Keep diagnostic messages in logs when building JSON or download responses.
Select status codes for their meaning
A 200 response means the requested operation succeeded; a 201 indicates creation when that is the API contract. A 204 response has no body. Use 400 for malformed requests, 401 when authentication is required under the relevant scheme, 403 for denied access, and 404 for an unavailable resource. A 405 response should identify supported methods using Allow. Many APIs use 422 for well-formed input that fails validation, while 409 suits a conflict such as a uniqueness rule. A 500 is an unexpected server failure, not a normal missing field. Choose one consistent API convention and document it. Status codes help clients act correctly; returning every outcome as 200 forces each client to guess what happened from text.
Redirect after form processing
A successful form POST often redirects to a GET confirmation page. This Post/Redirect/Get pattern prevents an ordinary browser refresh from resubmitting the form. A 303 Location response explicitly directs the client to retrieve the target with GET. Other redirect codes differ in permanence and method preservation: 307 and 308 preserve the method, which can be undesirable after a POST. Send a trusted relative or absolute target and terminate processing with exit or a response return boundary. header('Location: ...') does not automatically stop PHP. Code after it can still change data or disclose content. Prefer a fixed application route or an allowlisted destination instead of accepting an arbitrary next URL from the visitor.
Avoid open redirects and header confusion
A redirect parameter such as next=https://attacker.example can send users to a hostile site while appearing to begin on your trusted domain. Use known route identifiers mapped to internal URLs, or rigorously validate permitted destinations. Be cautious with scheme-relative URLs beginning // and encoded forms of dangerous values. Do not insert raw request text into a header value, including filenames or custom metadata. Header injection and malformed responses can result from inappropriate line breaks or unsafe formatting, even though PHP rejects many invalid header strings. Your application policy should still choose controlled values. Treat security headers as an application design decision: a Content-Security-Policy must match the actual page's script and resource needs rather than being copied blindly from a tutorial.
Describe JSON and downloads explicitly
For JSON, send application/json with a suitable charset and encode the data without notices or extra HTML. For a CSV download, use text/csv and an attachment Content-Disposition with a controlled filename. X-Content-Type-Options: nosniff reduces some content-type guessing in supported browsers. Keep private downloads behind authorization and choose cache controls that do not permit shared storage of sensitive responses. Do not derive Content-Length from a compressed or transformed body without accounting for the actual bytes sent. Streaming a file requires checking its existence and access before headers commit a successful response. If a failure occurs halfway through a stream, you may no longer be able to change the status. Design operational logging for that possibility.
Understand caching and repeated header calls
header() can replace a prior header of the same type or add another depending on its replace argument. Some headers may validly repeat, while others should be set once under a clear response policy. Cache-Control defines whether a response can be stored and reused; private and no-store express different policies. A public static asset can have a long cache lifetime with a versioned filename, while an authenticated account page needs more careful treatment. Do not use cache directives as an access-control substitute. Test responses with an HTTP client that shows status and headers rather than inspecting only rendered HTML. Proxies and web servers may add or alter headers, so verify the deployed response when infrastructure behavior matters.
Practice task and worked solution
Create an endpoint that returns a JSON validation error with status 422, then a separate successful create response with 201. Set the content type before the body and ensure only encoded JSON appears. The solution uses an error envelope with field messages rather than exposing exceptions. Add method handling with 405 and Allow. Test with a tool that shows headers, including one invalid request and one unsupported method. Next, create a fixed-route 303 form redirect and verify a refresh retrieves the confirmation page rather than resubmitting data. Introduce a whitespace-emitting include in a test environment to identify the headers-already-sent cause, then remove that output.
Continue learning
Combine forms, REST APIs, and security for a complete handler flow.
The PHP header reference explains replacement behavior and the requirement to send headers before output.
HTTP Headers, Redirects and Response Codes Examples
Each example shows the code, the output it produces and what each part does.
Redirect safely after a validated POST
This handler assumes the form operation already passed validation, authorization, and CSRF checks. It redirects to a controlled route instead of using user-provided next URLs.
A successful POST receives HTTP 303 and Location: /account/profile-saved. The browser then makes a GET request to that route. A GET sent to this handler receives 405 with Allow: POST. Route availability depends on your application; this example does not create or deploy an account service.
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Allow: POST');
exit('Use POST.');
}
// Save validated application data here.
header('Cache-Control: no-store');
header('Location: /account/profile-saved', true, 303);
exit;Successful POST: HTTP 303, Location: /account/profile-saved
GET: HTTP 405, Allow: POSTHTTP Headers, Redirects and Response Codes FAQ
Does Location stop execution?
No. header('Location: ...') only adds a header to the response; PHP keeps running the rest of the script. Explicitly terminate or return the response straight after it, usually with exit;, so no later code runs or sends output.
Which redirect follows a successful POST with GET?
A 303 See Other communicates that behaviour clearly: the browser must load the new location with a GET request. That's exactly what the Post/Redirect/Get pattern needs, so refreshing the result page doesn't resubmit the form. Send it with header('Location: /thanks', true, 303);.
Can buffering solve every header problem?
Output buffering can defer output so a header sent later still works, but it hides the underlying ordering mistake and has limits, such as buffers flushed early. Clean response ordering is better: decide on status codes, headers and redirects before any HTML or whitespace is sent.
Should errors always return 200?
No. Use meaningful status codes under a consistent contract: 404 for missing resources, 422 or 400 for invalid input, 401 or 403 for access problems and 500 for server faults. Clients, browsers, monitoring tools and search engines all rely on the status code, not just the page content.
HTTP Headers, Redirects and Response Codes Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
A redirect accepts arbitrary next URLs. What fixes the design?
HTTP Headers, Redirects and Response Codes Quiz
2 questions to check what you learned.
What should follow a Location header when processing must stop?
- An assumption that PHP stops automatically
- More database writes
- A sleep loop
- exit or the framework's response-return boundary
Which response supports Post/Redirect/Get explicitly?
- 303 with a controlled Location target
- 204 with a large body
- 500 after every save
- 307 when GET conversion is required