#Infinite 409 Requests on refresh

1 messages · Page 1 of 1 (latest)

forest kayak
#

I got a weird issue which seems to only be happening on staging, locally it works fine.
If I login to my application and click on the navigation on User I get redirected to /users, if I refresh that page it refreshes and shows the users just as expected locally.

On staging though, if I login and click on User in the Navigation it shows the users, but if I refresh my page I get stuck into an infinite loop, it keeps refreshing with a HTTP 409

Request URL:
https://tenant.test.de/users
Request Method:
GET
Status Code:
409 Conflict
Remote Address:
168.119.252.49:443
Referrer Policy:
strict-origin-when-cross-origin

Any ideas why this could be happening, debugging it already 1-2 days without success.

#

This is the method in my UserController

    public function indexTenant(): Response
    {
        $users = User::with(['roles', 'permissions'])
            ->leftJoin('users as creators', 'users.created_by', '=', 'creators.id')
            ->select('users.*', 'creators.email as created_by')
            ->paginate(25);

        // Add avatar URLs to the users collection
        $users->getCollection()->transform(function ($user) {
            $user->avatar_url = $user->getAvatarUrl();
            return $user;
        });

        return Inertia::render('Tenant/User/Dashboard', [
            'users' => $users,
        ]);
    }
#

And I have this in my User Model:

    protected function generateCacheKey(): string
    {
        return "cloudfront_url:{$this->id}:{$this->avatar}:{$this->updated_at->timestamp}";
    }

    /**
     * Get the presigned URL for the user's avatar.
     *
     * @return string|null
     */
    public function getAvatarUrl(): ?string
    {
        if (!$this->avatar) {
            return null;
        }

        try {
            $distributionDomain = config('services.cloudfront.distribution_domain');

            $cacheKey = $this->generateCacheKey();

            return cache()->remember($cacheKey, 1_800, function () use ($distributionDomain) {
                $cloudFrontClient = new CloudFrontClient([
                    'region' => config('services.aws.region'),
                    'version' => config('services.aws_sdk.version')
                ]);

                $resourceUrl = "https://{$distributionDomain}/{$this->avatar}";
                $expires = time() + 3_600; // URL expires in 1 hour

                return $cloudFrontClient->getSignedUrl([
                    'url' => $resourceUrl,
                    'expires' => $expires,
                    'private_key' => config('services.cloudfront.private_key_path'),
                    'key_pair_id' => config('services.cloudfront.key_pair_id')
                ]);
            });
        } catch (\Exception $e) {
            Log::error('Failed to generate signed CloudFront URL: ' . $e->getMessage());

            return "https://{$distributionDomain}/{$this->avatar}?v=" . $this->updated_at->timestamp;
        }
    }
#

This is confusing me so much I am not sure what I am missing.

#

Debugging this since 2 days 😄

#

Probably going to try react query or something not sure how to further debug with 409 issue

#

If someone could help I would really appreciate it

#

User Model related function:


    protected function generateCacheKey(): string
    {
        return "cloudfront_url:{$this->id}:{$this->avatar}:{$this->updated_at->timestamp}";
    }

    /**
     * Get the presigned URL for the user's avatar.
     *
     * @return string|null
     */
    public function getAvatarUrl(): ?string
    {
        if (!$this->avatar) {
            return null;
        }

        try {
            $distributionDomain = config('services.cloudfront.distribution_domain');

            $cacheKey = $this->generateCacheKey();

            return cache()->remember($cacheKey, 1_800, function () use ($distributionDomain) {
                $cloudFrontClient = new CloudFrontClient([
                    'region' => config('services.aws.region'),
                    'version' => config('services.aws_sdk.version')
                ]);

                $resourceUrl = "https://{$distributionDomain}/{$this->avatar}";
                $expires = time() + 3_600; // URL expires in 1 hour

                return $cloudFrontClient->getSignedUrl([
                    'url' => $resourceUrl,
                    'expires' => $expires,
                    'private_key' => config('services.cloudfront.private_key_path'),
                    'key_pair_id' => config('services.cloudfront.key_pair_id')
                ]);
            });
        } catch (\Exception $e) {
            Log::error('Failed to generate signed CloudFront URL: ' . $e->getMessage());

            return "https://{$distributionDomain}/{$this->avatar}?v=" . $this->updated_at->timestamp;
        }
    }
}
lament current
#

