Fetch-Transport
Der standardmäßige fetch() in Phasis läuft über PHPs ext-curl – CURLOPT_FOLLOWLOCATION=false (die Weiterleitungsschleife der Spezifikation läuft in der Phasis-Umgebung), wobei CURLOPT_PROGRESSFUNCTION das AbortSignal abfragt. Für die meisten Anwendungsfälle beim Einbetten reicht das aus.
Für alles andere – Proxys, benutzerdefinierte DNS, den vorhandenen HTTP-Client der Host-App, Netzwerk-Sandboxing und Mocking in Tests – lässt sich der Transport austauschen.
Schnittstelle
public function setFetchTransport(callable $transport): void
Deine aufrufbare Funktion erhält:
function (array $request, ?\Phasis\Value\JsObject $signal): array
$request ist ein assoziatives Array:
| Schlüssel | Typ | Hinweise |
|---|---|---|
method |
string |
Bereits in Großbuchstaben (GET, POST, …) |
url |
string |
Aufgelöste absolute URL nach URL-Parser und Weiterleitungskette |
headers |
list<list<string>> |
Paare aus [name, value]; Groß-/Kleinschreibung gemäß Normalisierung der Spezifikation; Einträge mit mehreren Werten sind separate Paare (Set-Cookie usw.) |
body |
string |
Rohdaten. Leere Zeichenfolge bei Methoden ohne Body. |
redirect |
string |
"follow" / "error" / "manual" (dein Transport sollte NICHT folgen – die Weiterleitungsschleife läuft in Phasis) |
timeout |
int |
Zeitbudget in Millisekunden. 0 = kein Limit. |
credentials |
string |
"omit" / "same-origin" / "include" – informativ; Cookies werden separat über setCookieJar() eingebunden. |
$signal ist null, wenn der Anfrage kein Signal mitgegeben wurde, andernfalls ein JsObject mit aborted und reason. Frage es bei langen Übertragungen regelmäßig ab.
Rückgabeformat:
return [
'status' => 200, // int
'statusText' => 'OK', // string
'headers' => [['content-type', 'text/plain'], ['x-custom', '1']],
'body' => '...response bytes...', // string
];
Wenn der Transport einen Fehler auslöst, fängt Phasis ihn ab und übersetzt ihn:
- Ein
Phasis\BuiltIn\Fetch\TransportExceptionmit$e->kind === "aborted"→fetchlöst eine Ablehnung mitAbortError DOMExceptionaus. kind === "timeout"→TypeErrormit der Meldung.kind === "network-error"(Standard) →TypeError.- Alles andere →
TypeErrormit der Fehlermeldung.
Beispiele
Guzzle
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
$client = new Client(['allow_redirects' => false]);
$engine->setFetchTransport(function (array $req, $signal) use ($client) {
try {
$r = $client->request($req['method'], $req['url'], [
'headers' => collect($req['headers'])->groupBy(0)->map->pluck(1)->all(),
'body' => $req['body'],
'timeout' => max(1, $req['timeout'] / 1000.0),
'http_errors' => false,
]);
} catch (GuzzleException $e) {
throw new \Phasis\BuiltIn\Fetch\TransportException($e->getMessage(), 'network-error');
}
$headers = [];
foreach ($r->getHeaders() as $name => $values) {
foreach ($values as $v) {
$headers[] = [$name, $v];
}
}
return [
'status' => $r->getStatusCode(),
'statusText' => $r->getReasonPhrase(),
'headers' => $headers,
'body' => (string) $r->getBody(),
];
});
Symfony HttpClient
use Symfony\Component\HttpClient\HttpClient;
$client = HttpClient::create(['max_redirects' => 0]);
$engine->setFetchTransport(function (array $req, $signal) use ($client) {
$headers = [];
foreach ($req['headers'] as [$name, $value]) {
$headers[$name][] = $value;
}
$r = $client->request($req['method'], $req['url'], [
'headers' => $headers,
'body' => $req['body'],
'timeout' => $req['timeout'] / 1000.0,
'max_redirects'=> 0,
]);
return [
'status' => $r->getStatusCode(),
'statusText' => '',
'headers' => collect($r->getHeaders(false))
->flatMap(fn ($values, $name) => array_map(fn ($v) => [$name, $v], $values))
->values()->all(),
'body' => $r->getContent(false),
];
});
Alles ablehnen
$engine->setFetchTransport(function () {
throw new \Phasis\BuiltIn\Fetch\TransportException(
'network access disabled in this sandbox',
'network-error',
);
});
Damit wird jeder Aufruf von fetch() mit einem TypeError abgelehnt. Nützlich, um von Nutzern bereitgestelltes JS einzuschränken.
Mock für Tests
$canned = [
'https://api.example.com/users' => ['status' => 200, 'body' => '{"users":[]}'],
'https://api.example.com/404' => ['status' => 404, 'body' => 'not found'],
];
$engine->setFetchTransport(function (array $req) use ($canned) {
$r = $canned[$req['url']] ?? ['status' => 500, 'body' => 'no mock'];
return [
'status' => $r['status'],
'statusText' => $r['status'] === 200 ? 'OK' : 'ERR',
'headers' => [['content-type', 'application/json']],
'body' => $r['body'],
];
});
Der Policy-Hook
Der Transport führt die Anfrage aus; der Policy-Hook entscheidet, ob sie überhaupt ausgeführt wird. Verwende ihn für ausschließlich auf HTTPS basierende Erzwingung, das Einfügen von Headern, das Umschreiben von URL, Allowlists oder die an einzelne Mandanten gebundene Zuordnung von Zugangsdaten.
public function setFetchPolicy(callable $hook): void
Hook-Signatur: function (JsObject $request): ?JsObject. Die Rückgabe von null erlaubt die Anfrage unverändert. Die Rückgabe eines Request schreibt sie um. Ein ausgelöster Fehler lehnt den Fetch mit diesem Fehler ab.
$allowedHosts = ['api.example.com', 'cdn.example.com'];
$engine->setFetchPolicy(function ($req) use ($allowedHosts) {
$url = parse_url($req->get('url')->value);
if (!in_array($url['host'] ?? '', $allowedHosts, true)) {
throw new \RuntimeException("host not allowed: " . ($url['host'] ?? ''));
}
// Inject internal-only auth header
$req->get('headers')->call('set', 'X-Internal-Token', $secret);
return null;
});
Der Policy-Hook wird vor dem Transport ausgeführt – selbst ein fehlerhafter oder nicht vertrauenswürdiger Transport kann ihn also nicht umgehen.
Zusammenfassung des Standardverhaltens
| Aspekt | Standard |
|---|---|
| Transport | CurlTransport (PHP ext-curl) |
| Weiterleitungen | Gemäß Spezifikation bis zu 20 folgen. redirect: "manual" gibt die Weiterleitungs-Response mit type: "opaqueredirect" zurück. |
| Timeout | 30 s Gesamtzeit, 10 s Verbindungsaufbau |
| Überprüfung von TLS | CURLOPT_SSL_VERIFYPEER = true (kein Opt-out – Überschreiben durch Ersetzen des Transports) |
| User-Agent | Phasis/0.1 wird nur eingefügt, wenn nicht gesetzt |
| Cookies | Werden nicht gespeichert. Verwende setCookieJar(), um sie zu aktivieren. |
| Policy-Hook | Keiner. Alle Anfragen sind erlaubt, sofern sie nicht ausdrücklich abgelehnt werden. |
| AbortSignal | Wird während der Übertragung über eine Abfrage des curl-Fortschritts-Callbacks berücksichtigt. |
Siehe auch
- Referenz zu API – Fetch-Hooks – kompakte Signaturreferenz.
- Web-APIs – die vollständige Fetch-Pack-Oberfläche.
- Cookie-Jar – Cookies mit einem PHP oder JS-Jar-Objekt über mehrere Fetch-Aufrufe hinweg speichern.