skip to content

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?

level: middleimportance: should knowfreq 35%

answer

  1. options under the 'http' key
  2. method, header, content, timeout
  3. ignore_errors to keep error bodies
  4. http_get_last_response_headers() since 8.4
  5. $http_response_header deprecated in 8.5

basics

~20 s

Build 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 s

A 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

for a junior

Recall that HTTP options go under the 'http' key and that the context is the third argument of file_get_contents().

for a middle

Explain method, header, content, timeout and ignore_errors, and how redirects put several status lines into the headers array.

for a senior

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.

for a principal

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.