There's only 2 cases where Inertia would use a 409 response. 1 would be when using an Inertia::location() response, which likely isn't the case as you're not doing that.
The other is asset versioning. Inertia would force a refresh whenever the asset version does not match between the client and the server, thus a full reload would then use the new assets. So likely you're doing something wrong there, and it could be quite a few reasons, such as using a CDN, or using multiple versions of Inertia on the client (which you shouldn't do)

forest kayak
#

I am using a CDN Cloudfront, I think I am not using multi version of Inertia on the client

lament current
#

So likely you've set the ASSET_URL then, which means you'd need to overwrite the version method in your Inertia middleware, and return something to determine which asset version you're on

forest kayak
#

Hmm where would I have the ASSET_URL set? How can I overwrite the version

lament current
#

I just mentioned where? 💀

forest kayak
#

the cdn? Sorry it has been a long day 😄

#

oh in the HandleInertiaRequests middleware you mean

#

so probably this part

    public function version(Request $request)
    {
        return parent::version($request);
    }
#

I think that was not it, atleast this did not work:

    public function version() {
        return env('ASSET_VERSION', '1.0');
    }
Symfony\Component\ErrorHandler\Error\FatalError
Declaration of App\Http\Middleware\HandleInertiaRequests::version() must be compatible with Inertia\Middleware::version(Illuminate\Http\Request $request)

Going to check this out after a nap again.

forest kayak
#

@lament current can you help me, I am not sure if I am just not missing the place

#

Like the ASSET_URL, I am not using that in my .env?

lament current
#

So what exactly does your hosting setup look like? What version numbers are you seeing in the requests/responses?

forest kayak
#

I am using Laravel Forge with a Hetzner VPS, the databases were configured automatically, I am using AWS S3 and Cloudfront as a CDN, I am using some signed url logic since I do not want to expose my assets public.

#

I am using Laravel, Inertia with React

By version number you mean logging this?

    public function version(Request $request)
    {
        dd(parent::version($request))
        return parent::version($request);
    }
#

If I run this

    public function version(Request $request)
    {
        dd(parent::version($request));
        return parent::version($request);
    }

locally it returns null,

if I run it on production I get this:

45d3325116a779dd0757e45013ffe07e

#

If I refresh it is still this on prod (server on laravel forge):

45d3325116a779dd0757e45013ffe07e

Seems to be always the same

lament current
#

And what do your requests/responses show?

forest kayak
#

the 409 does not show a preview nor a response just the headers:

