Navigation
 Startseite
 Fachbücher
 Anzeigenmarkt
 Forum
 Webmaster News
 Script Newsletter
 Push Meldungen
 Kontakt
 Script Installation
 Php
 Php Tutorials
 Lernpfade
 Webhoster Vergleich
 Impressum

Community-Bereich
 kostenlos Registrieren
 Anmelden
 Benutzerliste

Script Datenbank
 Script Archiv
 Script Top 20
 Screenshots
 Testberichte

Suche
 

Unsere Php Scripts
 Counter Script
 Umfrage Script
 Bilder Upload Script
 Terminverwaltung
 Simple PHP Forum
 RSS Grabber

Tools und Generatoren
 .htpasswd Generator
 md5 Generator
 base64 Generator
 Markdown to HTML
 Colorpicker
 Unix timestamp Tool
 Unit Test Generator
 TLD Liste
 Webkatalog‑Verzeichnis

Webspace-Mieten.d...
Webhosting mit Plesk aus Erfurt: Vier...
https://www.Webspace-Mieten.de
Hosterplus.de
Bekommen Sie Speicherplatz (Webspace), Domains...
https://www.Hosterplus.de
 
 
 

PHP stream_context_create(): Header, POST und Timeout

Sie befinden sich: Home > Php Tutorial > PHP stream_context_create()...

PHP stream_context_create(): Header, POST und Timeout
Veröffentlicht am:
12.11.2026
Hits / Besucher:
14
Sprache:
  Deutsch
Tutorial Art:
eigenes
Eingetragen von:
Merkliste:
 
Beschreibung

Ein Shop soll jede Bestellung als JSON an den Versanddienst melden, mit Token im Header und mit Zeitlimit. cURL ist auf dem Server nicht freigeschaltet, also bekommt file_get_contents() einen Stream-Kontext mit. Viele Varianten davon scheitern still: Der Token fehlt, ein POST kommt als leerer GET an, eine Fehlerseite wandert in den Cache. Einen einfachen POST zeigt das Tutorial zu file_get_contents(), eine Header-Abfrage mit HEAD und Zeitlimit das zu get_headers(). Hier geht es um den Kontext selbst: welcher Schlüssel welchen Wrapper steuert, was PHP stream_context_create() wirklich an den Server schickt und wo ein Kontext still das Falsche tut.

Illustration zum Tutorial: PHP stream_context_create(): Header, POST und Timeout

Gemessen wurde mit PHP 8.4 unter Windows und Linux, Versionsfragen mit 8.1 bis 8.5. Der Testserver auf 127.0.0.1:8765 schickt jede Anfrage Byte für Byte zurück.

Was PHP stream_context_create() an den Server schickt

Ein Stream-Kontext ist ein Bündel von Optionen, das eine Stream-Funktion beim Öffnen mitbekommt. PHP stream_context_create() selbst öffnet nichts. Erst fopen(), file_get_contents(), file() oder get_headers() lesen die Optionen, und zwar nur die, die zu ihrem Wrapper passen. Ohne Kontext kommt beim Server erstaunlich wenig an:

<?php

$url = 'http://127.0.0.1:8765/echo';
echo file_get_contents($url);
/* GET /echo HTTP/1.1
Host: 127.0.0.1:8765
Connection: close */

Kein User-Agent, kein Accept. Manche APIs weisen solche Anfragen ab. Bei einem POST ergänzt PHP die Länge des Rumpfs selbst. Beim Typ rät es dagegen und sagt das auch:

<?php

$context = stream_context_create([
'http' => [
'method' => 'POST',
'content' => 'name=Mia&menge=3',
],
]);
echo file_get_contents($url, false, $context);
/* Notice: file_get_contents(): Content-type not
specified assuming
application/x-www-form-urlencoded

POST /echo HTTP/1.1
Host: 127.0.0.1:8765
Connection: close
Content-Length: 16
Content-Type: application/x-www-form-urlencoded

name=Mia&menge=3 */

Wer JSON schickt und den Typ vergisst, bekommt einen Formulartyp untergeschoben. Das Diagramm zeichnet nach, wie ein Kontext aus PHP stream_context_create() zum passenden Wrapper findet:

