http clients and apis

Stream-Based HTTP Requests

PHP can make basic HTTP requests with streams, usually through file_get_contents() and stream_context_create(). This is useful for small scripts and for understanding the raw pieces of an HTTP request.

Streams are not the best tool for every integration. Production API clients often need stronger timeout handling, retries, logging, authentication helpers, testing support, and clearer errors than raw streams provide.

Create a stream context

The context tells PHP how to make the request.

PHP example
<?php

declare(strict_types=1);

$context = stream_context_create([
    'http' => [
        'method' => 'GET',
        'header' => "Accept: application/json\r\n",
        'timeout' => 3,
    ],
]);

echo 'Stream context ready for GET request' . PHP_EOL;

// Prints:
// Stream context ready for GET request

In a real request, you pass the context to file_get_contents($url, false, $context).

Build headers carefully

The stream wrapper expects HTTP headers as a string with line breaks.

PHP example
<?php

declare(strict_types=1);

function headerLines(array $headers): string
{
    $lines = [];

    foreach ($headers as $name => $value) {
        $lines[] = $name . ': ' . $value;
    }

    return implode("\r\n", $lines) . "\r\n";
}

echo headerLines([
    'Accept' => 'application/json',
    'User-Agent' => 'ExampleClient/1.0',
]);

// Prints:
// Accept: application/json
// User-Agent: ExampleClient/1.0

Do not put untrusted user input into header names or raw header values.

Send a JSON body

For a POST request, encode the body and set the Content-Type header.

PHP example
<?php

declare(strict_types=1);

$payload = ['name' => 'Keyboard', 'price' => 99.99];
$body = json_encode($payload, JSON_THROW_ON_ERROR);

$contextOptions = [
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json\r\nAccept: application/json\r\n",
        'content' => $body,
        'timeout' => 5,
    ],
];

echo $contextOptions['http']['method'] . ' ' . $contextOptions['http']['content'] . PHP_EOL;

// Prints:
// POST {"name":"Keyboard","price":99.99}

This prepares the request. It does not call the network in the example.

Read response metadata

After a stream request, PHP may populate $http_response_header in the local scope. It contains the response status line and headers.

PHP example
<?php

declare(strict_types=1);

$responseHeaders = [
    'HTTP/1.1 200 OK',
    'Content-Type: application/json',
];

$statusLine = $responseHeaders[0] ?? '';
preg_match('#HTTP/\S+\s+(\d{3})#', $statusLine, $matches);

echo 'Status: ' . ($matches[1] ?? 'unknown') . PHP_EOL;

// Prints:
// Status: 200

If you need robust response objects, redirect handling, middleware, or test doubles, a library such as Guzzle or Symfony HttpClient is usually a better choice.

Error handling limitations

file_get_contents() can return false for network or stream errors. HTTP error responses can also be awkward depending on context options, status codes, and wrapper behaviour.

For learning, streams reveal the basic mechanics. For important third-party API integrations, prefer a dedicated HTTP client with explicit exceptions, response objects, timeout options, and better diagnostics.

When streams are reasonable

Streams can be fine for:

  • small internal scripts
  • quick health checks
  • simple one-off integrations
  • examples where avoiding dependencies is useful

Use a fuller HTTP client when the code needs authentication, retries, observability, testing, request signing, proxy support, or production reliability.

What to check in a project

Check that stream requests set a timeout. The default can leave requests hanging too long.

Check that errors are handled when file_get_contents() returns false.

Check that JSON bodies use json_encode() and the correct Content-Type.

Check that credentials are not logged or built into URLs.

Check whether a stream request has grown complex enough to move to cURL, Guzzle, Symfony HttpClient, or a framework client.

What you should be able to do

After this lesson, you should be able to create a stream context, set method, headers, timeout, and body, understand where response headers appear, and recognise when raw streams are too limited for production API work.

Practice

Task: Prepare Stream Request Options

Write a small PHP script that prepares stream context options for a JSON API request.

Requirements

  • Use declare(strict_types=1);.
  • Create a function that accepts method, headers, timeout, and optional payload.
  • Build the http options array expected by stream_context_create().
  • Encode the payload as JSON when one is provided.
  • Include a GET case with no body.
  • Include a POST case with a JSON body.
  • Print enough output to show the method, timeout, and body behaviour.

Check Your Work

Run the script and confirm the GET has no content value while the POST does.

Show solution

This solution prepares stream options without making a network call.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array<string, string> $headers
 * @param array<string, mixed>|null $payload
 * @return array{http: array<string, mixed>}
 */
function streamRequestOptions(string $method, array $headers, int $timeout, ?array $payload = null): array
{
    $headerLines = [];

    foreach ($headers as $name => $value) {
        $headerLines[] = $name . ': ' . $value;
    }

    $options = [
        'http' => [
            'method' => strtoupper($method),
            'header' => implode("\r\n", $headerLines) . "\r\n",
            'timeout' => $timeout,
        ],
    ];

    if ($payload !== null) {
        $options['http']['content'] = json_encode($payload, JSON_THROW_ON_ERROR);
    }

    return $options;
}

$get = streamRequestOptions('GET', ['Accept' => 'application/json'], 3);
$post = streamRequestOptions('POST', [
    'Accept' => 'application/json',
    'Content-Type' => 'application/json',
], 5, ['name' => 'Keyboard']);

echo $get['http']['method'] . ' timeout ' . $get['http']['timeout'] . PHP_EOL;
echo $post['http']['method'] . ' body ' . $post['http']['content'] . PHP_EOL;

// Prints:
// GET timeout 3
// POST body {"name":"Keyboard"}

Why This Works

The GET case proves a simple read request can be configured without a body. The POST case proves the JSON payload is encoded and attached as request content.