Request URL:
https://tenant.mydomain.de/users
Request Method:
GET
Status Code:
409 Conflict
Remote Address:
168.119.252.49:443
Referrer Policy:
strict-origin-when-cross-origin
cache-control:
no-cache, private
content-type:
text/html; charset=UTF-8
date:
Wed, 30 Oct 2024 10:55:56 GMT
server:
nginx
set-cookie:
XSRF-TOKEN=eyJpdiI6InRnN2NEc0g0Z09YQ2s2Uy9tRXUxNkE9PSIsInZhbHVlIjoiT2VhOTNNcFVuS2RyUGI1Tnl1Nms5WnFyYXVnZnZFVUcyVVpXbisvT1VRdFBibFVOdVZ0TDdlcHpjeGRRUmh2NWZNaTFGNDJJVnZlNUtGSUJZb080Ym1UV3VqRW1CTDZKaVRKMGxFQnVFNXA3STZ6aWJ0NTByU2dYeGFSVjE1WFUiLCJtYWMiOiI1ZmZlODZjMTc3NTFkOGRjYWVhMzY2NjI3MmI1Y2RmZmI5MzZlMWU4ZTAzNjY2MjdhNjk4YzcwZDVmNzM3ZjQxIiwidGFnIjoiIn0%3D; expires=Wed, 30 Oct 2024 12:55:56 GMT; Max-Age=7200; path=/; secure; samesite=lax
set-cookie:
akafo_global_session=eyJpdiI6IlpPUmRFOW1maHR5Zk5FeFh2ak9oTXc9PSIsInZhbHVlIjoiaGhPODNSdFdnN2t5RkdVeVFoUkZpcVJmbU1JUy9RY1UvN3BtRkpCa1dlV0tjbjdrUk9oVW5WbWtLSWlScGJiOVQ4SVlmcEtGTkNsZnNFMTNsclBSWVYrVkduNFAwUE1ic2M5eE1YSVR4L1dGVm5BaWFqYTA4bHRITHFuOVFTdHIiLCJtYWMiOiJlZDU2MWM0MThmMTI3MWYxMjEzMTYwNmUzZDE5MzNiOGI5MTZjZTgzOGQyNWRjYzNiMDUzOWRiNzc2ZDc4M2Q0IiwidGFnIjoiIn0%3D; expires=Wed, 30 Oct 2024 12:55:56 GMT; Max-Age=7200; path=/; secure; httponly; samesite=lax
x-inertia-location:
https://tenant.mydomain.de/users
#
:authority:
tenant.mydomain.de
:method:
GET
:path:
/users
:scheme:
https
accept:
text/html, application/xhtml+xml
accept-encoding:
gzip, deflate, br, zstd
accept-language:
de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7,uk;q=0.6,tr;q=0.5
authorization:
Basic YWthZm9lX2FkbWluOnJlN2tmQ2NUR1FEeG55S3VhVEZlb21xVjJCS3RyOA==
cache-control:
no-cache
content-type:
application/json
cookie:
XSRF-TOKEN=eyJpdiI6InNSTlp6bG9lY1E4ZmR0eDJyWDN5L0E9PSIsInZhbHVlIjoiUWdiNEZOZ0tCS29oV3Q3UWVwbXp3ZkRYeVZzeFNsdlEvSE5FT2JKM1lablo3RTg4Y3lzNnJJZVdWQTNZYkZ3RHAyNjRTeGVSMi8wQkxLSVJxWFR5RWYvb2wzZ0tYWkEvWTcrTU5MWEZhOVBKam1FN2dRNjZ0eTFjR2VUK1cwYm0iLCJtYWMiOiI4NzgzMGE5Yzg3YTFmZDg5OGI2YTNjYTA1OTVlYjhjNDU5YjA1Y2EzNzg1ZjZlZWM3ZjhkMzE2Y2E1ZmI4Y2QyIiwidGFnIjoiIn0%3D; akafo_global_session=eyJpdiI6IlVCcERqcGwwWks0b2cxc2U0MVZvRGc9PSIsInZhbHVlIjoiRTF5aVRhQ1lpQnp5UHhvUkhjMHdjenF3TW5lR1V1YVJ4MkVuNnRRLzdjZWdsRGRsa2s5a1NjaWljb01PUHlXbG5vRE5aTFVWQTIrY0JZc0hYckhnTjBXRXBCZER6Vk9yYlRKVVFha1grQ0FLZXBuU01od0FndDJ3VTN5L0NCOXoiLCJtYWMiOiJhNjJmYTA4YTQ4YWY4M2U0NTBlMjAxYjhmM2Q0OTgxOGFiYmEzNDE3ZDY3MjgyYzUwY2RmNDI5NWU3ZTk0MGE5IiwidGFnIjoiIn0%3D
dnt:
1
pragma:
no-cache
priority:
u=1, i
referer:
https://tenant.mydomain.de/users
sec-ch-ua:
"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"
sec-ch-ua-mobile:
?0
sec-ch-ua-platform:
"macOS"
sec-fetch-dest:
empty
sec-fetch-mode:
cors
sec-fetch-site:
same-origin
user-agent:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
x-inertia:
true
x-requested-with:
XMLHttpRequest
x-xsrf-token:
eyJpdiI6InNSTlp6bG9lY1E4ZmR0eDJyWDN5L0E9PSIsInZhbHVlIjoiUWdiNEZOZ0tCS29oV3Q3UWVwbXp3ZkRYeVZzeFNsdlEvSE5FT2JKM1lablo3RTg4Y3lzNnJJZVdWQTNZYkZ3RHAyNjRTeGVSMi8wQkxLSVJxWFR5RWYvb2wzZ0tYWkEvWTcrTU5MWEZhOVBKam1FN2dRNjZ0eTFjR2VUK1cwYm0iLCJtYWMiOiI4NzgzMGE5Yzg3YTFmZDg5OGI2YTNjYTA1OTVlYjhjNDU5YjA1Y2EzNzg1ZjZlZWM3ZjhkMzE2Y2E1ZmI4Y2QyIiwidGFnIjoiIn0=
#