flowchart TD
    A[stream_context_create] --> B[Kontext]
    B --> C{Schema der Adresse}
    C -->|http oder https| D[Optionen unter http]
    C -->|https| E[zusätzlich ssl]
    C -->|ftp| F[Optionen unter ftp]
    C -->|Datei| G[keine Wirkung]
    D --> H[Anfrage an den Server]
    E --> H
    F --> H
    B --> I[unbekannte Schlüssel]
    I --> J[still ignoriert]

Die Signatur im Original: Array rein, Resource raus

PHP stream_context_create() nimmt zwei optionale Arrays entgegen:

stream_context_create(

?array $options = null,
?array $params = null
): resource

/* options - an associative array of arrays in
the format
$arr['wrapper']['option'] = $value
params - an associative array; the usual
key is notification, a callable
for progress events
return - a stream context resource of type
stream-context
A stream is a resource object which can be
read from or written to. The stream
functions share a common set of options:
pass the context to fopen(), file(),
file_get_contents() or get_headers() to use
a custom method, header or timeout and to
send an HTTP request with content. Unknown
options are ignored, a wrong format throws
a ValueError. */

PHP stream_context_create() liefert eine Resource vom Typ stream-context, auch in PHP 8.5 noch kein Objekt. Einen Fehlerwert gibt es nicht. Nur eine falsche Form wie ['http' => 'x'] wirft ValueError: Options should have the form ["wrappername"]["optionname"] = $value. Alles andere nimmt die Funktion kommentarlos an, auch Tippfehler. Dort beginnt der erste stille Fehler.

Welcher Schlüssel in PHP stream_context_create() welchen Wrapper steuert

Lea baut einen Abruf für eine Partner-API unter https:// und schreibt die Optionen deshalb unter den Schlüssel https. Beim Server kommt ein GET statt des POST an, ohne Warnung. Der Schlüssel benennt nicht das Schema der Adresse, sondern die Gruppe der Optionen:

Schlüssel gilt für typische Optionen
http http:// und https:// method, header, content, user_agent, timeout, ignore_errors, follow_location, proxy
ssl https://, ftps://, ssl://, tls:// verify_peer, cafile, peer_name
ftp ftp:// und ftps:// overwrite, resume_pos
socket alle Socket-Verbindungen bindto, tcp_nodelay

Gegen einen TLS-Testserver, dessen Zertifikat als vertrauenswürdig hinterlegt war, kam Folgendes an:

<?php

$url = 'https://localhost:8766/auftrag';
$falsch = stream_context_create([
'https' => ['method' => 'POST'],
]);
$richtig = stream_context_create([
'http' => ['method' => 'POST'],
]);
foreach ([$falsch, $richtig] as $context) {
$r = file_get_contents($url, false, $context);
echo strtok($r, "\r\n"), PHP_EOL;
}
/* GET /auftrag HTTP/1.1
POST /auftrag HTTP/1.1 */

Genauso verpufft ein Tippfehler wie 'timout' => 5, den stream_context_get_options() später unverändert anzeigt. Auch Optionen, die nicht zum Ziel passen, bleiben wirkungslos, etwa ssl-Einstellungen bei einer http-Adresse. Weil PHP stream_context_create() nichts davon meldet, lohnt eine kleine Prüfung beim Bauen des Kontexts:

<?php

function pruefe_optionen(array $optionen): array
{
$bekannt = ['http', 'ssl', 'ftp', 'socket'];
$fremd = array_diff(
array_keys($optionen),
$bekannt
);
if ($fremd !== []) {
throw new LogicException(
'Unbekannt: ' . implode(', ', $fremd)
);
}
return $optionen;
}

try {
pruefe_optionen(['https' => ['method' => 'POST']]);
} catch (LogicException $e) {
echo $e->getMessage();
}
/* Unbekannt: https */

Header und JSON mit file_get_contents(): warum der Token fehlt

Svenja bindet einen Versanddienstleister an. Die API verlangt JSON und einen Token im Header X-Token, antwortet aber mit 401. Die Header stammen aus einem älteren Beispiel mit \r\n zwischen den Zeilen, und beim Umbau auf ein Array ist jedes Zeilenende mitgewandert:

<?php

$json = json_encode(['paket' => 1]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => [
"Content-Type: application/json\r\n",
"X-Token: geheim\r\n",
],
'content' => $json,
],
]);
echo file_get_contents($url, false, $context);
/* POST /echo HTTP/1.1
Host: 127.0.0.1:8765
Connection: close
Content-Length: 11
Content-Type: application/json

X-Token: geheim

{"paket":1} */

