Saltar al contenido

Transporte Fetch

El fetch() predeterminado de Phasis usa PHP ext-curl: CURLOPT_FOLLOWLOCATION=false (el bucle de redirecciones de la especificación se ejecuta en Phasis) mientras CURLOPT_PROGRESSFUNCTION comprueba AbortSignal. Para la mayoría de las integraciones, esto basta.

Para los demás casos —proxies, DNS personalizados, el cliente HTTP existente de la aplicación host, aislamiento de red y simulaciones en pruebas—, el transporte es conectable.

La interfaz

PHP
public function setFetchTransport(callable $transport): void

Tu función recibe:

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

$request es un array asociativo:

Clave Tipo Notas
method string Ya está en mayúsculas (GET, POST, …)
url string URL parser y cadena de redirecciones resueltos como URL absoluto
headers list<list<string>> Pares de [name, value]; con las mayúsculas y minúsculas normalizadas según la especificación; las entradas con varios valores son pares separados (Set-Cookie, etc.)
body string Bytes sin procesar. Cadena vacía para métodos sin cuerpo.
redirect string "follow" / "error" / "manual" (el transporte NO debe seguirlas: el bucle de redirecciones se ejecuta en Phasis)
timeout int Límite de tiempo real en milisegundos. 0 = sin límite.
credentials string "omit" / "same-origin" / "include" — informativos; las cookies se conectan aparte mediante setCookieJar().

$signal es null si la solicitud no recibió ninguna señal; de lo contrario, es un JsObject que expone aborted y reason. Compruébalo durante transferencias largas.

Formato de retorno:

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

Si el transporte lanza una excepción, Phasis la captura y la transforma:

  • Un Phasis\BuiltIn\Fetch\TransportException con $e->kind === "aborted" → fetch se rechaza con AbortError DOMException.
  • kind === "timeout" → TypeError con el mensaje.
  • kind === "network-error" (predeterminado) → TypeError.
  • Cualquier otro caso → TypeError con el mensaje de la excepción.

Ejemplos prácticos

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

Bloqueo total

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

Esto hace que cada llamada fetch() se rechace con TypeError. Es útil para aislar JS proporcionado por usuarios.

Simulador para pruebas

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

El hook de política

El transporte ejecuta la solicitud; el hook de política decide si se ejecuta. Úsalo para aplicar reglas solo para HTTPS, inyectar encabezados, reescribir URL, gestionar listas de permitidos o vincular credenciales por inquilino.

PHP
public function setFetchPolicy(callable $hook): void

Firma del hook: function (JsObject $request): ?JsObject. Si devuelve null, permite la solicitud tal cual. Si devuelve un Request, la reescribe. Si lanza una excepción, la solicitud fetch se rechaza con el error lanzado.

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

El hook de política se ejecuta antes del transporte, así que ni siquiera un transporte defectuoso o no fiable puede eludirlo.

Resumen del comportamiento predeterminado

Aspecto Valor predeterminado
Transporte CurlTransport (PHP ext-curl)
Redirecciones Sigue hasta 20, según la especificación. redirect: "manual" devuelve la Response de redirección con type: "opaqueredirect".
Tiempo de espera 30 s de tiempo real, 10 s para conectar
Verificación de TLS CURLOPT_SSL_VERIFYPEER = true (sin opción para desactivarla; se cambia sustituyendo el transporte)
User-Agent Phasis/0.1 se inyecta solo si no está definido
Cookies No se almacenan. Usa setCookieJar() para habilitarlas.
Hook de política Ninguno. Se permiten todas las solicitudes salvo que se denieguen explícitamente.
AbortSignal Se respeta durante la solicitud mediante la comprobación de curl en la devolución de llamada de progreso.

Véase también