Files
nimbus/src/Modules/Relay/Actions/RequestRelayAction.php
Mazen Touati 64ef46a8a4 fix(relay): properly relay request paramters (#28)
also:
- improve the relay test.
- remove unnecessary exemption of the content-type header from the headers list.
2026-01-03 16:48:30 +01:00

150 lines
5.2 KiB
PHP

<?php
namespace Sunchayn\Nimbus\Modules\Relay\Actions;
use Carbon\CarbonImmutable;
use GuzzleHttp\Cookie\SetCookie;
use Illuminate\Container\Container;
use Illuminate\Cookie\CookieValuePrefix;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers\AuthorizationHandlerFactory;
use Sunchayn\Nimbus\Modules\Relay\DataTransferObjects\RelayedRequestResponseData;
use Sunchayn\Nimbus\Modules\Relay\DataTransferObjects\RequestRelayData;
use Sunchayn\Nimbus\Modules\Relay\ValueObjects\PrintableResponseBody;
use Sunchayn\Nimbus\Modules\Relay\ValueObjects\ResponseCookieValueObject;
class RequestRelayAction
{
public const DEFAULT_CONTENT_TYPE = 'application/json';
public const MICROSECONDS_TO_MILLISECONDS = 1_000_000;
public const NON_STANDARD_STATUS_CODES = [
419 => 'Method Not Allowed',
];
public function __construct(
private readonly Container $container,
private readonly AuthorizationHandlerFactory $authorizationHandlerFactory,
) {}
public function execute(RequestRelayData $requestRelayData): RelayedRequestResponseData
{
$pendingRequest = $this->prepareRequest($requestRelayData);
$authorizationHandler = $this
->authorizationHandlerFactory
->create($requestRelayData->authorization);
$pendingRequest = $authorizationHandler->authorize($pendingRequest);
$start = hrtime(true);
$response = $pendingRequest->send(
method: $requestRelayData->method,
url: $requestRelayData->endpoint,
);
$durationInMs = $this->calculateDuration($start);
return new RelayedRequestResponseData(
statusCode: $response->getStatusCode(),
statusText: $this->getStatusTextFromCode($response->getStatusCode()),
body: PrintableResponseBody::fromResponse($response),
headers: $response->getHeaders(),
durationMs: $durationInMs,
timestamp: CarbonImmutable::now()->getTimestamp(),
cookies: $this->processCookies($response->getHeader('Set-Cookie')),
);
}
private function prepareRequest(RequestRelayData $requestRelayData): PendingRequest
{
$contentType = $requestRelayData->headers['content-type'] ?? self::DEFAULT_CONTENT_TYPE;
$queryParameters = $requestRelayData->queryParameters;
$requestBody = $requestRelayData->body;
if (in_array($requestRelayData->method, ['get', 'head'])) {
$queryParameters = array_merge(
$queryParameters,
$requestBody,
);
$requestBody = [];
}
// SSL verification is disabled to support development environments with self-signed certificates.
return Http::withoutVerifying()
->withHeaders($requestRelayData->headers)
->withQueryParameters($queryParameters)
->when(
$requestBody !== [],
fn (PendingRequest $pendingRequest) => $pendingRequest->withBody(
json_encode($requestRelayData->body) ?: throw new RuntimeException('Cannot parse body.'),
contentType: $contentType,
),
);
}
/**
* Calculates request duration in milliseconds with precision formatting.
*
* Uses high-resolution time for accurate microsecond measurements,
* formatted to 2 decimal places for readability.
*/
private function calculateDuration(int $startTime): float
{
return (float) number_format(
(hrtime(true) - $startTime) / self::MICROSECONDS_TO_MILLISECONDS,
2,
thousands_separator: ''
);
}
/**
* Processes Set-Cookie headers into structured cookie objects.
*
* Cookies are URL-decoded and prefixed with Laravel's encryption key
* to match the application's cookie handling behavior.
*
* @param string[] $setCookieHeaders
* @return ResponseCookieValueObject[]
*/
private function processCookies(array $setCookieHeaders): array
{
return Arr::map(
$setCookieHeaders,
function (string $cookieString): ResponseCookieValueObject {
$setCookie = SetCookie::fromString($cookieString);
return new ResponseCookieValueObject(
key: $setCookie->getName(),
rawValue: urldecode($setCookie->getValue() ?? ''),
prefix: CookieValuePrefix::create(
$setCookie->getName(),
$this->container->get('encrypter')->getKey()
),
);
},
);
}
/**
* Maps HTTP status codes to human-readable status text.
*
* Includes Laravel-specific status codes like 419 (Method Not Allowed)
* that aren't part of the standard HTTP specification.
*/
private function getStatusTextFromCode(int $statusCode): string
{
$statusCodeToTextMapping = Response::$statusTexts + self::NON_STANDARD_STATUS_CODES;
return $statusCodeToTextMapping[$statusCode] ?? 'Non-standard Status Code.';
}
}