Die Option header von PHP stream_context_create() nimmt einen String oder ein Array, dessen Elemente PHP mit \r\n verbindet. Endet ein Element selbst so, entsteht eine Leerzeile, und die beendet in HTTP den Header-Block. Der Token steht danach im Rumpf. Ein leeres Element wirkt genauso, ein abschließendes \r\n im String-Format entfernt PHP dagegen. Am sichersten führt man die Header als Paare:

<?php

function kopfzeilen(array $paare): array
{
$zeilen = [];
foreach ($paare as $name => $wert) {
$zeilen[] = trim($name) . ': '
. trim($wert);
}
return $zeilen;
}

$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => kopfzeilen([
'Content-Type' => 'application/json',
'X-Token' => "geheim\r\n",
]),
'content' => $json,
],
]);
echo file_get_contents($url, false, $context);
/* POST /echo HTTP/1.1
Host: 127.0.0.1:8765
Connection: close
Content-Length: 11
Content-Type: application/json
X-Token: geheim

{"paket":1} */

Die Paare gehören aber nicht direkt in die Option. Ein assoziatives Array wie ['Content-Type' => 'application/json', 'X-Token' => 'geheim'] im header von PHP stream_context_create() verliert die Namen: Im Test kamen nur application/json und geheim als eigene Zeilen an, dazu der Notice zum fehlenden Typ und ein untergeschobenes Content-Type: application/x-www-form-urlencoded. Ein Array als content verschwindet sogar ohne jede Meldung, der POST geht ohne Content-Length und ohne Rumpf hinaus. Formulardaten aus einem Array baut http_build_query(), JSON baut json_encode():

<?php

$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: '
. 'application/x-www-form-urlencoded',
'content' => http_build_query(
['name' => 'Mia', 'menge' => 3]
),
],
]);
echo file_get_contents($url, false, $context);
/* POST /echo HTTP/1.1
Host: 127.0.0.1:8765
Connection: close
Content-Length: 16
Content-Type: application/x-www-form-urlencoded

name=Mia&menge=3 */

Beim User-Agent schlägt ein Eintrag im header die Option user_agent, und die Option schlägt die php.ini. Zugangsdaten in der Adresse wie http://mia:geheim@... setzt PHP selbst in einen Authorization-Header um.

Einen Header, den jeder Browser sendet, sollte man nicht übernehmen: Accept-Encoding: gzip. Der Wrapper entpackt nicht, und json_decode() scheitert an den komprimierten Bytes:

<?php

$context = stream_context_create([
'http' => ['header' => 'Accept-Encoding: gzip'],
]);
$roh = file_get_contents(
'http://127.0.0.1:8765/wetter.json',
false,
$context
);
var_dump(json_decode($roh));
/* NULL */
echo json_last_error_msg(), PHP_EOL;
/* Control character error, possibly
incorrectly encoded */
echo gzdecode($roh);
/* {"ort":"Bremen","grad":12.5} */

Entweder fehlt der Header, oder die Antwort läuft durch gzdecode(). Mit compress.zlib:// vor der Adresse entpackt PHP schon beim Lesen.

Weiterleitungen: warum aus einem POST ein leerer GET wird

Andrea meldet Zahlungen per POST an das System eines Partners. Nach dessen Umzug leitet die alte Adresse mit 301 auf die neue um. Ihr Skript meldet weiter Erfolg, beim Partner kommen aber nur leere Anfragen an:

<?php

$alteUrl = 'http://127.0.0.1:8765/alt';
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => '{"status":"bezahlt"}',
],
]);
echo file_get_contents($alteUrl, false, $context);
/* GET /neu HTTP/1.1
Host: 127.0.0.1:8765
Connection: close */

Ein Kontext aus PHP stream_context_create() folgt Weiterleitungen von sich aus, wechselt aber auf GET und lässt Rumpf und Content-Type weg. Die Antwort des Ziels kommt mit 200, also sieht alles nach Erfolg aus. Entscheidend ist der Statuscode, gemessen in PHP 8.1, 8.3, 8.4 und 8.5:

