Zum Inhalt springen

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

PHP
public function setFetchTransport(callable $transport): void

Deine aufrufbare Funktion erhält:

PHP
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:

PHP
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\TransportException mit $e->kind === "aborted" → fetch löst eine Ablehnung mit AbortError DOMException aus.
  • kind === "timeout" → TypeError mit der Meldung.
  • kind === "network-error" (Standard) → TypeError.
  • Alles andere → TypeError mit der Fehlermeldung.

Beispiele

Guzzle

PHP
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

PHP
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

PHP
$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

PHP
$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.

PHP
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.

PHP
$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