Aller au contenu

Transport Fetch

Par défaut, le fetch() de Phasis passe par le ext-curl de PHP — CURLOPT_FOLLOWLOCATION=false (la boucle de redirection conforme à la spécification s’exécute côté Phasis), tandis que CURLOPT_PROGRESSFUNCTION interroge le AbortSignal. Cela convient à la plupart des intégrations.

Pour tous les autres cas — proxys, DNS personnalisés, client HTTP existant de l’application hôte, isolation du réseau ou simulation dans les tests — le transport est remplaçable.

Interface

PHP
public function setFetchTransport(callable $transport): void

Votre fonction reçoit :

PHP
function (array $request, ?\Phasis\Value\JsObject $signal): array

$request est un tableau associatif :

Clé Type Remarques
method string Déjà en majuscules (GET, POST, …)
url string URL résolue en valeur absolue après l’analyseur URL et la chaîne de redirections
headers list<list<string>> Paires de [name, value] ; casse normalisée selon la spécification ; les entrées à valeurs multiples forment des paires distinctes (Set-Cookie, etc.)
body string Octets bruts. Chaîne vide pour les méthodes sans corps.
redirect string "follow" / "error" / "manual" (votre transport ne doit PAS suivre les redirections — la boucle s’exécute côté Phasis)
timeout int Budget en temps réel, en millisecondes. 0 = aucune limite.
credentials string "omit" / "same-origin" / "include" — à titre indicatif ; les cookies sont gérés séparément via setCookieJar().

$signal vaut null si aucun signal n’a été fourni avec la requête ; sinon, c’est un JsObject exposant aborted et reason. Interrogez-le pendant les transferts longs.

Format de retour :

PHP
return [
    'status'     => 200,                          // int
    'statusText' => 'OK',                         // string
    'headers'    => [['content-type', 'text/plain'], ['x-custom', '1']],
    'body'       => '...response bytes...',       // string
];

Si le transport lance une exception, Phasis l’intercepte et la convertit :

  • Un Phasis\BuiltIn\Fetch\TransportException avec $e->kind === "aborted" → fetch est rejeté avec AbortError DOMException.
  • kind === "timeout" → TypeError contenant le message.
  • kind === "network-error" (par défaut) → TypeError.
  • Toute autre exception → TypeError avec le message de l’exception.

Exemples détaillés

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),
    ];
});

Tout refuser

PHP
$engine->setFetchTransport(function () {
    throw new \Phasis\BuiltIn\Fetch\TransportException(
        'network access disabled in this sandbox',
        'network-error',
    );
});

Chaque appel fetch() est alors rejeté avec TypeError. Pratique pour isoler du JS fourni par l’utilisateur.

Simulateur pour les 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'],
    ];
});

Crochet de politique

Le transport exécute la requête ; le crochet de politique décide de l’exécuter ou non. Utilisez-le pour n’autoriser que HTTPS, ajouter des en-têtes, réécrire URL, définir des listes d’autorisation ou associer des identifiants par locataire.

PHP
public function setFetchPolicy(callable $hook): void

Signature du crochet : function (JsObject $request): ?JsObject. Renvoyer null autorise la requête telle quelle. Renvoyer une Request la réécrit. Une exception entraîne le rejet de fetch avec l’erreur lancée.

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;
});

Le crochet de politique s’exécute avant le transport — même un transport défectueux ou non fiable ne peut donc pas le contourner.

Résumé du comportement par défaut

Aspect Par défaut
Transport CurlTransport (PHP ext-curl)
Redirections Suivies jusqu’à 20, conformément à la spécification. redirect: "manual" renvoie la réponse de redirection avec type: "opaqueredirect".
Délai d’expiration 30 s en temps réel, 10 s pour la connexion
Vérification de TLS CURLOPT_SSL_VERIFYPEER = true (aucune désactivation possible — remplacez le transport pour modifier ce comportement)
User-Agent Phasis/0.1 ajouté uniquement s’il est absent
Cookies Aucun stockage. Utilisez setCookieJar() pour l’activer.
Crochet de politique Aucun. Toutes les requêtes sont autorisées, sauf refus explicite.
AbortSignal Pris en compte en cours de transfert via une interrogation du rappel de progression curl.

Voir aussi