Antwort des Servers zweite Anfrage Rumpf
301, 302, 303 GET fehlt
307, 308 POST wird mit Content-Type erneut gesendet

Bei schreibenden Anfragen gehört die Weiterleitung deshalb abgeschaltet und als Fehler behandelt:

<?php

$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => '{"status":"bezahlt"}',
'follow_location' => 0,
'ignore_errors' => true,
],
]);
$body = file_get_contents($alteUrl, false, $context);
$kopf = http_get_last_response_headers();
echo $kopf[0], PHP_EOL;
/* HTTP/1.1 301 Moved Permanently */
echo implode(preg_grep('/^Location:/i', $kopf));
/* Location: /neu */

Jetzt geht keine leere Anfrage mehr hinaus, und das Skript sieht die 301 samt Ziel. http_get_last_response_headers() gibt es ab PHP 8.4, eine Variante für ältere Versionen folgt weiter unten. Vorbeugen lässt sich mit der endgültigen Adresse.

timeout ist keine Gesamtfrist

Murat lädt nachts Preislisten von Lieferanten und hat 'timeout' => 5 gesetzt, trotzdem läuft der Cronjob bei einem Lieferanten minutenlang. Ein Testserver, der 100.000 Bytes in zehn Portionen mit je 50 ms Pause schickt, zeigt den Grund:

<?php

$context = stream_context_create([
'http' => ['timeout' => 0.2],
]);
$start = microtime(true);
$daten = file_get_contents(
'http://127.0.0.1:8765/preisliste',
false,
$context
);
printf('%d Bytes in %.2f s', strlen($daten),
microtime(true) - $start);
/* 100000 Bytes in 0.50 s */

Obwohl die Übertragung länger als 0,2 Sekunden dauert, kommt alles an, unter Windows nach gut 0,6 s. Die Option timeout in PHP stream_context_create() begrenzt die Wartezeit auf das nächste Datenpaket, nicht die Gesamtdauer. Ohne Kontext gilt default_socket_timeout aus der php.ini, meist 60 Sekunden. Auf eine Sekunde gesetzt, brach eine Anfrage an einen schweigenden Server nach 1,00 s mit Failed to open stream: HTTP request failed! ab. Eine echte Frist misst das Skript selbst:

<?php

function lade_mit_frist(
string $url,
float $frist
): string {
$ende = microtime(true) + $frist;
$context = stream_context_create([
'http' => ['timeout' => $frist],
]);
$h = @fopen($url, 'r', false, $context);
if ($h === false) {
throw new RuntimeException('keine Antwort');
}
$daten = '';
while (!feof($h)) {
if (microtime(true) > $ende) {
fclose($h);
throw new RuntimeException(
'Frist abgelaufen'
);
}
$daten .= fread($h, 8192);
}
fclose($h);
return $daten;
}

try {
lade_mit_frist(
'http://127.0.0.1:8765/preisliste',
0.25
);
} catch (RuntimeException $e) {
echo $e->getMessage();
}
/* Frist abgelaufen */

Mit 0,25 Sekunden brach die Funktion nach 0,25 s ab (Windows 0,25 bis 0,31 s), mit zwei Sekunden kam die ganze Liste an. Gegen einen Server, der drei Sekunden schweigt, meldete sie bei einer Frist von einer Sekunde nach 1,00 s keine Antwort. Ein einzelnes fread() kann bis zum timeout warten, die Frist wird zwischen zwei Lesevorgängen geprüft.

Der Wert von timeout gilt auch für den Verbindungsaufbau, und das trifft Entwickler unter Windows. Dort versucht PHP bei localhost zuerst die IPv6-Adresse ::1. Lauscht der Dienst nur unter 127.0.0.1, vergehen rund zwei Sekunden, bis Windows aufgibt:

<?php

$context = stream_context_create([
'http' => ['timeout' => 0.5],
]);
$start = microtime(true);
$r = @file_get_contents(
'http://localhost:8765/echo', false, $context
);
printf('%s nach %.1f s', $r === false ? 'false'
: 'Antwort', microtime(true) - $start);
/* Windows: false nach 0.5 s
Linux: Antwort nach 0.0 s */

