Fix ladl.link transfer QR redirect loop causing 500 errors.
Deploy Ladill Link / deploy (push) Successful in 1m1s

Proxy transfer paths directly to transfer.ladill.com and stop following ladl.link redirects server-side so recipient links resolve.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-29 20:55:48 +00:00
co-authored by Cursor
parent 62bab93b26
commit c5e92012ff
3 changed files with 117 additions and 29 deletions
+86 -29
View File
@@ -8,45 +8,40 @@ use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpFoundation\Response;
/**
* Proxies ladl.link ecosystem paths to the platform QR resolver (/q/* on ladill.com).
* The browser stays on ladl.link; resolution happens server-side.
* Proxies ladl.link ecosystem paths to the platform QR resolver (/q/* on ladill.com)
* or directly to satellite apps (e.g. transfer.ladill.com). The browser stays on
* ladl.link; resolution happens server-side without redirect loops.
*/
class LinkPlatformProxy
{
public const INTERNAL_HEADER = 'X-Ladill-Internal';
public function forward(Request $request, string $slug, ?string $path = null): Response
{
$normalizedPath = $path !== null ? ltrim($path, '/') : '';
if ($normalizedPath === 'transfer' || str_starts_with($normalizedPath, 'transfer/')) {
return $this->fetch(
$request,
rtrim($this->transferBaseUrl(), '/').'/q/'.$slug.'/'.$normalizedPath,
);
}
$platform = rtrim((string) config('app.platform_url', 'https://ladill.com'), '/');
$target = $platform.'/q/'.$slug;
if ($path !== null && $path !== '') {
$target .= '/'.ltrim($path, '/');
}
if ($qs = $request->getQueryString()) {
$target .= '?'.$qs;
if ($normalizedPath !== '') {
$target .= '/'.$normalizedPath;
}
$method = strtoupper($request->method());
$followRedirects = $method === 'GET' || $method === 'HEAD';
$client = Http::withOptions([
'allow_redirects' => $followRedirects ? ['max' => 5] : false,
])->timeout(15);
$pending = match ($method) {
'POST' => $client->withHeaders($this->forwardHeaders($request))->asForm()->post($target, $request->post()),
'PATCH' => $client->withHeaders($this->forwardHeaders($request))->patch($target, $request->all()),
'DELETE' => $client->withHeaders($this->forwardHeaders($request))->delete($target),
default => $client->withHeaders($this->forwardHeaders($request))->get($target),
};
$upstream = $pending->toPsrResponse();
$response = new Response(
(string) $upstream->getBody(),
$upstream->getStatusCode(),
$this->sanitizeHeaders($upstream->getHeaders())
);
$response = $this->fetch($request, $target);
if ($response->isRedirection()) {
$location = $response->headers->get('Location');
if (is_string($location)) {
if ($followUp = $this->followLadillLinkRedirect($request, $slug, $location)) {
return $followUp;
}
$response->headers->set('Location', $this->rewriteLocation($location, $slug));
}
}
@@ -54,6 +49,62 @@ class LinkPlatformProxy
return $response;
}
private function fetch(Request $request, string $target): Response
{
if ($qs = $request->getQueryString()) {
$target .= (str_contains($target, '?') ? '&' : '?').$qs;
}
$method = strtoupper($request->method());
$client = Http::withOptions(['allow_redirects' => false])
->timeout(15)
->withHeaders($this->forwardHeaders($request));
$pending = match ($method) {
'POST' => $client->asForm()->post($target, $request->post()),
'PATCH' => $client->patch($target, $request->all()),
'DELETE' => $client->delete($target),
default => $client->get($target),
};
$upstream = $pending->toPsrResponse();
return new Response(
(string) $upstream->getBody(),
$upstream->getStatusCode(),
$this->sanitizeHeaders($upstream->getHeaders()),
);
}
private function followLadillLinkRedirect(Request $request, string $slug, string $location): ?Response
{
$parsed = parse_url($location);
if (! is_array($parsed)) {
return null;
}
$host = strtolower((string) ($parsed['host'] ?? ''));
$linkHost = strtolower((string) parse_url(LadillLink::baseUrl(), PHP_URL_HOST));
if ($host !== $linkHost) {
return null;
}
$path = ltrim((string) ($parsed['path'] ?? ''), '/');
$slugPrefix = $slug.'/';
if (! str_starts_with($path, $slugPrefix)) {
return null;
}
return $this->forward($request, $slug, substr($path, strlen($slugPrefix)));
}
private function transferBaseUrl(): string
{
$domain = (string) config('app.transfer_domain', 'transfer.ladill.com');
return 'https://'.$domain;
}
/** @return array<string, string> */
private function forwardHeaders(Request $request): array
{
@@ -62,7 +113,7 @@ class LinkPlatformProxy
'Accept-Language' => $request->header('Accept-Language'),
'User-Agent' => $request->header('User-Agent'),
'Referer' => $request->header('Referer'),
'X-Ladill-Internal' => '1',
self::INTERNAL_HEADER => '1',
]);
}
@@ -87,9 +138,15 @@ class LinkPlatformProxy
if (str_starts_with($location, $prefix)) {
$suffix = substr($location, strlen($prefix));
$rewritten = LadillLink::url($slug).$suffix;
return $rewritten;
return LadillLink::url($slug).$suffix;
}
$transfer = rtrim($this->transferBaseUrl(), '/').'/q/'.$slug;
if (str_starts_with($location, $transfer)) {
$suffix = substr($location, strlen($transfer));
return LadillLink::url($slug).$suffix;
}
return $location;