These are the headers ⏫

lament current
#

Could've just only send the things relevant to the version...

forest kayak
#

I can visit the /users page in my navigation which works fine, but once I refresh I am stuck in a 409 loop and it refreshes

#

sorry

lament current
#

Does the request include the X-Inertia-Version header or not?

#

Looks like it doesn't, so then it's probably because you're doing something wrong on the frontend. How are you making the request? And which Inertia version are you using?

forest kayak
#

if I click on the navigation yes:

x-inertia-version:
45d3325116a779dd0757e45013ffe07e

if I refresh no

#

"@inertiajs/react": "^1.2.0",

lament current
#

if I refresh no
Meaning, a browser refresh? Yeah, it wouldn't ever send that then, because the browser doesn't magically send it

forest kayak
#

I am doing this:

    public function indexTenant(): Response
    {
        $users = User::with(['roles', 'permissions'])
            ->leftJoin('users as creators', 'users.created_by', '=', 'creators.id')
            ->select('users.*', 'creators.email as created_by')
            ->paginate(25);

        // Add avatar URLs to the users collection
        $users->getCollection()->transform(function ($user) {
            $user->avatar_url = $user->getAvatarUrl();
            return $user;
        });

        return Inertia::render('Tenant/User/Dashboard', [
            'users' => $users,
        ]);
    }

Then in my Dashboard

export default function UserDashboard(props: DashboardProps) {
  const { users, auth } = props

And map through the users

lament current
#

That doesn't matter

#

I already said that, since that doesn't return a 409 response.

#

Maybe just send the URL or something, so I could take a look, cuz debugging this way is incredibly difficult

forest kayak
#

yeah can I pm u since I have a htaccess in front

lament current
#

Yeah no, ain't gonna work, since I don't have a static IP (and I'm not giving that anyways)

forest kayak
#

ok I remove the htaccess and give you a test login then?

lament current
#

So what exactly is your page doing? I see it's doing quite some weird stuff. When you navigate using Inertia, it loads the page and afterwards sends another request, so loading the same data twice. On reload the same thing happens, except the initial request succeeds, while one made on-load doesn't include a version.
So it kinda looks like you have a mounted hook, in which you perform a reload (for some reason), and perhaps with a different instance of inertia, which could happen if you have two versions of Inertia installed

forest kayak
#

ok let me check, so most likely a version issue or frontend issue right?

lament current
#

Probably frontend issue

forest kayak
#

my package json only holds one inertia version

#

so probably a react issue

lament current
#

Like, code issue in the page component

forest kayak
#

yeah

#

let me check, ill let you know once I fixed that, thats help alot so far

#

thank you

forest kayak
#

awesome it worked

#

I had a search component which called the same user route initially

#

so that was the issue

#

not 100% sure though why I could not reproduce this on my end though

lament current
#

Also odd that it doesn't send the version along, but I guess it was being called before Inertia was booted or something? 🤷‍♂️

forest kayak
#

thanks again for the nice support

#

❤️