Ohne Zeitlimit kam die Antwort unter Windows erst nach gut zwei Sekunden. Mit 0,5 Sekunden endete die Anfrage im Test mit false. Die Meldung erscheint in der Sprache des Systems und begann im Test mit Failed to open stream: Ein Verbindungsversuch ist fehlgeschlagen, da die Gegenstelle nach einer bestimmten Zeitspanne nicht richtig reagiert hat. Abhilfe schafft 127.0.0.1 statt localhost oder ein Dienst, der auch auf ::1 lauscht.

Fehlerstatus prüfen, statt Fehlerseiten zu speichern

Tim zeigt auf der Vereinsseite das Wetter an und cacht die Antwort der API eine Stunde. Bei einem Fehler liefert file_get_contents() nur false und die Warnung Failed to open stream: HTTP request failed! HTTP/1.1 500 Internal Server Error. Um die Fehlermeldung der API zu sehen, schaltet er ignore_errors ein:

<?php

$apiUrl = 'http://127.0.0.1:8765/fehler';
$context = stream_context_create([
'http' => ['ignore_errors' => true],
]);
$wetter = file_get_contents($apiUrl, false, $context);
var_dump($wetter);
/* string(20) "{"fehler":"Wartung"}" */

Seit Tim ignore_errors an PHP stream_context_create() übergibt, kommt bei einem Fehler kein false mehr, sondern die Fehlerseite, ohne Warnung, und sein Cache speichert sie als Wetterdaten. Prüfen muss jetzt das eigene Skript, und zwar die letzte Statuszeile, denn nach einer Weiterleitung stehen mehrere Antworten hintereinander:

<?php

function letzter_status(array $kopf): int
{
$status = 0;
$muster = '#^HTTP/\S+ (\d{3})#';
foreach ($kopf as $zeile) {
if (preg_match($muster, $zeile, $t)) {
$status = (int) $t[1];
}
}
return $status;
}

function letzte_kopfzeilen(?array $lokal): array
{
if (function_exists(
'http_get_last_response_headers'
)) {
return http_get_last_response_headers()
?? [];
}
return $lokal ?? [];
}

$wetter = file_get_contents($apiUrl, false, $context);
$kopf = letzte_kopfzeilen(
$http_response_header ?? null
);
echo letzter_status($kopf);
/* 500 */

Liefert die Prüfung etwas anderes als 200, landet nichts im Cache. letzte_kopfzeilen() hat einen Grund, der erst mit PHP 8.5 sichtbar wird. Die Header stehen in $http_response_header, ab 8.4 auch in http_get_last_response_headers(). Nina setzt PHP stream_context_create() in einer Bibliothek für 8.1 bis 8.5 ein und verbindet beides per Versionsweiche. Unter 8.5 meldet ihre CI trotzdem Deprecated: The predefined locally scoped $http_response_header variable is deprecated, call http_get_last_response_headers() instead.

Die Meldung entsteht schon beim Kompilieren, im Test auch für eine Funktion, die nie aufgerufen wurde. Eine Versionsweiche hilft also nicht. Sie kommt bei jedem direkten Lesen der Variablen, aber nicht bei isset() und nicht beim Null-Koaleszenz-Operator. Deshalb übergibt der Aufruf oben $http_response_header ?? null: Unter PHP 8.1 kommen die Header aus der Variablen, unter 8.5 bleibt es still. Welche Wege es zu den Headern sonst gibt, zeigt das Tutorial zu stream_get_meta_data().

Proxy, HTTPS und FTP: Streams jenseits von HTTP

Claudia arbeitet im Intranet eines Energieversorgers, Anfragen nach außen laufen über den Firmenproxy mit Anmeldung. PHP stream_context_create() bekommt dessen Adresse mit tcp://. Entscheidend ist request_fulluri: Ohne die Option schickt PHP dem Proxy nur den Pfad (GET /status HTTP/1.1), und die meisten Proxys wissen dann nicht, wohin. Im Test spielte der Testserver den Proxy:

<?php

$context = stream_context_create([
'http' => [
'proxy' => 'tcp://127.0.0.1:8765',
'request_fulluri' => true,
'header' => 'Proxy-Authorization: Basic '
. base64_encode('mia:geheim'),
],
]);
echo file_get_contents(
'http://intranet.example/status',
false,
$context
);
/* GET http://intranet.example/status HTTP/1.1
Host: intranet.example
Connection: close
Proxy-Authorization: Basic bWlhOmdlaGVpbQ== */

