In PHP, how do you send a JSON POST with a timeout through file_get_contents() and stream_context_create(), and read the response status?
answer
- options under the 'http' key
- method, header, content, timeout
- ignore_errors to keep error bodies
- http_get_last_response_headers() since 8.4
- $http_response_header deprecated in 8.5
basics
~20 sBuild a context with stream_context_create(['http' => ['method' => 'POST', 'header' => …, 'content' => $json, 'timeout' => 5.0, 'ignore_errors' => true]]), pass it to file_get_contents(), then read the status line from http_get_last_response_headers(), which replaces the deprecated $http_response_header.
solid answer
~50 sA stream context carries per-wrapper options, and for HTTP they live under the `'http'` key: `method`, `header` (a string of lines joined with `\r\n` or an array of lines), `content` for the body, `timeout` as a float of seconds (default `default_socket_timeout`, 60), and `ignore_errors` so a 4xx or 5xx response still returns its body instead of `false`. Pass it as the third argument: `file_get_contents($url, false, $ctx)`. As of PHP 8.5, the response headers come from `http_get_last_response_headers()`, added in 8.4; the magic local `$http_response_header` is deprecated in 8.5. The first element of each response is the status line, such as `HTTP/1.1 201 Created`, and redirects add every hop's headers to the same array, so parse the last status line. With `fopen()` the same headers are in `stream_get_meta_data($h)['wrapper_data']`. The `timeout` bounds each wait for data, not the total request time.
go deeper
Recall that HTTP options go under the 'http' key and that the context is the third argument of file_get_contents().
Explain method, header, content, timeout and ignore_errors, and how redirects put several status lines into the headers array.
Show you read the final status with http_get_last_response_headers(), keep error bodies for logging, and know the timeout bounds each wait rather than the whole call.
Decide when the built-in wrapper is acceptable and when outbound calls need a client with connect timeouts, retries, circuit breaking and metrics.
## Stream contexts A **stream context** is a resource created by `stream_context_create(?array $options, ?array $params)` that carries options for one or more wrappers. It is passed as the context argument of `fopen()`, `file_get_contents()`, `file_put_contents()`, `copy()` and friends. Options are grouped by wrapper name: `'http'` for HTTP and HTTPS, `'ssl'` for TLS settings, `'ftp'`, `'zlib'` and so on. `stream_context_set_default()` changes the default context used when none is passed. ## The HTTP options that matter | Option | Type | Default | Purpose | |---|---|---|---| | `method` | string | `GET` | HTTP method | | `header` | string or array | none | extra request headers; string lines joined with `\r\n` | | `content` | string | none | request body | | `timeout` | float | `default_socket_timeout` (60) | seconds to wait for data | | `ignore_errors` | bool | `false` | return the body even on 4xx/5xx | | `follow_location` | int | follow 3xx | set `0` to stop following redirects | | `max_redirects` | int | `20` | redirect limit | | `protocol_version` | float | `1.1` since PHP 8.0 | HTTP version | | `user_agent` | string | the `user_agent` ini value | `User-Agent` when `header` does not set one | ## A JSON POST with a timeout ```php <?php declare(strict_types=1); $ctx = stream_context_create(['http' => [ 'method' => 'POST', 'header' => ['Content-Type: application/json', 'Accept: application/json'], 'content' => json_encode(['invoice' => 'INV-2026-0912'], JSON_THROW_ON_ERROR), 'timeout' => 5.0, 'ignore_errors' => true, ]]); $body = file_get_contents('https://billing.internal/api/receipts', false, $ctx); $headers = http_get_last_response_headers() ?? []; $statusLines = array_values(array_filter($headers, fn (string $h): bool => str_starts_with($h, 'HTTP/'))); preg_match('{^HTTP/\S+ (\d{3})}', end($statusLines) ?: '', $m); $status = (int) ($m[1] ?? 0); if ($body === false || $status < 200 || $status >= 300) { throw new RuntimeException("receipt API failed with status {$status}"); } ``` What each part does: 1. **`header` as an array** avoids forgetting the `\r\n` separators a string needs. 2. **`ignore_errors`** keeps the error body, so you can log the API's message; without it a 4xx/5xx returns `false` and only a warning mentions the status. 3. **`http_get_last_response_headers()`** returns the header lines of the last HTTP request made through the wrapper, or `null` if there was none. 4. **The last status line wins.** When redirects are followed, the array contains every hop's status line and headers in order, so the first element may be a `301`. ## Version notes - `http_get_last_response_headers()` and `http_clear_last_response_headers()` were added in **PHP 8.4**. - The magic local variable **`$http_response_header`**, which PHP used to create in the calling scope, is **deprecated in PHP 8.5**. Code that reads it should switch to the function. - The default `protocol_version` became `1.1` in PHP 8.0; before that the wrapper spoke HTTP/1.0. ## Reading the response with fopen() For large responses, open the URL as a stream instead of loading it whole: 1. `$h = fopen($url, 'r', false, $ctx)` sends the request and reads the headers. 2. `stream_get_meta_data($h)['wrapper_data']` holds the header lines, redirects included. 3. `fgets()`, `fread()` or `stream_copy_to_stream()` then read the body in chunks. 4. `fclose($h)` ends the connection. This keeps memory flat for big downloads, while `file_get_contents()` returns the whole body as one string. ## Pitfalls - **Timeout semantics.** `timeout` bounds each wait on the socket. A server that trickles a byte every few seconds can keep the request alive far longer than the number suggests. - **No retries or backoff.** The wrapper sends once. Anything more needs your own loop or a real HTTP client. - **Content-Length.** The wrapper adds a `Content-Length` for `content` when you do not; setting a wrong one by hand breaks the request. - **Redirects on POST.** Following a redirect can change the request; set `follow_location` to `0` when an API should never redirect. - **URL wrappers disabled.** If `allow_url_fopen` is Off, none of this runs; the call returns `false` with a warning. For anything beyond simple calls, cURL or an HTTP client library gives connection timeouts, retries, streaming and proper error objects.
- Why might the first element of http_get_last_response_headers() be a 301 when the request succeeded?With `follow_location` enabled, the wrapper follows redirects and appends each response's status line and headers to the same array. The first element belongs to the first hop, so a successful request that was redirected starts with `HTTP/1.1 301 …`. Parse the last line that starts with `HTTP/` to get the final status.
- How do you get the response headers when you open the URL with fopen() instead?Call `stream_get_meta_data($h)`: its `wrapper_data` element holds the same header lines for the open HTTP stream. Then read the body with `stream_get_contents($h)` or `fgets()` and close the handle. `http_get_last_response_headers()` also works after the request.
- What changed about $http_response_header in PHP 8.5?It is deprecated. PHP used to create that local variable automatically in the scope that called the HTTP wrapper. Since PHP 8.4 `http_get_last_response_headers()` returns the same data explicitly, and in 8.5 relying on the magic variable raises a deprecation, so new code should use the function.
saying these in an interview costs you the question
- The timeout option caps the total duration of the whole request.
- A 500 response still returns its body from file_get_contents() by default.
- The first header line is always the final response status.
- $http_response_header is the recommended way to read headers in PHP 8.5.
- The header option must be a single string; arrays are ignored.