Für https kommen die TLS-Optionen unter ssl dazu. Ein Server mit Zertifikat aus der firmeneigenen Zertifizierungsstelle scheitert ohne weitere Angaben mit drei Meldungen, die erste lautet SSL operation failed with code 1. OpenSSL Error messages: error:0A000086:SSL routines::certificate verify failed, danach folgen Failed to enable crypto und Failed to open stream: operation failed. Mit cafile klappt die Verbindung. Wer den Server über seine IP-Adresse anspricht, scheitert erneut mit Peer certificate CN=`localhost' did not match expected CN=`127.0.0.1'. Dafür gibt es peer_name:

<?php

$context = stream_context_create([
'ssl' => [
'cafile' => __DIR__ . '/firma-ca.pem',
'peer_name' => 'localhost',
],
]);
$antwort = file_get_contents(
'https://127.0.0.1:8766/status',
false,
$context
);
echo strtok($antwort, "\r\n");
/* GET /status HTTP/1.1 */

verify_peer auf false hilft hier nicht. Es schaltet nur die Prüfung der Zertifizierungsstelle ab, den Namen vergleicht PHP weiter: Über die IP-Adresse kam im Test wieder die CN-Meldung, gefolgt von Failed to enable crypto und Failed to open stream: operation failed. Erst mit zusätzlich verify_peer_name auf false verschwand sie, und dann nimmt PHP jedes Zertifikat an. peer_name dagegen behält beide Prüfungen und sagt PHP nur, welcher Name im Zertifikat stehen muss. Einzelheiten zur Zertifikatsprüfung stehen im Tutorial zu stream_socket_client().

Auch für FTP nimmt PHP stream_context_create() Optionen entgegen. Dirk lädt jede Nacht eine Preisliste auf einen Marktplatz. Ohne Option weigert sich PHP, eine vorhandene Datei zu ersetzen: Remote file already exists and overwrite context option not specified. Mit overwrite klappte es, bis nach einem Verbindungsabbruch die Liste ganz fehlte. Das Protokoll eines Test-FTP-Servers zeigt, warum:

<?php

$csv = "artikel;preis\nA-100;9.90\n";
$context = stream_context_create([
'ftp' => ['overwrite' => true],
]);
file_put_contents(
'ftp://mia:geheim@127.0.0.1:2121/preise.csv',
$csv,
0,
$context
);
/* Befehle am Server:
USER mia, PASS geheim, TYPE I,
SIZE /preise.csv, DELE /preise.csv,
EPSV, STOR /preise.csv */

Mit overwrite löscht PHP die alte Datei, bevor die neue geschrieben ist. Reißt die Verbindung nach DELE ab, ist keine Datei mehr da. Sicherer ist ein temporärer Name, denn rename() funktioniert auch mit ftp-Adressen:

<?php

$basis = 'ftp://mia:geheim@127.0.0.1:2121';
file_put_contents("$basis/preise.tmp", $csv);
rename("$basis/preise.tmp", "$basis/preise.csv");
/* Befehle am Server nach der Anmeldung:
TYPE I, SIZE /preise.tmp, EPSV,
STOR /preise.tmp, dann neue Verbindung:
RNFR /preise.tmp, RNTO /preise.csv */

Ob RNTO ein vorhandenes Ziel ersetzt, entscheidet der Server. Für abgebrochene Downloads gibt es resume_pos, PHP schickt dann vor dem Abruf ein REST.

Der Standardkontext und seine Nebenwirkungen

Statt den Proxy überall einzutragen, setzt Claudia ihn einmal mit stream_context_set_default(). Er gilt für alle Aufrufe ohne eigenen Kontext, im Test für file_get_contents(), get_headers() und simplexml_load_file():

<?php

stream_context_set_default([
'http' => [
'user_agent' => 'Intranet/1.0',
'timeout' => 10,
],
]);
echo file_get_contents('http://127.0.0.1:8765/a');
/* GET /a HTTP/1.1
Host: 127.0.0.1:8765
Connection: close
User-Agent: Intranet/1.0 */

$eigener = stream_context_create([
'http' => ['header' => 'Accept: text/plain'],
]);
echo file_get_contents(
'http://127.0.0.1:8765/b', false, $eigener
);
/* GET /b HTTP/1.1
Host: 127.0.0.1:8765
Connection: close
Accept: text/plain */

Der zweite Aufruf zeigt die Grenze: Was mit PHP stream_context_create() entsteht, erbt nichts vom Standard. Gemeinsame Einstellungen gehören deshalb in eine Funktion, die alle eigenen Kontexte baut. Für libxml gibt es libxml_set_streams_context(). Vorsicht bei stream_context_get_default(): Mit einem Argument ändert sie den Standard dauerhaft, statt eine Kopie zu liefern.

<?php

stream_context_get_default([
'http' => ['timeout' => 5],
]);
$h = fopen('http://127.0.0.1:8765/c', 'r');
print_r(stream_context_get_options($h)['http']);
/* Array
(
[user_agent] => Intranet/1.0
[timeout] => 5
) */

Der vermeintliche Lesezugriff hat das Zeitlimit aller späteren Aufrufe ohne Kontext auf fünf Sekunden gesetzt. get_headers() sendet ohne Kontext übrigens einen GET, für reine Header setzt man 'method' => 'HEAD'.

Einen mit PHP stream_context_create() erzeugten Kontext ändert stream_context_set_option() mit vier Argumenten, wirksam ab der nächsten Anfrage. Die Form mit einem Array als zweitem Argument meldet seit PHP 8.4 Deprecated: Calling stream_context_set_option() with 2 arguments is deprecated, use stream_context_set_options() instead. stream_context_set_options() gibt es ab PHP 8.3.

Fortschritt anzeigen mit dem notification-Callback

Nina zeigt beim Download großer Feeds einen Fortschritt in der Konsole. Dafür nimmt PHP stream_context_create() im zweiten Parameter einen Callback entgegen, den PHP bei jedem Ereignis der Übertragung aufruft:

<?php

function fortschritt(
int $code, int $stufe, ?string $text,
int $nr, int $bytes, int $max
): void {
static $gemeldet = -1;
if ($code === STREAM_NOTIFY_FILE_SIZE_IS) {
echo "Größe: $max Bytes\n";
} elseif ($code === STREAM_NOTIFY_PROGRESS
&& $max > 0) {
$prozent = intdiv($bytes * 100, $max);
if (intdiv($prozent, 25) > $gemeldet) {
$gemeldet = intdiv($prozent, 25);
echo "$prozent %\n";
}
}
}

$context = stream_context_create(
[],
['notification' => 'fortschritt']
);
$daten = file_get_contents(
'http://127.0.0.1:8765/preisliste',
false,
$context
);
/* Größe: 100000 Bytes
0 %
28 %
50 %
78 %
100 % */

Die Ereignisse kamen im Test immer in derselben Reihenfolge: STREAM_NOTIFY_CONNECT, MIME_TYPE_IS, FILE_SIZE_IS mit der Größe aus Content-Length, PROGRESS und am Ende COMPLETED. Bei einer Weiterleitung kommt REDIRECTED dazu, bei 404 oder 401 FAILURE mit der Statuszeile. Wer den Callback nicht an PHP stream_context_create() übergibt, sondern erst nach fopen() mit stream_context_set_params() setzt, bekommt keine Fortschrittsmeldung mehr, im Test höchstens noch ein COMPLETED mit null Bytes. Ein Tippfehler im Funktionsnamen fällt erst bei der Anfrage auf: Invalid callback fortschrit, function "fortschrit" not found or invalid function name.

Praxis: ein API-Client mit PHP stream_context_create()

Zurück zum Shop vom Anfang. Die Funktion meldet eine Bestellung an den Versanddienst und setzt um, was die Abschnitte oben zu PHP stream_context_create() gemessen haben: Header als Paare, eigener User-Agent, Zeitlimit, keine Weiterleitung bei POST, eigene Statusprüfung und Header ohne Deprecation. kopfzeilen(), letzter_status() und letzte_kopfzeilen() stammen aus den Beispielen oben.

<?php

function api_post(
string $url,
array $daten,
string $token,
float $frist = 5.0
): array {
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => kopfzeilen([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Authorization' => "Bearer $token",
]),
'user_agent' => 'Bestellsync/1.0',
'content' => json_encode($daten),
'timeout' => $frist,
'ignore_errors' => true,
'follow_location' => 0,
],
]);
$body = @file_get_contents($url, false, $context);
if ($body === false) {
$f = error_get_last()['message'] ?? '?';
throw new RuntimeException($f);
}
$kopf = letzte_kopfzeilen(
$http_response_header ?? null
);
$status = letzter_status($kopf);
if ($status < 200 || $status >= 300) {
throw new RuntimeException(
"HTTP $status: " . substr($body, 0, 80)
);
}
return json_decode(
$body, true, 512, JSON_THROW_ON_ERROR
);
}

try {
$antwort = api_post(
'http://127.0.0.1:8765/bestellung',
['id' => 7, 'menge' => 2],
'geheim'
);
echo 'Auftrag ', $antwort['auftrag'];
} catch (Exception $e) {
error_log('Versand: ' . $e->getMessage());
}
/* Auftrag 4711 */

Das @ unterdrückt nur die Ausgabe der Warnung, error_get_last() holt ihren Text in die Exception. JsonException erbt nicht von RuntimeException, deshalb fängt der Aufruf Exception. Gegen fünf Antworten des Testservers verhielt sich die Funktion auf beiden Systemen und unter PHP 8.1 und 8.5 gleich:

Antwort des Servers Ergebnis von api_post()
200 mit JSON Array mit auftrag 4711
500 mit Fehlerseite RuntimeException: HTTP 500: {"fehler":"Wartung"}
302 auf eine neue Adresse RuntimeException: HTTP 302, keine zweite Anfrage
keine Antwort innerhalb der Frist RuntimeException, Text endet auf HTTP request failed!
200 mit HTML statt JSON JsonException: Syntax error

Keiner der fünf Fälle endet als stilles Weiterarbeiten mit falschen Daten.

Wann cURL die bessere Wahl ist

In drei Fällen kommt man mit PHP stream_context_create() nicht weiter, dort ist cURL die bessere Wahl:

allow_url_fopen ist abgeschaltet

Hat der Hoster die Einstellung abgeschaltet, scheitert jeder Kontext mit http:// wrapper is disabled in the server configuration by allow_url_fopen=0 und no suitable wrapper could be found. Eine Prüfung vorab erkennt den Fall, bevor die erste Anfrage scheitert.

<?php

$erlaubt = filter_var(
ini_get('allow_url_fopen'),
FILTER_VALIDATE_BOOL
);
var_dump($erlaubt);
/* bool(true), mit allow_url_fopen=0 bool(false) */
Der Dienst verlangt HTTP/2

Mit 'protocol_version' => 2.0 schreibt PHP nur GET /echo HTTP/2.0 in eine gewöhnliche Textanfrage. Echtes HTTP/2 spricht der Wrapper nicht, die Push-Dienste von Apple setzen es aber voraus. Chunked-Antworten unter dem Standard HTTP/1.1 setzt PHP korrekt zusammen.

Parallele Anfragen und harte Fristen

file_get_contents() wartet die komplette Antwort ab, bevor die nächste Anfrage beginnt. Zwanzig Lieferanten parallel abzufragen, gelingt mit cURL am einfachsten über curl_multi_exec(), und auch Gesamtfrist und gzip erledigt cURL über Optionen.

Fazit

PHP stream_context_create() macht aus file_get_contents() einen brauchbaren HTTP-Client, wenn man weiß, was der Kontext sendet. HTTP-Optionen stehen immer unter http, unbekannte Schlüssel bleiben ohne Wirkung und ohne Meldung, und Header gehören ohne eigenes Zeilenende ins Array. Bei schreibenden Anfragen schaltet man die Weiterleitung ab, weil ein POST 301, 302 und 303 nicht übersteht.

Die Option timeout von PHP stream_context_create() begrenzt nur die Wartezeit auf das nächste Paket. Zu ignore_errors gehört immer eine Statusprüfung, und $http_response_header liest man nur noch mit ??. Beim FTP-Upload ist ein temporärer Name sicherer als overwrite, und für HTTP/2 oder parallele Anfragen bleibt cURL das bessere Werkzeug.

 

Tags:

 

 

Kommentare (0)

Noch keine Kommentare. Sei der Erste!

Melde dich an, um einen Kommentar zu schreiben.
Bücherregal mit drei Büchern: 'PHP 4 - Grundlagen und Profiwissen' von Hanser Verlag, 'Webdesign in a Nutshell' von O'Reilly Verlag, und 'Webgestaltung' von Galileo Computing.